Use the dictionary in the romfs instead of creating our own

This commit is contained in:
Alex Barney 2019-01-27 20:44:38 -06:00
parent e5f808cd2d
commit fb4619f4ab
11 changed files with 481 additions and 215 deletions

View file

@ -33,7 +33,9 @@ namespace LibHac.IO
{
var levelData = new IntegrityVerificationStorage(levelInfo[i], Levels[i - 1], integrityCheckLevel, leaveOpen);
Levels[i] = new CachedStorage(levelData, 4, leaveOpen);
int cacheCount = Math.Min((int)Util.DivideByRoundUp(levelData.Length, levelInfo[i].BlockSize), 4);
Levels[i] = new CachedStorage(levelData, cacheCount, leaveOpen);
LevelValidities[i - 1] = levelData.BlockValidities;
IntegrityStorages[i - 1] = levelData;
}

View file

@ -0,0 +1,197 @@
using System;
using System.Runtime.InteropServices;
namespace LibHac.IO
{
public class HierarchicalRomFileTable
{
private IStorage DirHashTableStorage { get; }
private IStorage DirEntryTableStorage { get; }
private IStorage FileHashTableStorage { get; }
private IStorage FileEntryTableStorage { get; }
private RomFsDictionary<FileRomEntry> FileTable { get; }
private RomFsDictionary<DirectoryRomEntry> DirectoryTable { get; }
public HierarchicalRomFileTable(IStorage dirHashTable, IStorage dirEntryTable, IStorage fileHashTable,
IStorage fileEntryTable)
{
DirHashTableStorage = dirHashTable;
DirEntryTableStorage = dirEntryTable;
FileHashTableStorage = fileHashTable;
FileEntryTableStorage = fileEntryTable;
FileTable = new RomFsDictionary<FileRomEntry>(FileHashTableStorage, FileEntryTableStorage);
DirectoryTable = new RomFsDictionary<DirectoryRomEntry>(DirHashTableStorage, DirEntryTableStorage);
}
public bool OpenFile(string path, out RomFileInfo fileInfo)
{
FindFileRecursive(path.AsSpan(), out RomEntryKey key);
if (FileTable.TryGetValue(ref key, out FileRomEntry entry, out int _))
{
fileInfo = entry.Info;
return true;
}
fileInfo = default;
return false;
}
public bool OpenFile(int offset, out RomFileInfo fileInfo)
{
if (FileTable.TryGetValue(offset, out FileRomEntry entry))
{
fileInfo = entry.Info;
return true;
}
fileInfo = default;
return false;
}
public bool OpenDirectory(string path, out FindPosition position)
{
FindDirectoryRecursive(path.AsSpan(), out RomEntryKey key);
if (DirectoryTable.TryGetValue(ref key, out DirectoryRomEntry entry, out int _))
{
position = entry.Pos;
return true;
}
position = default;
return false;
}
public bool OpenDirectory(int offset, out FindPosition position)
{
if (DirectoryTable.TryGetValue(offset, out DirectoryRomEntry entry))
{
position = entry.Pos;
return true;
}
position = default;
return false;
}
public bool FindNextFile(ref FindPosition position, out RomFileInfo info, out string name)
{
if (FileTable.TryGetValue(position.NextFile, out FileRomEntry entry, out name))
{
position.NextFile = entry.NextSibling;
info = entry.Info;
return true;
}
info = default;
return false;
}
public bool FindNextDirectory(ref FindPosition position, out string name)
{
if (DirectoryTable.TryGetValue(position.NextDirectory, out DirectoryRomEntry entry, out name))
{
position.NextDirectory = entry.NextSibling;
return true;
}
return false;
}
private void FindFileRecursive(ReadOnlySpan<char> path, out RomEntryKey key)
{
var parser = new PathParser(path);
FindParentDirectoryRecursive(ref parser, out DirectoryRomEntry _, out int parentOffset);
key = new RomEntryKey(parser.GetCurrent(), parentOffset);
}
private void FindDirectoryRecursive(ReadOnlySpan<char> path, out RomEntryKey key)
{
var parser = new PathParser(path);
FindParentDirectoryRecursive(ref parser, out DirectoryRomEntry _, out int parentOffset);
ReadOnlySpan<char> name = parser.GetCurrent();
if (name.Length == 0) parentOffset = 0;
key = new RomEntryKey(name, parentOffset);
}
private void FindParentDirectoryRecursive(ref PathParser parser, out DirectoryRomEntry parentEntry, out int parentOffset)
{
parentEntry = default;
parentOffset = default;
RomEntryKey key = default;
while (parser.TryGetNext(out key.Name) && !parser.IsFinished())
{
DirectoryTable.TryGetValue(ref key, out parentEntry, out parentOffset);
key.Parent = parentOffset;
}
}
}
internal ref struct RomEntryKey
{
public ReadOnlySpan<char> Name;
public int Parent;
public RomEntryKey(ReadOnlySpan<char> name, int parent)
{
Name = name;
Parent = parent;
}
public uint GetRomHashCode()
{
uint hash = 123456789 ^ (uint)Parent;
foreach (char c in Name)
{
hash = c ^ ((hash << 27) | (hash >> 5));
}
return hash;
}
}
[StructLayout(LayoutKind.Sequential)]
public struct RomFsEntry<T> where T : unmanaged
{
public int Parent;
public T Value;
public int Next;
public int KeyLength;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct FileRomEntry
{
public int NextSibling;
public RomFileInfo Info;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct RomFileInfo
{
public long Offset;
public long Length;
}
[StructLayout(LayoutKind.Sequential)]
public struct DirectoryRomEntry
{
public int NextSibling;
public FindPosition Pos;
}
[StructLayout(LayoutKind.Sequential)]
public struct FindPosition
{
public int NextDirectory;
public int NextFile;
}
}

View file

@ -0,0 +1,60 @@
using System;
using System.Diagnostics;
namespace LibHac.IO
{
public ref struct PathParser
{
private ReadOnlySpan<char> _path;
private int _offset;
private int _length;
private bool _finished;
public PathParser(ReadOnlySpan<char> path)
{
Debug.Assert(PathTools.IsNormalized(path));
if (path.Length < 1 || path[0] != '/')
{
throw new ArgumentException("Path must begin with a '/'");
}
_path = path;
_offset = 0;
_length = 0;
_finished = false;
}
public bool TryGetNext(out ReadOnlySpan<char> name)
{
bool success = MoveNext();
name = GetCurrent();
return success;
}
public bool MoveNext()
{
if (_finished) return false;
_offset = _offset + _length + 1;
int end = _offset;
while (end < _path.Length && _path[end] != '/')
{
end++;
}
_finished = end + 1 >= _path.Length;
_length = end - _offset;
return true;
}
public ReadOnlySpan<char> GetCurrent()
{
return _path.Slice(_offset, _length);
}
public bool IsFinished() => _finished;
}
}

View file

@ -91,6 +91,35 @@ namespace LibHac.IO
return path.Substring(0, i);
}
public static bool IsNormalized(ReadOnlySpan<char> path)
{
var state = NormalizeState.Initial;
foreach (char c in path)
{
switch (state)
{
case NormalizeState.Initial when c == '/': state = NormalizeState.Delimiter; break;
case NormalizeState.Initial: return false;
case NormalizeState.Normal when c == '/': state = NormalizeState.Delimiter; break;
case NormalizeState.Delimiter when c == '/': return false;
case NormalizeState.Delimiter when c == '.': state = NormalizeState.Dot; break;
case NormalizeState.Delimiter: state = NormalizeState.Normal; break;
case NormalizeState.Dot when c == '/': return false;
case NormalizeState.Dot when c == '.': state = NormalizeState.DoubleDot; break;
case NormalizeState.Dot: state = NormalizeState.Normal; break;
case NormalizeState.DoubleDot when c == '/': return false;
case NormalizeState.DoubleDot: state = NormalizeState.Normal; break;
}
}
return state == NormalizeState.Normal || state == NormalizeState.Delimiter;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsDirectorySeparator(char c)
{
@ -111,5 +140,14 @@ namespace LibHac.IO
(index + 3 == path.Length || IsDirectorySeparator(path[index + 3])) &&
path[index + 1] == '.' && path[index + 2] == '.';
}
private enum NormalizeState
{
Initial,
Normal,
Delimiter,
Dot,
DoubleDot
}
}
}

