diff --git a/hactoolnet/Program.cs b/hactoolnet/Program.cs index 203a1e8f..6fb53664 100644 --- a/hactoolnet/Program.cs +++ b/hactoolnet/Program.cs @@ -9,8 +9,10 @@ namespace hactoolnet { static void Main(string[] args) { + FileReadTest(args); //ReadNca(); - ListSdfs(args); + //ListSdfs(args); + //ReadNcaSdfs(args); } private static void ListSdfs(string[] args) @@ -27,21 +29,47 @@ namespace hactoolnet //DecryptTitle(sdfs, 0x010023900AEE0000); } + static void FileReadTest(string[] args) + { + var sdfs = LoadSdFs(args); + var title = sdfs.Titles[0x0100E95004038000]; + var nca = title.ProgramNca; + var romfsStream = nca.OpenSection(1, false); + var romfs = new Romfs(romfsStream); + var file = romfs.OpenFile("/stream/voice/us/127/127390101.nop"); + + using (var output = new FileStream("127390101.nop", FileMode.Create)) + { + file.CopyTo(output); + } + } + static void ReadNca() { var keyset = ExternalKeys.ReadKeyFile("keys.txt", "titlekeys.txt"); - using (var file = new FileStream("671d172e7993ee033d1be25ee76378e3.nca", FileMode.Open, FileAccess.Read)) + using (var file = new FileStream("27eeccfe5f6e7637352273bc46ab97e4.nca", FileMode.Open, FileAccess.Read)) { var nca = new Nca(keyset, file, false); - var romfs = nca.OpenSection(0, false); + var romfsStream = nca.OpenSection(1, false); - using (var output = new FileStream("romfs_net", FileMode.Create)) + var romfs = new Romfs(romfsStream); + var bfstm = romfs.OpenFile("/Sound/Resource/Stream/BGM_Castle.bfstm"); + + using (var output = new FileStream("BGM_Castle.bfstm", FileMode.Create)) { - romfs.CopyTo(output); + bfstm.CopyTo(output); } } } + static void ReadNcaSdfs(string[] args) + { + var sdfs = LoadSdFs(args); + var nca = sdfs.Ncas["8EE79C7AB0F16737BC50F049DFDBB595"]; + var romfsStream =nca.OpenSection(1, false); + var romfs = new Romfs(romfsStream); + } + static void DecryptNax0(SdFs sdFs, string name) { if (!sdFs.Ncas.ContainsKey(name)) return; diff --git a/libhac/AesCtrStream.cs b/libhac/AesCtrStream.cs index 3938088b..b6090c27 100644 --- a/libhac/AesCtrStream.cs +++ b/libhac/AesCtrStream.cs @@ -106,17 +106,17 @@ namespace libhac public override void Flush() { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } public override long Seek(long offset, SeekOrigin origin) { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } public override void SetLength(long value) { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } /// @@ -127,11 +127,11 @@ namespace libhac /// The number of bytes to be written to the current stream. public override void Write(byte[] buffer, int offset, int count) { - throw new System.NotImplementedException(); + throw new NotImplementedException(); } public override bool CanRead => true; - public override bool CanSeek => false; + public override bool CanSeek => true; public override bool CanWrite => false; public override long Length { get; } diff --git a/libhac/Romfs.cs b/libhac/Romfs.cs index 8d28d5f8..111106e7 100644 --- a/libhac/Romfs.cs +++ b/libhac/Romfs.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; using System.Text; namespace libhac @@ -7,20 +10,205 @@ namespace libhac public class Romfs { public static readonly int IvfcMaxLevel = 6; + public RomfsHeader Header { get; } + public List Directories { get; } = new List(); + public List Files { get; } = new List(); + public RomfsDir RootDir { get; } + + private Dictionary FileDict { get; } + private Stream Stream { get; set; } + + public Romfs(Stream stream) + { + var watch = Stopwatch.StartNew(); + + byte[] dirMetaTable; + byte[] fileMetaTable; + using (var reader = new BinaryReader(stream, Encoding.Default, true)) + { + Header = new RomfsHeader(reader); + stream.Position = Header.DirMetaTableOffset; + dirMetaTable = reader.ReadBytes((int)Header.DirMetaTableSize); + stream.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.ParentOffset == 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(); + ResolveFilenames(); + FileDict = Files.ToDictionary(x => x.FullPath, x => x); + Stream = stream; + + watch.Stop(); + Console.WriteLine(watch.Elapsed.TotalMilliseconds); + Console.WriteLine(Files.Count); + } + + public Stream OpenFile(string filename) + { + if (!FileDict.TryGetValue(filename, out var file)) + { + throw new FileNotFoundException(); + } + + var stream = new SubStream(Stream, Header.DataOffset + file.DataOffset, file.DataLength); + return stream; + } + + public bool FileExists(string filename) => FileDict.ContainsKey(filename); + + private void SetReferences() + { + var dirDict = Directories.ToDictionary(x => x.Offset, x => x); + var fileDict = Files.ToDictionary(x => x.Offset, x => x); + + foreach (var dir in Directories) + { + if (dir.ParentOffset >= 0 && dir.ParentOffset != dir.Offset) dir.Parent = dirDict[dir.ParentOffset]; + 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 (var 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]; + } + } + + private void ResolveFilenames() + { + var sb = new StringBuilder(); + foreach (var file in Files) + { + sb.Append(file.Name); + var dir = file.ParentDir; + while (dir != null) + { + sb.Insert(0, "/"); + sb.Insert(0, dir.Name); + dir = dir.Parent; + } + + file.FullPath = sb.ToString(); + sb.Clear(); + } + } } public class RomfsHeader { - public ulong HeaderSize; - public ulong DirHashTableOffset; - public ulong DirHashTableSize; - public ulong DirMetaTableOffset; - public ulong DirMetaTableSize; - public ulong FileHashTableOffset; - public ulong FileHashTableSize; - public ulong FileMetaTableOffset; - public ulong FileMetaTableSize; - public ulong DataOffset; + public long HeaderSize { get; } + public long DirHashTableOffset { get; } + public long DirHashTableSize { get; } + public long DirMetaTableOffset { get; } + public long DirMetaTableSize { get; } + public long FileHashTableOffset { get; } + public long FileHashTableSize { get; } + public long FileMetaTableOffset { get; } + public long FileMetaTableSize { get; } + public long DataOffset { get; } + + public RomfsHeader(BinaryReader reader) + { + HeaderSize = reader.ReadInt64(); + DirHashTableOffset = reader.ReadInt64(); + DirHashTableSize = reader.ReadInt64(); + DirMetaTableOffset = reader.ReadInt64(); + DirMetaTableSize = reader.ReadInt64(); + FileHashTableOffset = reader.ReadInt64(); + FileHashTableSize = reader.ReadInt64(); + FileMetaTableOffset = reader.ReadInt64(); + FileMetaTableSize = reader.ReadInt64(); + DataOffset = reader.ReadInt64(); + } + } + + [DebuggerDisplay("{" + nameof(Name) + "}")] + public class RomfsDir + { + public int Offset { get; set; } + public int ParentOffset { get; } + public int NextSiblingOffset { get; } + public int FirstChildOffset { get; } + public int FirstFileOffset { get; } + public int NextDirHashOffset { get; } + public int NameLength { get; } + public string Name { get; } + + public RomfsDir Parent { get; internal set; } + 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) + { + ParentOffset = 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 + { + public int Offset { get; set; } + public int ParentDirOffset { get; } + public int NextSiblingOffset { get; } + public long DataOffset { get; } + public long DataLength { get; } + public int NextFileHashOffset { get; } + public int NameLength { get; } + public string Name { get; } + + public RomfsDir ParentDir { get; internal set; } + public RomfsFile NextSibling { get; internal set; } + public RomfsFile NextFileHash { get; internal set; } + public string FullPath { get; 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); + } } public class IvfcLevel diff --git a/libhac/SdFs.cs b/libhac/SdFs.cs index a6449ac5..d61bfcb9 100644 --- a/libhac/SdFs.cs +++ b/libhac/SdFs.cs @@ -70,6 +70,7 @@ namespace libhac title.Id = metadata.TitleId; title.Version = new TitleVersion(metadata.TitleVersion); title.Metadata = metadata; + title.MetaNca = nca; title.Ncas.Add(nca); foreach (var content in metadata.ContentEntries) @@ -80,6 +81,16 @@ namespace libhac { title.Ncas.Add(contentNca); } + + switch (content.Type) + { + case CnmtContentType.Program: + title.ProgramNca = contentNca; + break; + case CnmtContentType.Control: + title.ControlNca = contentNca; + break; + } } Titles.Add(title.Id, title); @@ -111,5 +122,9 @@ namespace libhac public TitleVersion Version { get; internal set; } public List Ncas { get; } = new List(); public Cnmt Metadata { get; internal set; } + + public Nca MetaNca { get; internal set; } + public Nca ProgramNca { get; internal set; } + public Nca ControlNca { get; internal set; } } } diff --git a/libhac/Substream.cs b/libhac/Substream.cs index a46e8872..57afae9f 100644 --- a/libhac/Substream.cs +++ b/libhac/Substream.cs @@ -18,7 +18,7 @@ namespace libhac Length = length; Offset = offset; - baseStream.Seek(offset, SeekOrigin.Current); + baseStream.Seek(offset, SeekOrigin.Begin); } public override int Read(byte[] buffer, int offset, int count) { diff --git a/libhac/Util.cs b/libhac/Util.cs index 86d3c9dc..a645ee1b 100644 --- a/libhac/Util.cs +++ b/libhac/Util.cs @@ -98,6 +98,11 @@ namespace libhac return Encoding.ASCII.GetString(reader.ReadBytes(size), 0, size); } + public static string ReadUtf8(this BinaryReader reader, int size) + { + return Encoding.UTF8.GetString(reader.ReadBytes(size), 0, size); + } + // todo Maybe make less naive public static string GetRelativePath(string path, string basePath) { @@ -256,6 +261,17 @@ namespace libhac // Return formatted number with suffix return readable.ToString("0.### ") + suffix; } + + public static long GetNextMultiple(long value, int multiple) + { + if (multiple <= 0) + return value; + + if (value % multiple == 0) + return value; + + return value + multiple - value % multiple; + } } public class ByteArray128BitComparer : EqualityComparer