6internal static class NitroFileSystemParser
9 private const ushort RootDirectoryId = 0xF000;
16 public static NdsFileSystem Parse(
17 IImageDataSource source,
19 NdsReadOptions options)
21 (
byte[] fnt,
byte[] fat) = ReadTables(source, header, options);
22 return ParseTables(source, fnt, fat, options);
31 public static async ValueTask<NdsFileSystem> ParseAsync(
32 IImageDataSource source,
34 NdsReadOptions options,
35 CancellationToken cancellationToken)
37 ValidateTableLengths(header, options);
38 byte[] fnt =
new byte[header.FileNameTable.Length];
39 byte[] fat =
new byte[header.FileAllocationTable.Length];
40 await source.ReadExactlyAsync(
41 header.FileNameTable.Offset,
43 cancellationToken).ConfigureAwait(
false);
44 await source.ReadExactlyAsync(
45 header.FileAllocationTable.Offset,
47 cancellationToken).ConfigureAwait(
false);
48 return ParseTables(source, fnt, fat, options);
56 private static (
byte[] Fnt,
byte[] Fat) ReadTables(
57 IImageDataSource source,
59 NdsReadOptions options)
61 ValidateTableLengths(header, options);
62 byte[] fnt =
new byte[header.FileNameTable.Length];
63 byte[] fat =
new byte[header.FileAllocationTable.Length];
64 source.ReadExactly(header.FileNameTable.Offset, fnt);
65 source.ReadExactly(header.FileAllocationTable.Offset, fat);
75 private static NdsFileSystem ParseTables(
76 IImageDataSource source,
79 NdsReadOptions options)
81 NdsFileAllocation[] allocations = ParseAllocations(source.Length, fat);
84 var emptyRoot =
new NdsDirectory(RootDirectoryId,
string.Empty,
"/",
null);
85 return new(emptyRoot, [emptyRoot], [], allocations);
90 throw new InvalidDataException(
"The NitroFS filename table is smaller than its root directory record.");
93 int directoryCount = NdsBinary.ReadUInt16(fnt, 6);
94 if (directoryCount == 0 || directoryCount > options.MaximumDirectoryCount || directoryCount > fnt.Length / 8)
96 throw new InvalidDataException($
"The NitroFS directory count {directoryCount} is invalid.");
99 var directoriesById =
new Dictionary<ushort, NdsDirectory>();
100 var files =
new List<NdsFile>();
101 var visiting =
new HashSet<ushort>();
102 NdsDirectory root = ReadDirectory(RootDirectoryId,
string.Empty,
"/",
null, 0);
103 if (directoriesById.Count != directoryCount)
105 throw new InvalidDataException(
106 $
"The NitroFS declares {directoryCount} directories, but {directoriesById.Count} are reachable from the root.");
109 NdsDirectory[] directories = directoriesById.Values.OrderBy(
static directory => directory.Id).ToArray();
110 NdsFile[] orderedFiles = files.OrderBy(
static file => file.Id).ToArray();
111 return new(root, directories, orderedFiles, allocations);
115 NdsDirectory ReadDirectory(
119 NdsDirectory? parent,
122 if (depth > options.MaximumDirectoryDepth || directoryId < RootDirectoryId)
124 throw new InvalidDataException($
"NitroFS directory 0x{directoryId:X4} exceeds the configured limits.");
127 int index = directoryId - RootDirectoryId;
128 if (index >= directoryCount || !visiting.Add(directoryId) || directoriesById.ContainsKey(directoryId))
130 throw new InvalidDataException($
"NitroFS directory 0x{directoryId:X4} is invalid, duplicated, or cyclic.");
133 int recordOffset = index * 8;
134 int subTableOffset = checked((
int)NdsBinary.ReadUInt32(fnt, recordOffset));
135 int fileId = NdsBinary.ReadUInt16(fnt, recordOffset + 4);
136 ushort recordedParent = NdsBinary.ReadUInt16(fnt, recordOffset + 6);
137 ushort expectedParent = parent?.Id ?? checked((ushort)directoryCount);
138 if (recordedParent != expectedParent || subTableOffset < directoryCount * 8 || subTableOffset >= fnt.Length)
140 throw new InvalidDataException($
"NitroFS directory record 0x{directoryId:X4} is inconsistent.");
143 var directory =
new NdsDirectory(directoryId, name, fullPath, parent);
144 directoriesById.Add(directoryId, directory);
145 var childDirectories =
new List<NdsDirectory>();
146 var childFiles =
new List<NdsFile>();
147 var childNames =
new HashSet<string>(StringComparer.Ordinal);
148 int cursor = subTableOffset;
149 while (cursor < fnt.Length)
151 byte descriptor = fnt[cursor++];
154 directory.SetChildren(childDirectories, childFiles);
155 visiting.Remove(directoryId);
159 bool isDirectory = (descriptor & 0x80) != 0;
160 int nameLength = descriptor & 0x7F;
161 if (nameLength == 0 || cursor > fnt.Length - nameLength)
163 throw new InvalidDataException($
"NitroFS directory 0x{directoryId:X4} contains a truncated name.");
166 string childName = Encoding.Latin1.GetString(fnt.AsSpan(cursor, nameLength));
167 cursor += nameLength;
168 ValidateName(childName, childNames);
169 string childPath = fullPath ==
"/" ?
"/" + childName : fullPath +
"/" + childName;
172 if (cursor > fnt.Length - 2)
174 throw new InvalidDataException($
"NitroFS child directory '{childPath}' has no directory ID.");
177 ushort childId = NdsBinary.ReadUInt16(fnt, cursor);
179 childDirectories.Add(ReadDirectory(childId, childName, childPath, directory, depth + 1));
183 if ((uint)fileId >= allocations.Length)
185 throw new InvalidDataException($
"NitroFS file '{childPath}' references missing FAT ID {fileId}.");
188 NdsFileAllocation allocation = allocations[fileId];
189 var file =
new NdsFile(source, fileId, childName, childPath, allocation.Data, directory);
190 childFiles.Add(file);
196 throw new InvalidDataException($
"NitroFS directory 0x{directoryId:X4} has no terminator.");
204 private static NdsFileAllocation[] ParseAllocations(
long imageLength, ReadOnlySpan<byte> fat)
206 if (fat.Length % 8 != 0)
208 throw new InvalidDataException(
"The NitroFS file allocation table length is not a multiple of eight.");
211 var allocations =
new NdsFileAllocation[fat.Length / 8];
212 for (
int fileId = 0; fileId < allocations.Length; fileId++)
214 int offset = fileId * 8;
215 uint start = NdsBinary.ReadUInt32(fat, offset);
216 uint end = NdsBinary.ReadUInt32(fat, offset + 4);
217 if (end < start || end > imageLength)
219 throw new InvalidDataException($
"NitroFS FAT entry {fileId} lies outside the image.");
222 allocations[fileId] =
new(fileId,
new(start, end - start));
231 private static void ValidateTableLengths(NdsHeader header, NdsReadOptions options)
233 if (header.FileNameTable.Length > options.MaximumFileNameTableBytes ||
234 header.FileAllocationTable.Length > options.MaximumFileAllocationTableBytes ||
235 header.FileNameTable.Length > Array.MaxLength ||
236 header.FileAllocationTable.Length > Array.MaxLength)
238 throw new InvalidDataException(
"A NitroFS table exceeds the configured parsing limits.");
241 if (header.FileNameTable.IsEmpty && !header.FileAllocationTable.IsEmpty)
243 throw new InvalidDataException(
"A NitroFS allocation table cannot be interpreted without a filename table.");
250 private static void ValidateName(
string name, HashSet<string> siblingNames)
252 if (
string.IsNullOrEmpty(name) ||
253 name is
"." or
".." ||
254 name.Contains(
'/', StringComparison.Ordinal) ||
255 name.Contains(
'\\', StringComparison.Ordinal))
257 throw new InvalidDataException(
"NitroFS contains an unsafe or empty entry name.");
260 if (!siblingNames.Add(name))
262 throw new InvalidDataException($
"NitroFS contains duplicate sibling name '{name}'.");