View file

@ -0,0 +1,128 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace LibHac.IO
{
internal class RomFsDictionary<T> where T : unmanaged
{
private int HashBucketCount { get; }
private IStorage BucketStorage { get; }
private IStorage EntryStorage { get; }
// Hack around not being able to get the size of generic structures
private readonly int _sizeOfEntry = 12 + Marshal.SizeOf<T>();
public RomFsDictionary(IStorage bucketStorage, IStorage entryStorage)
{
BucketStorage = bucketStorage;
EntryStorage = entryStorage;
HashBucketCount = (int)(bucketStorage.Length / 4);
}
public bool TryGetValue(ref RomEntryKey key, out T value, out int offset)
{
int i = FindEntry(ref key);
offset = i;
if (i >= 0)
{
GetEntryInternal(i, out RomFsEntry<T> entry);
value = entry.Value;
return true;
}
value = default;
return false;
}
public bool TryGetValue(int offset, out T value, out string entryName)
{
if (offset < 0 || offset + _sizeOfEntry >= EntryStorage.Length)
{
value = default;
entryName = default;
return false;
}
GetEntryInternal(offset, out RomFsEntry<T> entry, out entryName);
value = entry.Value;
return true;
}
public bool TryGetValue(int offset, out T value)
{
if (offset < 0 || offset + _sizeOfEntry >= EntryStorage.Length)
{
value = default;
return false;
}
GetEntryInternal(offset, out RomFsEntry<T> entry);
value = entry.Value;
return true;
}
private int FindEntry(ref RomEntryKey key)
{
uint hashCode = key.GetRomHashCode();
int i = GetBucket((int)(hashCode % HashBucketCount));
while (i != -1)
{
GetEntryInternal(i, out RomFsEntry<T> entry);
if (IsEqual(ref key, ref entry, i))
{
break;
}
i = entry.Next;
}
return i;
}
private bool IsEqual(ref RomEntryKey key, ref RomFsEntry<T> entry, int entryOffset)
{
if (key.Parent != entry.Parent) return false;
if (key.Name.Length != entry.KeyLength) return false;
GetEntryInternal(entryOffset, out RomFsEntry<T> _, out string name);
return key.Name.Equals(name.AsSpan(), StringComparison.Ordinal);
}
private void GetEntryInternal(int offset, out RomFsEntry<T> outEntry)
{
Span<byte> b = stackalloc byte[_sizeOfEntry];
EntryStorage.Read(b, offset);
outEntry = MemoryMarshal.Read<RomFsEntry<T>>(b);
}
private void GetEntryInternal(int offset, out RomFsEntry<T> outEntry, out string entryName)
{
GetEntryInternal(offset, out outEntry);
if (outEntry.KeyLength > 0x300)
{
throw new InvalidDataException("Rom entry name is too long.");
}
var buf = new byte[outEntry.KeyLength];
EntryStorage.Read(buf, offset + _sizeOfEntry);
entryName = Encoding.ASCII.GetString(buf);
}
private int GetBucket(int index)
{
Debug.Assert(index < HashBucketCount);
Span<byte> buf = stackalloc byte[4];
BucketStorage.Read(buf, index * 4);
return MemoryMarshal.Read<int>(buf);
}
}
}

