Ejemplo n.º 1
0
        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);
        }
Ejemplo n.º 2
0
        public void Should_represent_directory()
        {
            var sut = new FilesystemDirectory(ResourceHelpers.GetResourceDirectoryInfo());

            sut.IsDirectory.Should().BeTrue();
            sut.Name.Should().Be("Resources");
        }
Ejemplo n.º 3
0
        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;
            }
        }
Ejemplo n.º 4
0
        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();
        }
Ejemplo n.º 6
0
        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();
        }
Ejemplo n.º 7
0
        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);
        }
Ejemplo n.º 8
0
        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");
        }
Ejemplo n.º 9
0
        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);
        }
Ejemplo n.º 10
0
        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;
                }
            }
        }
Ejemplo n.º 11
0
 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();
        }
Ejemplo n.º 13
0
        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);
        }
Ejemplo n.º 14
0
        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");
        }
Ejemplo n.º 15
0
        private void extractDirectory(FilesystemDirectory dir)
        {
            SaveFileDialog fileDialog = new SaveFileDialog()
            {
                FileName = dir.name,
            };

            if (fileDialog.ShowDialog() == DialogResult.OK)
            {
                extractDirectory(dir, fileDialog.FileName);
                MessageBox.Show("Done!");
            }
        }
Ejemplo n.º 16
0
        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();
        }
Ejemplo n.º 17
0
        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();
        }
Ejemplo n.º 18
0
        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);
        }
Ejemplo n.º 19
0
 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);
         }
     }
 }
Ejemplo n.º 20
0
        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();
        }
Ejemplo n.º 21
0
        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;
            }
        }
Ejemplo n.º 22
0
        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);
        }
Ejemplo n.º 23
0
        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);
        }
Ejemplo n.º 24
0
        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;
            }
        }
Ejemplo n.º 25
0
        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
            }
        }