diff --git a/src/LibHac/IO/PartitionFileSystem.cs b/src/LibHac/IO/PartitionFileSystem.cs index 9c79b141..51c5bba2 100644 --- a/src/LibHac/IO/PartitionFileSystem.cs +++ b/src/LibHac/IO/PartitionFileSystem.cs @@ -119,7 +119,7 @@ namespace LibHac.IO throw new InvalidDataException($"Invalid Partition FS type \"{Magic}\""); } - int entrySize = GetFileEntrySize(Type); + int entrySize = PartitionFileEntry.GetEntrySize(Type); int stringTableOffset = 16 + entrySize * NumFiles; HeaderSize = stringTableOffset + StringTableSize; @@ -135,19 +135,6 @@ namespace LibHac.IO Files[i].Name = reader.ReadAsciiZ(); } } - - private static int GetFileEntrySize(PartitionFileSystemType type) - { - switch (type) - { - case PartitionFileSystemType.Standard: - return 24; - case PartitionFileSystemType.Hashed: - return 0x40; - default: - throw new ArgumentOutOfRangeException(nameof(type), type, null); - } - } } public class PartitionFileEntry @@ -178,5 +165,18 @@ namespace LibHac.IO reader.BaseStream.Position += 4; } } + + public static int GetEntrySize(PartitionFileSystemType type) + { + switch (type) + { + case PartitionFileSystemType.Standard: + return 0x18; + case PartitionFileSystemType.Hashed: + return 0x40; + default: + throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } } } diff --git a/src/LibHac/IO/PartitionFileSystemBuilder.cs b/src/LibHac/IO/PartitionFileSystemBuilder.cs new file mode 100644 index 00000000..4e1bf45b --- /dev/null +++ b/src/LibHac/IO/PartitionFileSystemBuilder.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace LibHac.IO +{ + public class PartitionFileSystemBuilder + { + private const int HeaderSize = 0x10; + private const int MetaDataAlignment = 0x20; + + private List Entries { get; } = new List(); + private long CurrentOffset { get; set; } + + /// + /// Creates a new and populates it with all + /// the files in the root directory. + /// + public PartitionFileSystemBuilder(IFileSystem input) + { + IDirectory rootDir = input.OpenDirectory("/", OpenDirectoryMode.Files); + + foreach (DirectoryEntry file in rootDir.Read().OrderBy(x => x.FullPath, StringComparer.Ordinal)) + { + AddFile(file.FullPath.TrimStart('/'), input.OpenFile(file.FullPath, OpenMode.Read)); + } + } + + public void AddFile(string filename, IFile file) + { + var entry = new Entry + { + Name = filename, + File = file, + Length = file.GetSize(), + Offset = CurrentOffset, + NameLength = Encoding.UTF8.GetByteCount(filename) + }; + + CurrentOffset += entry.Length; + + Entries.Add(entry); + } + + public IStorage Build(PartitionFileSystemType type) + { + byte[] meta = BuildMetaData(type); + + var sources = new List(); + sources.Add(new MemoryStorage(meta)); + + sources.AddRange(Entries.Select(x => new FileStorage(x.File))); + + return new ConcatenationStorage(sources, true); + } + + private byte[] BuildMetaData(PartitionFileSystemType type) + { + int entryTableSize = Entries.Count * PartitionFileEntry.GetEntrySize(type); + int stringTableSize = CalcStringTableSize(HeaderSize + entryTableSize); + int metaDataSize = HeaderSize + entryTableSize + stringTableSize; + + var metaData = new byte[metaDataSize]; + var writer = new BinaryWriter(new MemoryStream(metaData)); + + writer.WriteUTF8(GetMagicValue(type)); + writer.Write(Entries.Count); + writer.Write(stringTableSize); + writer.Write(0); + + int stringOffset = 0; + + foreach (Entry entry in Entries) + { + writer.Write(entry.Offset); + writer.Write(entry.Length); + writer.Write(stringOffset); + writer.Write(0); + + stringOffset += entry.NameLength + 1; + } + + foreach (Entry entry in Entries) + { + writer.WriteUTF8Z(entry.Name); + } + + return metaData; + } + + private int CalcStringTableSize(int startOffset) + { + int size = 0; + + foreach (Entry entry in Entries) + { + size += entry.NameLength + 1; + } + + int endOffset = Util.AlignUp(startOffset + size, MetaDataAlignment); + return endOffset - startOffset; + } + + private string GetMagicValue(PartitionFileSystemType type) + { + switch (type) + { + case PartitionFileSystemType.Standard: return "PFS0"; + case PartitionFileSystemType.Hashed: return "HFS0"; + default: throw new ArgumentOutOfRangeException(nameof(type), type, null); + } + } + + private class Entry + { + public string Name; + public IFile File; + public long Length; + public long Offset; + public int NameLength; + } + } +} diff --git a/src/hactoolnet/Options.cs b/src/hactoolnet/Options.cs index 6ecf38d2..b60d7d2a 100644 --- a/src/hactoolnet/Options.cs +++ b/src/hactoolnet/Options.cs @@ -62,6 +62,7 @@ namespace hactoolnet { Nca, Pfs0, + PfsBuild, Nsp, Romfs, RomfsBuild, diff --git a/src/hactoolnet/ProcessFsBuild.cs b/src/hactoolnet/ProcessFsBuild.cs new file mode 100644 index 00000000..f7b68d73 --- /dev/null +++ b/src/hactoolnet/ProcessFsBuild.cs @@ -0,0 +1,55 @@ +using System.IO; +using LibHac.IO; +using LibHac.IO.RomFs; + +namespace hactoolnet +{ + internal static class ProcessFsBuild + { + public static void ProcessRomFs(Context ctx) + { + if (ctx.Options.OutFile == null) + { + ctx.Logger.LogMessage("Output file must be specified."); + return; + } + + var localFs = new LocalFileSystem(ctx.Options.InFile); + + var builder = new RomFsBuilder(localFs); + IStorage romfs = builder.Build(); + + ctx.Logger.LogMessage($"Building RomFS as {ctx.Options.OutFile}"); + + using (var outFile = new FileStream(ctx.Options.OutFile, FileMode.Create, FileAccess.ReadWrite)) + { + romfs.CopyToStream(outFile, romfs.Length, ctx.Logger); + } + + ctx.Logger.LogMessage($"Finished writing {ctx.Options.OutFile}"); + } + + public static void ProcessPartitionFs(Context ctx) + { + if (ctx.Options.OutFile == null) + { + ctx.Logger.LogMessage("Output file must be specified."); + return; + } + + var localFs = new LocalFileSystem(ctx.Options.InFile); + + var builder = new PartitionFileSystemBuilder(localFs); + IStorage partitionFs = builder.Build(PartitionFileSystemType.Standard); + + ctx.Logger.LogMessage($"Building Partition FS as {ctx.Options.OutFile}"); + + using (var outFile = new FileStream(ctx.Options.OutFile, FileMode.Create, FileAccess.ReadWrite)) + { + partitionFs.CopyToStream(outFile, partitionFs.Length, ctx.Logger); + } + + ctx.Logger.LogMessage($"Finished writing {ctx.Options.OutFile}"); + } + } +} diff --git a/src/hactoolnet/ProcessRomFsBuild.cs b/src/hactoolnet/ProcessRomFsBuild.cs deleted file mode 100644 index 8cd6d525..00000000 --- a/src/hactoolnet/ProcessRomFsBuild.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.IO; -using LibHac.IO; -using LibHac.IO.RomFs; - -namespace hactoolnet -{ - internal static class ProcessRomFsBuild - { - public static void Process(Context ctx) - { - if (ctx.Options.OutFile == null) - { - ctx.Logger.LogMessage("Output file must be specified."); - return; - } - - var localFs = new LocalFileSystem(ctx.Options.InFile); - - var builder = new RomFsBuilder(localFs); - IStorage romfs = builder.Build(); - - ctx.Logger.LogMessage($"Building RomFS as {ctx.Options.OutFile}"); - - using (var outFile = new FileStream(ctx.Options.OutFile, FileMode.Create, FileAccess.ReadWrite)) - { - romfs.CopyToStream(outFile, romfs.Length, ctx.Logger); - } - - ctx.Logger.LogMessage($"Finished writing {ctx.Options.OutFile}"); - } - } -} diff --git a/src/hactoolnet/Program.cs b/src/hactoolnet/Program.cs index bc73f0b0..5f7905cc 100644 --- a/src/hactoolnet/Program.cs +++ b/src/hactoolnet/Program.cs @@ -61,11 +61,14 @@ namespace hactoolnet case FileType.Nsp: ProcessNsp.Process(ctx); break; + case FileType.PfsBuild: + ProcessFsBuild.ProcessPartitionFs(ctx); + break; case FileType.Romfs: ProcessRomfs.Process(ctx); break; case FileType.RomfsBuild: - ProcessRomFsBuild.Process(ctx); + ProcessFsBuild.ProcessRomFs(ctx); break; case FileType.Nax0: break;