View file

@ -1,52 +1,43 @@
using System.Collections.Generic;
using System.IO;
namespace LibHac.IO
{
public class RomFsDirectory : IDirectory
{
public IFileSystem ParentFileSystem { get; }
IFileSystem IDirectory.ParentFileSystem => ParentFileSystem;
public RomFsFileSystem ParentFileSystem { get; }
public string FullPath { get; }
private RomfsDir Directory { get; }
public OpenDirectoryMode Mode { get; }
public RomFsDirectory(RomFsFileSystem fs, string path, OpenDirectoryMode mode)
{
path = PathTools.Normalize(path);
private FindPosition InitialPosition { get; }
if (!fs.DirectoryDict.TryGetValue(path, out RomfsDir dir))
public RomFsDirectory(RomFsFileSystem fs, string path, FindPosition position, OpenDirectoryMode mode)
{
throw new DirectoryNotFoundException(path);
}
ParentFileSystem = fs;
Directory = dir;
InitialPosition = position;
FullPath = path;
Mode = mode;
}
public IEnumerable<DirectoryEntry> Read()
{
FindPosition position = InitialPosition;
HierarchicalRomFileTable tab = ParentFileSystem.FileTable;
if (Mode.HasFlag(OpenDirectoryMode.Directories))
{
RomfsDir dirEntry = Directory.FirstChild;
while (dirEntry != null)
while (tab.FindNextDirectory(ref position, out string name))
{
yield return new DirectoryEntry(dirEntry.Name, FullPath + '/' + dirEntry.Name, DirectoryEntryType.Directory, 0);
dirEntry = dirEntry.NextSibling;
yield return new DirectoryEntry(name, FullPath + '/' + name, DirectoryEntryType.Directory, 0);
}
}
if (Mode.HasFlag(OpenDirectoryMode.Files))
{
RomfsFile fileEntry = Directory.FirstFile;
while (fileEntry != null)
while (tab.FindNextFile(ref position, out RomFileInfo info, out string name))
{
yield return new DirectoryEntry(fileEntry.Name, FullPath + '/' + fileEntry.Name, DirectoryEntryType.File, fileEntry.DataLength);
fileEntry = fileEntry.NextSibling;
yield return new DirectoryEntry(name, FullPath + '/' + name, DirectoryEntryType.File, info.Length);
}
}
}
@ -55,25 +46,22 @@ namespace LibHac.IO
{
int count = 0;
FindPosition position = InitialPosition;
HierarchicalRomFileTable tab = ParentFileSystem.FileTable;
if (Mode.HasFlag(OpenDirectoryMode.Directories))
{
RomfsDir dirEntry = Directory.FirstChild;
while (dirEntry != null)
while (tab.FindNextDirectory(ref position, out string _))
{
count++;
dirEntry = dirEntry.NextSibling;
}
}
if (Mode.HasFlag(OpenDirectoryMode.Files))
{
RomfsFile fileEntry = Directory.FirstFile;
while (fileEntry != null)
while (tab.FindNextFile(ref position, out RomFileInfo _, out string _))
{
count++;
fileEntry = fileEntry.NextSibling;
}
}

View file

@ -1,97 +1,35 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace LibHac.IO
{
public class RomFsFileSystem : IFileSystem
{
public RomfsHeader Header { get; }
public List<RomfsDir> Directories { get; } = new List<RomfsDir>();
public List<RomfsFile> Files { get; } = new List<RomfsFile>();
public RomfsDir RootDir { get; }
public Dictionary<string, RomfsFile> FileDict { get; }
public Dictionary<string, RomfsDir> DirectoryDict { get; }
public HierarchicalRomFileTable FileTable { get; }
private IStorage BaseStorage { get; }
// todo Don't parse entire table when opening
public RomFsFileSystem(IStorage storage)
{
BaseStorage = storage;
Header = new RomfsHeader(storage.AsFile(OpenMode.Read));
byte[] dirMetaTable;
byte[] fileMetaTable;
IStorage dirHashTable = storage.Slice(Header.DirHashTableOffset, Header.DirHashTableSize);
IStorage dirEntryTable = storage.Slice(Header.DirMetaTableOffset, Header.DirMetaTableSize);
IStorage fileHashTable = storage.Slice(Header.FileHashTableOffset, Header.FileHashTableSize);
IStorage fileEntryTable = storage.Slice(Header.FileMetaTableOffset, Header.FileMetaTableSize);
using (var reader = new BinaryReader(BaseStorage.AsStream(), Encoding.Default, true))
{
Header = new RomfsHeader(reader);
reader.BaseStream.Position = Header.DirMetaTableOffset;
dirMetaTable = reader.ReadBytes((int)Header.DirMetaTableSize);
reader.BaseStream.Position = Header.FileMetaTableOffset;
fileMetaTable = reader.ReadBytes((int)Header.FileMetaTableSize);
}
using (var reader = new BinaryReader(new MemoryStream(dirMetaTable)))
{
int position = 0;
while (position + 20 < Header.DirMetaTableSize)
{
var dir = new RomfsDir(reader) { Offset = position };
Directories.Add(dir);
if (dir.ParentDirOffset == position) RootDir = dir;
position = (int)reader.BaseStream.Position;
}
}
using (var reader = new BinaryReader(new MemoryStream(fileMetaTable)))
{
int position = 0;
while (position + 20 < Header.FileMetaTableSize)
{
var file = new RomfsFile(reader) { Offset = position };
Files.Add(file);
position = (int)reader.BaseStream.Position;
}
}
SetReferences();
RomfsEntry.ResolveFilenames(Files);
RomfsEntry.ResolveFilenames(Directories);
FileDict = Files.ToDictionary(x => x.FullPath, x => x);
DirectoryDict = Directories.ToDictionary(x => x.FullPath, x => x);
}
private void SetReferences()
{
Dictionary<int, RomfsDir> dirDict = Directories.ToDictionary(x => x.Offset, x => x);
Dictionary<int, RomfsFile> fileDict = Files.ToDictionary(x => x.Offset, x => x);
foreach (RomfsDir dir in Directories)
{
if (dir.ParentDirOffset >= 0 && dir.ParentDirOffset != dir.Offset) dir.ParentDir = dirDict[dir.ParentDirOffset];
if (dir.NextSiblingOffset >= 0) dir.NextSibling = dirDict[dir.NextSiblingOffset];
if (dir.FirstChildOffset >= 0) dir.FirstChild = dirDict[dir.FirstChildOffset];
if (dir.FirstFileOffset >= 0) dir.FirstFile = fileDict[dir.FirstFileOffset];
if (dir.NextDirHashOffset >= 0) dir.NextDirHash = dirDict[dir.NextDirHashOffset];
}
foreach (RomfsFile file in Files)
{
if (file.ParentDirOffset >= 0) file.ParentDir = dirDict[file.ParentDirOffset];
if (file.NextSiblingOffset >= 0) file.NextSibling = fileDict[file.NextSiblingOffset];
if (file.NextFileHashOffset >= 0) file.NextFileHash = fileDict[file.NextFileHashOffset];
}
FileTable = new HierarchicalRomFileTable(dirHashTable, dirEntryTable, fileHashTable, fileEntryTable);
}
public DirectoryEntryType GetEntryType(string path)
{
path = PathTools.Normalize(path);
if (FileDict.ContainsKey(path)) return DirectoryEntryType.File;
if (DirectoryDict.ContainsKey(path)) return DirectoryEntryType.Directory;
if (FileExists(path)) return DirectoryEntryType.File;
if (DirectoryExists(path)) return DirectoryEntryType.Directory;
throw new FileNotFoundException(path);
}
@ -103,14 +41,21 @@ namespace LibHac.IO
public IDirectory OpenDirectory(string path, OpenDirectoryMode mode)
{
return new RomFsDirectory(this, path, mode);
path = PathTools.Normalize(path);
if (!FileTable.OpenDirectory(path, out FindPosition position))
{
throw new DirectoryNotFoundException();
}
return new RomFsDirectory(this, path, position, mode);
}
public IFile OpenFile(string path, OpenMode mode)
{
path = PathTools.Normalize(path);
if (!FileDict.TryGetValue(path, out RomfsFile file))
if (!FileTable.OpenFile(path, out RomFileInfo info))
{
throw new FileNotFoundException();
}
@ -120,26 +65,21 @@ namespace LibHac.IO
throw new ArgumentOutOfRangeException(nameof(mode), "RomFs files must be opened read-only.");
}
return OpenFile(file);
}
public IFile OpenFile(RomfsFile file)
{
return new RomFsFile(BaseStorage, Header.DataOffset + file.DataOffset, file.DataLength);
return new RomFsFile(BaseStorage, Header.DataOffset + info.Offset, info.Length);
}
public bool DirectoryExists(string path)
{
path = PathTools.Normalize(path);
return DirectoryDict.ContainsKey(path);
return FileTable.OpenDirectory(path, out FindPosition _);
}
public bool FileExists(string path)
{
path = PathTools.Normalize(path);
return FileDict.ContainsKey(path);
return FileTable.OpenFile(path, out RomFileInfo _);
}
public IStorage GetBaseStorage()
@ -168,8 +108,10 @@ namespace LibHac.IO
public long FileMetaTableSize { get; }
public long DataOffset { get; }
public RomfsHeader(BinaryReader reader)
public RomfsHeader(IFile file)
{
var reader = new FileReader(file);
HeaderSize = reader.ReadInt64();
DirHashTableOffset = reader.ReadInt64();
DirHashTableSize = reader.ReadInt64();

View file

@ -1,98 +0,0 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
namespace LibHac.IO
{
public abstract class RomfsEntry
{
public int Offset { get; set; }
public int ParentDirOffset { get; protected set; }
public int NameLength { get; protected set; }
public string Name { get; protected set; }
public RomfsDir ParentDir { get; internal set; }
public string FullPath { get; private set; }
internal static void ResolveFilenames(IEnumerable<RomfsEntry> entries)
{
var list = new List<string>();
var sb = new StringBuilder();
const string delimiter = "/";
foreach (RomfsEntry file in entries)
{
list.Add(file.Name);
RomfsDir dir = file.ParentDir;
while (dir != null)
{
list.Add(delimiter);
list.Add(dir.Name);
dir = dir.ParentDir;
}
//todo
if (list.Count == 1) list.Add("/");
for (int i = list.Count - 1; i >= 0; i--)
{
sb.Append(list[i]);
}
file.FullPath = sb.ToString();
list.Clear();
sb.Clear();
}
}
}
[DebuggerDisplay("{" + nameof(Name) + "}")]
public class RomfsDir : RomfsEntry
{
public int NextSiblingOffset { get; }
public int FirstChildOffset { get; }
public int FirstFileOffset { get; }
public int NextDirHashOffset { get; }
public RomfsDir NextSibling { get; internal set; }
public RomfsDir FirstChild { get; internal set; }
public RomfsFile FirstFile { get; internal set; }
public RomfsDir NextDirHash { get; internal set; }
public RomfsDir(BinaryReader reader)
{
ParentDirOffset = reader.ReadInt32();
NextSiblingOffset = reader.ReadInt32();
FirstChildOffset = reader.ReadInt32();
FirstFileOffset = reader.ReadInt32();
NextDirHashOffset = reader.ReadInt32();
NameLength = reader.ReadInt32();
Name = reader.ReadUtf8(NameLength);
reader.BaseStream.Position = Util.GetNextMultiple(reader.BaseStream.Position, 4);
}
}
[DebuggerDisplay("{" + nameof(Name) + "}")]
public class RomfsFile : RomfsEntry
{
public int NextSiblingOffset { get; }
public long DataOffset { get; }
public long DataLength { get; }
public int NextFileHashOffset { get; }
public RomfsFile NextSibling { get; internal set; }
public RomfsFile NextFileHash { get; internal set; }
public RomfsFile(BinaryReader reader)
{
ParentDirOffset = reader.ReadInt32();
NextSiblingOffset = reader.ReadInt32();
DataOffset = reader.ReadInt64();
DataLength = reader.ReadInt64();
NextFileHashOffset = reader.ReadInt32();
NameLength = reader.ReadInt32();
Name = reader.ReadUtf8(NameLength);
reader.BaseStream.Position = Util.GetNextMultiple(reader.BaseStream.Position, 4);
}
}
}

View file

@ -102,6 +102,15 @@ namespace LibHac.IO
}
}
public static byte[] ToArray(this IStorage storage)
{
if (storage == null) return new byte[0];
var arr = new byte[storage.Length];
storage.CopyTo(new MemoryStorage(arr));
return arr;
}
public static void CopyToStream(this IStorage input, Stream output, long length, IProgressReport progress = null)
{
const int bufferSize = 0x8000;

View file

@ -46,9 +46,9 @@ namespace hactoolnet
{
var romfs = new RomFsFileSystem(nca.OpenSection(1, false, ctx.Options.IntegrityLevel, true));
foreach (RomfsFile romfsFile in romfs.Files)
foreach (DirectoryEntry entry in romfs.EnumerateEntries())
{
ctx.Logger.LogMessage(romfsFile.FullPath);
ctx.Logger.LogMessage(entry.FullPath);
}
}

View file

@ -18,9 +18,9 @@ namespace hactoolnet
{
if (ctx.Options.ListRomFs)
{
foreach (RomfsFile romfsFile in romfs.Files)
foreach (DirectoryEntry entry in romfs.EnumerateEntries())
{
ctx.Logger.LogMessage(romfsFile.FullPath);
ctx.Logger.LogMessage(entry.FullPath);
}
}