private static TreeNode treeNodeFromDir(FilesystemDirectory d) { TreeNode root = new TreeNode(d.name) { Tag = d }; foreach (var c in d.children) { if (c is FilesystemDirectory childDir) { root.Nodes.Add(treeNodeFromDir(childDir)); } else { TreeNode child = new TreeNode(c.name) { Tag = c }; root.Nodes.Add(child); } } root.Expand(); return(root); }
public void Should_represent_directory() { var sut = new FilesystemDirectory(ResourceHelpers.GetResourceDirectoryInfo()); sut.IsDirectory.Should().BeTrue(); sut.Name.Should().Be("Resources"); }
public static void readRootFST(FilesystemDirectory fs, WrappedInputStream s, int fstOffset, int fstSize, int virtualOffset) { //TODO: Check against FST max size in header, and also Wii RAM limit (since this is also used on Wii I think, otherwise I'd say GameCube RAM limit) (88MB for Wii, 24MB system / 43MB total for GameCube) as it would be impossible for real hardware to read a bigger filesystem //TODO: Wii shifts offset by one long origPos = s.Position; Dictionary <int, FilesystemDirectory> parentDirectories = new Dictionary <int, FilesystemDirectory> { { 0, fs } }; try { s.Position = fstOffset + 8; //1 byte flag at fstOffset + 0: Filesystem root _must_ be a directory //Name offset is irrelevant since the root doesn't really have a name (I think it's just set to 0 anyway) //Parent index would also be irrelevant because it doesn't have a parent by definition //TODO: Throw error or something if not a directory //TODO: Throw error if number of entries * 12 > fstSize int numberOfEntries = s.readIntBE(); int fntOffset = fstOffset + (numberOfEntries * 12); int fntSize = fstSize - ((numberOfEntries) * 12); s.Position = fntOffset; byte[] fnt = s.read(fntSize); s.Position = fstOffset + 12; //Due to weirdness, we need to figure out which directories these files go in afterwards. It's really weird. I just hope it makes enough sense for whatever purpose you're reading this code for. Dictionary <int, FilesystemFile> filesToAdd = new Dictionary <int, FilesystemFile>(); Dictionary <int, int> directoryNextIndexes = new Dictionary <int, int> { { 0, numberOfEntries } }; for (int i = 0; i < numberOfEntries - 1; ++i) { byte[] entry = s.read(12); readFileEntry(entry, fnt, virtualOffset, parentDirectories, i + 1, filesToAdd, directoryNextIndexes); } //Now that we have the directory structure, add the files to them //This sucks, by the way //Like it's not that my code sucks (although it probably kinda does), it's like... I hate the GameCube filesystem foreach (var fileToAdd in filesToAdd) { int fileIndex = fileToAdd.Key; FilesystemFile file = fileToAdd.Value; for (int potentialParentIndex = fileIndex - 1; potentialParentIndex >= 0; potentialParentIndex--) { if (directoryNextIndexes.ContainsKey(potentialParentIndex)) { if (directoryNextIndexes[potentialParentIndex] > fileIndex) { var parentDir = parentDirectories[potentialParentIndex]; parentDir.addChild(file); break; } } } } } finally { s.Position = origPos; } }
public static void readFileEntry(byte[] entry, byte[] fnt, int virtualOffset, Dictionary <int, FilesystemDirectory> parentDirs, int index, Dictionary <int, FilesystemFile> filesToAdd, Dictionary <int, int> directoryNextIndexes) { bool isDirectory = entry[0] > 0; //FIXME: Should check this type flag, sometimes garbage files end up here where they have neither 0 (file) or 1 (directory). It seems to be on beta discs and such so it's probably caused by incorrect header entries causing the FST parsing to stuff up int filenameOffset = (entry[1] << 16) | (entry[2] << 8) | entry[3]; string name = getNullTerminatedString(fnt, filenameOffset); if (isDirectory) { var dir = new FilesystemDirectory { name = name }; parentDirs.Add(index, dir); int parentIndex = (entry[4] << 24) | (entry[5] << 16) | (entry[6] << 8) | entry[7]; int nextIndex = (entry[8] << 24) | (entry[9] << 16) | (entry[10] << 8) | entry[11]; //If I'm understanding all this correctly (I probably am not): The next index out of all the indexes that isn't a child of this directory; i.e. all indexes between here and next index are in this directory directoryNextIndexes.Add(index, nextIndex); //TODO: There has never been a case where parentDirs doesn't contain parentIndex other than the aforementioned garbage files, but we really should make this more robust parentDirs[parentIndex].addChild(dir); } else { int fileOffset = (entry[4] << 24) | (entry[5] << 16) | (entry[6] << 8) | entry[7]; int fileLength = (entry[8] << 24) | (entry[9] << 16) | (entry[10] << 8) | entry[11]; var file = new FilesystemFile { name = name, offset = fileOffset + virtualOffset, size = fileLength }; filesToAdd.Add(index, file); } }
public async Task Should_not_throw_exception_when_directory_not_present() { var resourceDirectory = new FilesystemDirectory(ResourceHelpers.GetResourceDirectoryInfo()); var sut = resourceDirectory.GetDirectory($"does_not_exist_{Guid.NewGuid()}"); await sut.DeleteAsync(); }
public void Should_list_all_nodes() { var sut = new FilesystemDirectory(ResourceHelpers.GetResourceDirectoryInfo("level1")); var nodes = sut.ToList(); nodes.Any(x => x.Name == "level2").Should().BeTrue(); nodes.Any(x => x.Name == "level1content.txt").Should().BeTrue(); }
public static FilesystemDirectory readGamecubeFS(WrappedInputStream s, int fstOffset, int fstSize, int virtualOffset) { FilesystemDirectory fs = new FilesystemDirectory { name = "GameCube Filesystem" }; readRootFST(fs, s, fstOffset, fstSize, virtualOffset); return(fs); }
public async Task Should_be_able_to_recurse_down_to_deepest_level() { var sut = new FilesystemDirectory(ResourceHelpers.GetResourceDirectoryInfo()); var deepestDirectory = (await(await sut.GetDirectoriesAsync()).First().GetDirectoriesAsync()).First(); deepestDirectory.Name.Should().Be("level2"); (await deepestDirectory.GetFilesAsync()).First().Name.Should().Be("level2content.txt"); }
public static FilesystemDirectory readNitroFS(WrappedInputStream s, uint fatOffset, uint fatSize, uint fntOffset, uint fntSize) { //Number of directories = fnt[6], but we don't need that info FilesystemDirectory fs = new FilesystemDirectory { name = "NitroFS" }; readNitroMainFNT(fs, s, fatOffset, fatSize, fntOffset, fntSize, 0); return(fs); }
private static void iterateRomFSEntry(WrappedInputStream s, FilesystemDirectory currentDirectory, byte[] metadataEntry, long directoryMetadataOffset, long fileMetadataOffset, long fileOffset) { uint firstChildDirectoryOffset = BitConverter.ToUInt32(metadataEntry, 8); uint firstFileOffset = BitConverter.ToUInt32(metadataEntry, 12); if (firstChildDirectoryOffset != 0xffffffff) { s.Position = directoryMetadataOffset + firstChildDirectoryOffset; while (true) { byte[] childDirectoryMetadata = s.read(24); uint nextSiblingDirectory = BitConverter.ToUInt32(childDirectoryMetadata, 4); uint nameLength = BitConverter.ToUInt32(childDirectoryMetadata, 20); string name = s.read((int)nameLength, Encoding.Unicode); FilesystemDirectory childDir = new FilesystemDirectory() { name = name }; currentDirectory.addChild(childDir); iterateRomFSEntry(s, childDir, childDirectoryMetadata, directoryMetadataOffset, fileMetadataOffset, fileOffset); if (nextSiblingDirectory == 0xffffffff) { break; } s.Position = directoryMetadataOffset + nextSiblingDirectory; } } if (firstFileOffset != 0xffffffff) { s.Position = fileMetadataOffset + firstFileOffset; while (true) { byte[] childFileMetadata = s.read(32); uint nextSiblingFile = BitConverter.ToUInt32(childFileMetadata, 4); ulong childFileOffset = BitConverter.ToUInt64(childFileMetadata, 8); ulong childFileSize = BitConverter.ToUInt64(childFileMetadata, 16); uint nameLength = BitConverter.ToUInt32(childFileMetadata, 28); string name = s.read((int)nameLength, Encoding.Unicode); currentDirectory.addChild(name, (long)childFileOffset + fileOffset, (long)childFileSize); if (nextSiblingFile == 0xffffffff) { break; } s.Position = fileMetadataOffset + nextSiblingFile; } } }
private static FilesystemFile getIconFile(FilesystemDirectory partition) { if (partition.contains("ExeFS")) { var exefs = (FilesystemDirectory)partition.getChild("ExeFS"); if (exefs.contains("icon")) { return((FilesystemFile)exefs.getChild("icon")); } } return(null); }
public async Task Should_delete_a_directory() { var resourceDirectory = new FilesystemDirectory(ResourceHelpers.GetResourceDirectoryInfo()); var sut = resourceDirectory.GetDirectory($"testfolder_{Guid.NewGuid()}"); await sut.CreateDirectoryAsync(); sut.Exists.Should().BeTrue(); await sut.DeleteAsync(); sut.Exists.Should().BeFalse(); }
void ResetCommand(string[] args) { #pragma warning restore 414 if (args.Length == 0) { throw BadParameter("pathspec is required."); } var pathspec = args[0] == "*" ? null : args[0]; var localRepository = GetExistingLocalRepository(FilesystemDirectory.AbsoluteDirectoryOf(pathspec)); localRepository.ResetFiles(pathspec == "*" ? null : pathspec); }
public async Task Should_contain_a_single_file() { var sut = new FilesystemDirectory(ResourceHelpers.GetResourceDirectoryInfo()); var fileList = (await sut.GetFilesAsync()).ToList(); fileList.Count.Should().Be(1); var firstFile = fileList.First(); firstFile.IsDirectory.Should().BeFalse(); firstFile.Name.Should().Be("SampleContent.txt"); }
private void extractDirectory(FilesystemDirectory dir) { SaveFileDialog fileDialog = new SaveFileDialog() { FileName = dir.name, }; if (fileDialog.ShowDialog() == DialogResult.OK) { extractDirectory(dir, fileDialog.FileName); MessageBox.Show("Done!"); } }
public async Task Should_not_blow_up_if_folder_already_exists() { var resourceDirectory = new FilesystemDirectory(ResourceHelpers.GetResourceDirectoryInfo()); var sut = resourceDirectory.GetDirectory($"testfolder_{Guid.NewGuid()}"); await sut.CreateDirectoryAsync(); sut.Exists.Should().BeTrue(); await sut.CreateDirectoryAsync(); await sut.DeleteAsync(); }
public async Task Should_give_handle_to_nonexistent_file() { const string tempFileName = "test.txt"; var resourceDirectory = new FilesystemDirectory(ResourceHelpers.GetResourceDirectoryInfo()); var directory = resourceDirectory.GetDirectory($"testfolder_{Guid.NewGuid()}"); var tempFile = directory.GetFile(tempFileName); tempFile.Exists.Should().BeFalse(); tempFile.RealPath.Should().Be(Path.Combine(directory.RealPath, tempFileName)); await directory.DeleteAsync(); }
public static void parseRomFS(ROMInfo info, WrappedInputStream s, FilesystemDirectory partition, long offset = 0) { FilesystemDirectory romfs = new FilesystemDirectory() { name = "RomFS" }; s.Position = offset; string magic = s.read(4, Encoding.ASCII); if (!magic.Equals("IVFC")) { return; } s.Position = offset + 8; int masterHashSize = s.readIntLE(); s.Position = offset + 0x4c; int level3BlockSize = s.readIntLE(); int level3HashBlockSize = 1 << level3BlockSize; int level3Offset = roundUpToMultiple(0x60 + masterHashSize, level3HashBlockSize); s.Position = offset + level3Offset + 4; //Header size should be 0x28... int directoryHashTableOffset = s.readIntLE(); int directoryHashTableLength = s.readIntLE(); int directoryMetadataOffset = s.readIntLE(); int directoryMetadataLength = s.readIntLE(); int fileHashTableOffset = s.readIntLE(); int fileHashTableLength = s.readIntLE(); int fileMetadataOffset = s.readIntLE(); int fileMetadataLength = s.readIntLE(); int fileDataOffset = s.readIntLE(); long baseOffset = offset + level3Offset; s.Position = baseOffset + directoryMetadataOffset; byte[] rootDirectory = s.read(directoryMetadataLength); iterateRomFSEntry(s, romfs, rootDirectory, baseOffset + directoryMetadataOffset, baseOffset + fileMetadataOffset, baseOffset + fileDataOffset); partition.addChild(romfs); }
private void extractDirectory(FilesystemDirectory dir, string outputPath) { Directory.CreateDirectory(outputPath); foreach (var child in dir.children) { string path = Path.Combine(outputPath, child.name); if (child is FilesystemFile childFile) { byte[] data = getFile(childFile); using (var stream = new FileInfo(path).OpenWrite()) { stream.Write(data, 0, data.Length); } } else if (child is FilesystemDirectory childDir) { extractDirectory(childDir, path); } } }
public async Task Should_be_able_to_write_to_file() { const string tempFileName = "test.txt"; var resourceDirectory = new FilesystemDirectory(ResourceHelpers.GetResourceDirectoryInfo()); var directory = resourceDirectory.GetDirectory($"level1/testfolder_{Guid.NewGuid()}"); var tempFile = directory.GetFile(tempFileName); using (var stream = await tempFile.OpenWriteAsync()) using (var writer = new StreamWriter(stream)) { writer.Write(WRITE_CONTENT); } File.Exists(tempFile.RealPath).Should().BeTrue(); File.Delete(tempFile.RealPath); await directory.DeleteAsync(); }
public static void readNitroSubFNT(FilesystemDirectory fs, WrappedInputStream s, uint fatOffset, uint fatSize, uint fntOffset, uint fntSize, uint subTableOffset, ushort firstID) { long origPos = s.Position; try { ushort currentFileID = firstID; s.Position = fntOffset + subTableOffset; while (true) { int length = s.read(); if (length == -1 || length == 0 || length == 0x80) { break; } if (length < 0x80) { ushort fileID = currentFileID; currentFileID++; string name = s.read(length, Encoding.ASCII); var offsetAndSize = getFatInfo(s, fatOffset, fileID); fs.addChild(name, offsetAndSize.Item1, offsetAndSize.Item2); } else { length -= 0x80; string name = s.read(length, Encoding.ASCII); ushort dirID = (ushort)((ushort)s.readShortLE() & 0x0fff); FilesystemDirectory folder = new FilesystemDirectory() { name = name, }; readNitroMainFNT(folder, s, fatOffset, fatSize, fntOffset, fntSize, dirID * (uint)8); fs.addChild(folder); } } } finally { s.Position = origPos; } }
public static void addAtariDOS2(ROMInfo info, List <byte[]> sectors) { var fs = new FilesystemDirectory() { name = "Atari DOS2", }; //Sectors 1 and 3 have boot record but nobody tells me what that does if (sectors.Count <= 359) { info.addInfo("Has files", false); return; } byte[] vtoc = sectors[359]; //#TODO: VTOC2 if disk is big enough info.addInfo("Number of free sectors", vtoc[3]); bool hasFiles = false; for (int i = 360; i < 368; ++i) { byte[] sector = sectors[i]; //Flags = sector[0] for (int j = 0; j < 8; ++j) { byte[] fileHeader = sector.Skip(16 * j).Take(16).ToArray(); if (fileHeader.All(b => b == 0)) { continue; } hasFiles = true; short sectorsInFile = (short)(fileHeader[1] | (fileHeader[2] << 8)); short startingSector = (short)(fileHeader[3] | (fileHeader[4] << 8)); string filename = Encoding.ASCII.GetString(fileHeader.Skip(5).Take(8).ToArray()).TrimEnd(); string extension = Encoding.ASCII.GetString(fileHeader.Skip(13).Take(3).ToArray()).TrimEnd(); //TODO: Actually get the offset and size of the file fs.addChild(filename + "." + extension, 0, 0); } } info.addInfo("Has files", hasFiles); info.addFilesystem(fs); }
public static void parseExeFS(ROMInfo info, WrappedInputStream s, FilesystemDirectory partition, long offset = 0) { FilesystemDirectory exefs = new FilesystemDirectory() { name = "ExeFS" }; s.Position = offset; for (int i = 0; i < 10; ++i) { string filename = s.read(8, Encoding.ASCII).TrimEnd('\0'); long fileOffset = (uint)s.readIntLE() + offset + 0x200; //Add ExeFS header as well long fileSize = (uint)s.readIntLE(); if (fileSize > 0) { exefs.addChild(filename, fileOffset, fileSize); } } partition.addChild(exefs); }
public static void readNitroMainFNT(FilesystemDirectory fs, WrappedInputStream s, uint fatOffset, uint fatSize, uint fntOffset, uint filenameTableSize, uint tableOffset) { //tableOffset being the offset _into_ the FNT, not the offset _of_ the FNT (which is fntOffset). That's confusing. I'll figure out how to be less confusing later long origPos = s.Position; try { s.Position = fntOffset + tableOffset; uint subTableOffset = (uint)s.readIntLE(); ushort firstID = (ushort)s.readShortLE(); if (firstID > 0xefff) { //Shouldn't go higher than that, apparently return; } //TODO: For tableOffset = 0 (root), read number of directories (short LE here): //Validate that <= 4096 directories //Validate that number of directories * 8 < fntSize readNitroSubFNT(fs, s, fatOffset, fatSize, fntOffset, filenameTableSize, subTableOffset, firstID); } finally { s.Position = origPos; } }
public static void parseNCCH(ROMInfo info, WrappedInputStream s, string prefix, long offset = 0) { //"NCCD" magic at 0x100 s.Position = offset + 0x104; uint contentSize = (uint)s.readIntLE() * MEDIA_UNIT; info.addInfo(combinePrefix(prefix, "Content size"), contentSize, ROMInfo.FormatMode.SIZE); byte[] partitionID = s.read(8); info.addInfo(combinePrefix(prefix, "Partition ID"), partitionID); string makerCode = s.read(2, Encoding.ASCII); info.addInfo(combinePrefix(prefix, "Publisher"), makerCode, NintendoCommon.LICENSEE_CODES); short version = s.readShortLE(); info.addInfo(combinePrefix(prefix, "NCCH version", true), version); //Seemingly not quite the same as card version... always 2? //Some stuff about a hash s.Position = offset + 0x118; byte[] programID = s.read(8); info.addInfo(combinePrefix(prefix, "Program ID"), programID); //Some stuff about a reserved and a logo SHA-256 s.Position = offset + 0x150; string productCode = s.read(16, Encoding.ASCII).TrimEnd('\0'); //Not just ABBC anymore! It's now CTR-P-ABBC... albeit that's 10 chars? info.addInfo(combinePrefix(prefix, "Product code"), productCode); if (productCode.Length == 10 && !productCode.Equals("CTR-P-CTAP")) { info.addInfo(combinePrefix(prefix, "Category"), productCode[4], CATEGORIES); info.addInfo(combinePrefix(prefix, "Type"), productCode[6], GAME_TYPES); info.addInfo(combinePrefix(prefix, "Short title"), productCode.Substring(7, 2)); info.addInfo(combinePrefix(prefix, "Country"), productCode[9], NintendoCommon.COUNTRIES); } s.Position = offset + 0x180; int extendedHeaderSize = s.readIntLE(); //NOT in media units! info.addInfo(combinePrefix(prefix, "Extended header size"), extendedHeaderSize, ROMInfo.FormatMode.SIZE); //Something about a reserved and an extended header SHA-256 s.Position = offset + 0x188; byte[] flags = s.read(8); bool isData = (flags[5] & 1) > 0; bool isExecutable = (flags[5] & 2) > 0; bool isCXI = !(isData & !isExecutable); bool isSystemUpdate = (flags[5] & 4) > 0; bool isElectronicManual = (flags[5] & 8) > 0; bool isTrial = (flags[5] & 16) > 0; //This isn't set on trials... bool isZeroKeyEncrypted = (flags[7] & 1) > 0; bool isDecrypted = (flags[7] & 4) > 0; info.addInfo(combinePrefix(prefix, "Is CXI"), isCXI); info.addInfo(combinePrefix(prefix, "Is data"), isData); info.addInfo(combinePrefix(prefix, "Is executable"), isExecutable); info.addInfo(combinePrefix(prefix, "Is system update"), isSystemUpdate); info.addInfo(combinePrefix(prefix, "Is electronic manual"), isElectronicManual); //TODO This just goes to show we should make some of this stuff extra if it's not the main thing ("Electronic manual is electronic manual" = true) info.addInfo(combinePrefix(prefix, "Is trial"), isTrial); info.addInfo(combinePrefix(prefix, "Is encrypted with 0 key"), isZeroKeyEncrypted); info.addInfo(combinePrefix(prefix, "Is decrypted"), isDecrypted); long plainRegionOffset = (uint)s.readIntLE() * MEDIA_UNIT + offset; long plainRegionSize = (uint)s.readIntLE() * MEDIA_UNIT; long logoRegionOffset = (uint)s.readIntLE() * MEDIA_UNIT + offset; long logoRegionSize = (uint)s.readIntLE() * MEDIA_UNIT; long exeFSOffset = (uint)s.readIntLE() * MEDIA_UNIT + offset; long exeFSSize = (uint)s.readIntLE() * MEDIA_UNIT; s.Seek(8, SeekOrigin.Current); //Skip over ExeFS hash region size and reserved long romFSOffset = (uint)s.readIntLE() * MEDIA_UNIT + offset; long romFSSize = (uint)s.readIntLE() * MEDIA_UNIT; FilesystemDirectory partition = new FilesystemDirectory() { name = prefix ?? "Main partition" }; info.addFilesystem(partition); if (plainRegionSize > 0) { info.addInfo(combinePrefix(prefix, "Plain region offset"), plainRegionOffset, ROMInfo.FormatMode.HEX); info.addInfo(combinePrefix(prefix, "Plain region size"), plainRegionSize, ROMInfo.FormatMode.SIZE); parsePlainRegion(info, s, prefix, plainRegionOffset); } if (exeFSSize > 0) { info.addInfo(combinePrefix(prefix, "ExeFS offset", true), exeFSOffset, ROMInfo.FormatMode.HEX); info.addInfo(combinePrefix(prefix, "ExeFS size", true), exeFSSize, ROMInfo.FormatMode.SIZE); if (isDecrypted) { //If the ROM is encrypted, it'll be all garbled, so there's not much we can do there... parseExeFS(info, s, partition, exeFSOffset); } } if (romFSSize > 0) { info.addInfo(combinePrefix(prefix, "RomFS offset", true), romFSOffset, ROMInfo.FormatMode.HEX); info.addInfo(combinePrefix(prefix, "RomFS size", true), romFSSize, ROMInfo.FormatMode.SIZE); if (isDecrypted) { parseRomFS(info, s, partition, romFSOffset); } } var icon = getIconFile(partition); if (icon != null) { parseSMDH(info, s, prefix, icon.offset); } if (isCXI & isDecrypted) { s.Position = offset + 0x200; string appTitle = s.read(8, Encoding.ASCII).TrimEnd('\0'); info.addInfo(combinePrefix(prefix, "Internal name"), appTitle); s.Position = offset + 0x20d; int extendedFlags = s.read(); info.addInfo(combinePrefix(prefix, "Compressed ExeFS code"), (extendedFlags & 1) > 0, true); info.addInfo(combinePrefix(prefix, "SD application", true), (extendedFlags & 2) > 0, true); short remasterVersion = s.readShortLE(); info.addInfo(combinePrefix(prefix, "Remaster version"), remasterVersion, true); s.Position = offset + 0x450; //This makes no sense - it should be at offset + 0x240 according to documentation, but it isn't var dependencyList = new List <string>(); for (int i = 0; i < 48; ++i) { string dependency = s.read(8, Encoding.ASCII).TrimEnd('\0'); if (dependency.Length == 0) { break; } dependencyList.Add(dependency); } info.addInfo("Dependencies", String.Join(", ", dependencyList), true); s.Position = offset + 0x3c0; //TODO: Add readLongLE and readLongBE to WrappedInputStream //This wouldn't work if you used this on a big endian C# environment I would think ulong saveDataSize = BitConverter.ToUInt64(s.read(8), 0); info.addInfo("Save size", saveDataSize, ROMInfo.FormatMode.SIZE); //TODO: Access control info stuff https://www.3dbrew.org/wiki/NCCH/Extended_Header } }