diff --git a/hactoolnet/Program.cs b/hactoolnet/Program.cs index 0936f2cc..d971d552 100644 --- a/hactoolnet/Program.cs +++ b/hactoolnet/Program.cs @@ -11,108 +11,59 @@ namespace hactoolnet { static void Main(string[] args) { + var sdfs = LoadSdFs(args); + Console.WriteLine("Listing NCA files"); - ListSdContents(args); + ListNcas(sdfs); Console.WriteLine("Listing titles"); - var watch = Stopwatch.StartNew(); + ListTitles(sdfs); - DumpMeta(args); - watch.Stop(); - Console.WriteLine(watch.Elapsed.TotalMilliseconds); + //DecryptNax0(sdfs, "C0628FB07A89E9050BDA258F74868E8D"); } - static void DecryptNax0(string[] args) + static void DecryptNax0(SdFs sdFs, string name) { - var keyset = ExternalKeys.ReadKeyFile(args[0]); - keyset.SetSdSeed(args[1].ToBytes()); - - using (var nax0 = Nax0.CreateFromPath(keyset, args[2], args[3])) + var nca = sdFs.Ncas[name]; + using (var output = new FileStream($"{nca.NcaId}.nca", FileMode.Create)) + using (var progress = new ProgressBar()) { - var nca = new Nca(keyset, nax0.Stream, true); - - using (var output = new FileStream(args[4], FileMode.Create)) - using (var progress = new ProgressBar()) - { - progress.LogMessage($"Title ID: {nca.Header.TitleId:X16}"); - progress.LogMessage($"Writing {args[4]}"); - nax0.Stream.CopyStream(output, nax0.Stream.Length, progress); - } + progress.LogMessage($"Title ID: {nca.Header.TitleId:X16}"); + progress.LogMessage($"Writing {nca.NcaId}.nca"); + nca.Stream.Position = 0; + nca.Stream.CopyStream(output, nca.Stream.Length, progress); } } - static void ListSdContents(string[] args) + static SdFs LoadSdFs(string[] args) { - Console.WriteLine($"Using key file {args[0]}"); - Console.WriteLine($"SD seed {BitConverter.ToString(args[1].ToBytes())}"); - Console.WriteLine($"SD path {args[2]}"); var keyset = ExternalKeys.ReadKeyFile(args[0]); - - if (keyset.master_keys[0].IsEmpty()) - { - Console.WriteLine("Need master key 0"); - } - keyset.SetSdSeed(args[1].ToBytes()); - using (var sdfs = new SdFs(keyset, args[2])) - { - sdfs.OpenAllNcas(); + var sdfs = new SdFs(keyset, args[2]); + return sdfs; + } - foreach (Nca nca in sdfs.Ncas) + static void ListNcas(SdFs sdfs) + { + foreach (Nca nca in sdfs.Ncas.Values.OrderBy(x => x.Header.TitleId)) + { + Console.WriteLine($"{nca.Header.TitleId:X16} {nca.Header.ContentType.ToString().PadRight(10, ' ')} {nca.NcaId}"); + } + } + + static void ListTitles(SdFs sdfs) + { + foreach (var title in sdfs.Titles.Values.OrderBy(x => x.Id)) + { + Console.WriteLine($"{title.Id:X16} v{title.Version.Version} ({title.Version}) {title.Metadata.Type}"); + + foreach (var content in title.Metadata.ContentEntries) { Console.WriteLine( - $"{nca.Header.TitleId:X16} {nca.Header.ContentType.ToString().PadRight(10, ' ')} {nca.Name}"); + $" {BitConverter.ToString(content.NcaId).Replace("-", "")}.nca {content.Type} {Util.GetBytesReadable(content.Size)}"); } - } - } - static void DumpMeta(string[] args) - { - var keyset = ExternalKeys.ReadKeyFile(args[0]); - keyset.SetSdSeed(args[1].ToBytes()); - List ncas; - using (var sdfs = new SdFs(keyset, args[2])) - { - sdfs.OpenAllNcas(); - ncas = sdfs.Ncas; - - var metadata = new List(); - - using (var progress = new ProgressBar()) - { - foreach (var nca in ncas.Where(x => x.Header.ContentType == ContentType.Meta)) - { - foreach (var section in nca.Sections.Where(x => x.Header.FsType == SectionFsType.Pfs0)) - { - var sect = nca.OpenSection(section.SectionNum); - var pfs0 = new Pfs0(sect); - - foreach (var entry in pfs0.Entries) - { - var path = Path.Combine("meta", entry.Name); - Directory.CreateDirectory(Path.GetDirectoryName(path)); - var file = pfs0.GetFile(entry.Index); - - metadata.Add(new Cnmt(new MemoryStream(file))); - File.WriteAllBytes(path, file); - } - } - } - - foreach (var meta in metadata.OrderBy(x => x.TitleId)) - { - progress.LogMessage($"{meta.TitleId:X16} v{meta.TitleVersion} {meta.Type}"); - - foreach (var content in meta.ContentEntries) - { - // Add an actual hexdump function - progress.LogMessage( - $" {BitConverter.ToString(content.NcaId).Replace("-", "")}.nca {content.Type}"); - } - - progress.LogMessage(""); - } - } + Console.WriteLine(""); } } } diff --git a/libhac/Nca.cs b/libhac/Nca.cs index fa01bf0a..e504e44c 100644 --- a/libhac/Nca.cs +++ b/libhac/Nca.cs @@ -8,7 +8,7 @@ namespace libhac public class Nca : IDisposable { public NcaHeader Header { get; private set; } - public string Name { get; set; } + public string NcaId { get; set; } public bool HasRightsId { get; private set; } public int CryptoType { get; private set; } public byte[][] DecryptedKeys { get; } = Util.CreateJaggedArray(4, 0x10); diff --git a/libhac/NcaStructs.cs b/libhac/NcaStructs.cs index 7c6eb938..4e10fdf7 100644 --- a/libhac/NcaStructs.cs +++ b/libhac/NcaStructs.cs @@ -31,7 +31,7 @@ namespace libhac head.Signature1 = reader.ReadBytes(0x100); head.Signature2 = reader.ReadBytes(0x100); head.Magic = reader.ReadAscii(4); - if(head.Magic != "NCA3") throw new InvalidDataException("Not an NCA3 file"); + if (head.Magic != "NCA3") throw new InvalidDataException("Not an NCA3 file"); head.Distribution = reader.ReadByte(); head.ContentType = (ContentType)reader.ReadByte(); head.CryptoType = reader.ReadByte(); @@ -216,6 +216,29 @@ namespace libhac } } + public class TitleVersion + { + public uint Version { get; } + public byte Major { get; } + public byte Minor { get; } + public byte Patch { get; } + public byte Revision { get; } + + public TitleVersion(uint version) + { + Version = version; + Revision = (byte)version; + Patch = (byte)(version >> 8); + Minor = (byte)(version >> 16); + Major = (byte)(version >> 24); + } + + public override string ToString() + { + return $"{Major}.{Minor}.{Patch}.{Revision}"; + } + } + public enum ContentType { Program, diff --git a/libhac/SdFs.cs b/libhac/SdFs.cs index bc42f987..56c66422 100644 --- a/libhac/SdFs.cs +++ b/libhac/SdFs.cs @@ -11,7 +11,10 @@ namespace libhac public string RootDir { get; } public string ContentsDir { get; } public string[] Files { get; } - public List Ncas { get; } = new List(); + + public Dictionary Ncas { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + public Dictionary Titles { get; } = new Dictionary(); + private List Nax0s { get; } = new List(); public SdFs(Keyset keyset, string sdPath) @@ -24,6 +27,8 @@ namespace libhac } Files = Directory.GetFiles(ContentsDir, "00", SearchOption.AllDirectories).Select(Path.GetDirectoryName).ToArray(); + OpenAllNcas(); + ReadTitles(); } public void OpenAllNcas() @@ -37,30 +42,47 @@ namespace libhac var nax0 = Nax0.CreateFromPath(Keyset, file, sdPath); Nax0s.Add(nax0); nca = new Nca(Keyset, nax0.Stream, false); - nca.Name = Path.GetFileName(file); + nca.NcaId = Path.GetFileNameWithoutExtension(file); } catch (Exception ex) { Console.WriteLine($"{ex.Message} {file}"); } - if (nca != null) Ncas.Add(nca); + if (nca != null) Ncas.Add(nca.NcaId, nca); + } + } + + public void ReadTitles() + { + foreach (var nca in Ncas.Values.Where(x => x.Header.ContentType == ContentType.Meta)) + { + var title = new Title(); + + // Meta contents always have 1 Partition FS section with 1 file in it + Stream sect = nca.OpenSection(0); + var pfs0 = new Pfs0(sect); + var file = pfs0.GetFile(0); + + var metadata = new Cnmt(new MemoryStream(file)); + title.Id = metadata.TitleId; + title.Version = new TitleVersion(metadata.TitleVersion); + title.Metadata = metadata; + Titles.Add(title.Id, title); } } private void DisposeNcas() { - foreach (var nca in Ncas) + foreach (Nca nca in Ncas.Values) { nca.Dispose(); } Ncas.Clear(); - foreach (var nax0 in Nax0s) - { - nax0.Dispose(); - } + // Disposing the Nca disposes the Nax0 as well Nax0s.Clear(); + Titles.Clear(); } public void Dispose() @@ -68,4 +90,12 @@ namespace libhac DisposeNcas(); } } + + public class Title + { + public ulong Id { get; internal set; } + public TitleVersion Version { get; internal set; } + public List Ncas { get; } = new List(); + public Cnmt Metadata { get; internal set; } + } } diff --git a/libhac/Util.cs b/libhac/Util.cs index 733864d8..2cd53635 100644 --- a/libhac/Util.cs +++ b/libhac/Util.cs @@ -181,5 +181,52 @@ namespace libhac { return MediaSize * media; } + + public static string GetBytesReadable(long bytes) + { + // Get absolute value + long absBytes = (bytes < 0 ? -bytes : bytes); + // Determine the suffix and readable value + string suffix; + double readable; + if (absBytes >= 0x1000000000000000) // Exabyte + { + suffix = "EB"; + readable = (bytes >> 50); + } + else if (absBytes >= 0x4000000000000) // Petabyte + { + suffix = "PB"; + readable = (bytes >> 40); + } + else if (absBytes >= 0x10000000000) // Terabyte + { + suffix = "TB"; + readable = (bytes >> 30); + } + else if (absBytes >= 0x40000000) // Gigabyte + { + suffix = "GB"; + readable = (bytes >> 20); + } + else if (absBytes >= 0x100000) // Megabyte + { + suffix = "MB"; + readable = (bytes >> 10); + } + else if (absBytes >= 0x400) // Kilobyte + { + suffix = "KB"; + readable = bytes; + } + else + { + return bytes.ToString("0 B"); // Byte + } + // Divide by 1024 to get fractional value + readable = (readable / 1024); + // Return formatted number with suffix + return readable.ToString("0.### ") + suffix; + } } }