From c4efec762fabb4225365c7adb1ed419c5be21f4a Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Thu, 5 Jul 2018 16:37:30 -0500 Subject: [PATCH] Mimic hactool output. Verify hashes --- hactoolnet/CliParser.cs | 4 + hactoolnet/Options.cs | 4 +- hactoolnet/Program.cs | 57 ++++++-- libhac/Nca.cs | 295 +++++++++++++++++++++++++++++++++++++--- libhac/NcaStructs.cs | 40 ++++-- libhac/Pfs0.cs | 3 +- libhac/Romfs.cs | 15 +- libhac/SdFs.cs | 6 +- libhac/Util.cs | 48 +++++++ 9 files changed, 420 insertions(+), 52 deletions(-) diff --git a/hactoolnet/CliParser.cs b/hactoolnet/CliParser.cs index f9adabe8..9606e6b5 100644 --- a/hactoolnet/CliParser.cs +++ b/hactoolnet/CliParser.cs @@ -9,8 +9,10 @@ namespace hactoolnet { private static readonly CliOption[] CliOptions = { + new CliOption("custom", 0, (o, a) => o.RunCustom = true), new CliOption("intype", 't', 1, (o, a) => o.InFileType = ParseFileType(a[0])), new CliOption("raw", 'r', 0, (o, a) => o.Raw = true), + new CliOption("verify", 'y', 0, (o, a) => o.Validate = true), new CliOption("keyset", 'k', 1, (o, a) => o.Keyfile = a[0]), new CliOption("titlekeys", 1, (o, a) => o.TitleKeyFile = a[0]), new CliOption("section0", 1, (o, a) => o.SectionOut[0] = a[0]), @@ -132,6 +134,7 @@ namespace hactoolnet sb.AppendLine("Usage: hactoolnet.exe [options...] "); sb.AppendLine("Options:"); sb.AppendLine(" -r, --raw Keep raw data, don\'t unpack."); + sb.AppendLine(" -y, --verify Verify hashes."); sb.AppendLine(" -k, --keyset Load keys from an external file."); sb.AppendLine(" -t, --intype=type Specify input file type [nca, switchfs]"); sb.AppendLine(" --titlekeys Load title keys from an external file."); @@ -150,6 +153,7 @@ namespace hactoolnet sb.AppendLine(" --listapps List application info."); sb.AppendLine(" --listtitles List title info for all titles."); sb.AppendLine(" --title Specify title ID to use."); + sb.AppendLine(" --outdir <dir> Specify directory path to save title to."); sb.AppendLine(" --romfsdir <dir> Specify RomFS directory path."); return sb.ToString(); diff --git a/hactoolnet/Options.cs b/hactoolnet/Options.cs index e77c69fe..a9103446 100644 --- a/hactoolnet/Options.cs +++ b/hactoolnet/Options.cs @@ -4,9 +4,11 @@ namespace hactoolnet { internal class Options { + public bool RunCustom; public string InFile; public FileType InFileType = FileType.Nca; - public bool Raw = false; + public bool Raw; + public bool Validate; public string Keyfile; public string TitleKeyFile; public string[] SectionOut = new string[4]; diff --git a/hactoolnet/Program.cs b/hactoolnet/Program.cs index 3e065a3c..d155c0cc 100644 --- a/hactoolnet/Program.cs +++ b/hactoolnet/Program.cs @@ -9,13 +9,19 @@ namespace hactoolnet { public static class Program { - static void Main(string[] args) + public static void Main(string[] args) { Console.OutputEncoding = Encoding.UTF8; var ctx = new Context(); ctx.Options = CliParser.Parse(args); if (ctx.Options == null) return; + if (ctx.Options.RunCustom) + { + CustomTask(ctx); + return; + } + using (var logger = new ProgressBar()) { ctx.Logger = logger; @@ -64,6 +70,11 @@ namespace hactoolnet { nca.ExtractSection(i, ctx.Options.SectionOutDir[i], ctx.Logger); } + + if (ctx.Options.Validate && nca.Sections[i] != null) + { + nca.VerifySection(i, ctx.Logger); + } } if (ctx.Options.ListRomFs && nca.Sections[1] != null) @@ -75,6 +86,8 @@ namespace hactoolnet ctx.Logger.LogMessage(romfsFile.FullPath); } } + + ctx.Logger.LogMessage(nca.Dump()); } } @@ -116,6 +129,11 @@ namespace hactoolnet var romfs = new Romfs(title.ProgramNca.OpenSection(1, false)); romfs.Extract(ctx.Options.RomfsOutDir, ctx.Logger); } + + if (ctx.Options.OutDir != null) + { + SaveTitle(ctx, switchFs); + } } private static void OpenKeyset(Context ctx) @@ -143,6 +161,13 @@ namespace hactoolnet } } + // For running random stuff + // ReSharper disable once UnusedParameter.Local + private static void CustomTask(Context ctx) + { + + } + private static void ListSdfs(string[] args) { var sdfs = LoadSdFs(args); @@ -219,21 +244,32 @@ namespace hactoolnet } } - static void DecryptTitle(SdFs sdFs, ulong titleId) + private static void SaveTitle(Context ctx, SdFs switchFs) { - var title = sdFs.Titles[titleId]; - var dirName = $"{titleId:X16}v{title.Version.Version}"; + var id = ctx.Options.TitleId; + if (id == 0) + { + ctx.Logger.LogMessage("Title ID must be specified to save title"); + return; + } - Directory.CreateDirectory(dirName); + if (!switchFs.Titles.TryGetValue(id, out var title)) + { + ctx.Logger.LogMessage($"Could not find title {id:X16}"); + return; + } + + var saveDir = Path.Combine(ctx.Options.OutDir, $"{title.Id:X16}v{title.Version.Version}"); + Directory.CreateDirectory(saveDir); foreach (var nca in title.Ncas) { - using (var output = new FileStream(Path.Combine(dirName, nca.Filename), FileMode.Create)) - using (var progress = new ProgressBar()) + nca.Stream.Position = 0; + var outFile = Path.Combine(saveDir, nca.Filename); + ctx.Logger.LogMessage(nca.Filename); + using (var outStream = new FileStream(outFile, FileMode.Create, FileAccess.ReadWrite)) { - progress.LogMessage($"Writing {nca.Filename}"); - nca.Stream.Position = 0; - nca.Stream.CopyStream(output, nca.Stream.Length, progress); + nca.Stream.CopyStream(outStream, nca.Stream.Length, ctx.Logger); } } } @@ -318,4 +354,3 @@ namespace hactoolnet } } } - diff --git a/libhac/Nca.cs b/libhac/Nca.cs index 4eb1bcbf..e10a6229 100644 --- a/libhac/Nca.cs +++ b/libhac/Nca.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Security.Cryptography; +using System.Text; using libhac.XTSSharp; namespace libhac @@ -13,6 +14,8 @@ namespace libhac public bool HasRightsId { get; private set; } public int CryptoType { get; private set; } public byte[][] DecryptedKeys { get; } = Util.CreateJaggedArray<byte[][]>(4, 0x10); + public byte[] TitleKey { get; } + public byte[] TitleKeyDec { get; } = new byte[0x10]; public Stream Stream { get; private set; } private bool KeepOpen { get; } @@ -38,7 +41,9 @@ namespace libhac { if (keyset.TitleKeys.TryGetValue(Header.RightsId, out var titleKey)) { - Crypto.DecryptEcb(keyset.titlekeks[CryptoType], titleKey, DecryptedKeys[2], 0x10); + TitleKey = titleKey; + Crypto.DecryptEcb(keyset.titlekeks[CryptoType], titleKey, TitleKeyDec, 0x10); + DecryptedKeys[2] = TitleKeyDec; } } @@ -61,16 +66,20 @@ namespace libhac if (!raw) { - switch (sect.Header.FsType) + switch (sect.Header.Type) { - case SectionFsType.Pfs0: - offset = sect.Offset + sect.Pfs0.Pfs0Offset; - size = sect.Pfs0.Pfs0Size; + case SectionType.Pfs0: + offset = sect.Offset + sect.Pfs0.Superblock.Pfs0Offset; + size = sect.Pfs0.Superblock.Pfs0Size; break; - case SectionFsType.Romfs: - offset = sect.Offset + (long)sect.Header.Romfs.IvfcHeader.LevelHeaders[Romfs.IvfcMaxLevel - 1] + case SectionType.Romfs: + offset = sect.Offset + sect.Header.Romfs.IvfcHeader.LevelHeaders[Romfs.IvfcMaxLevel - 1].LogicalOffset; + size = sect.Header.Romfs.IvfcHeader.LevelHeaders[Romfs.IvfcMaxLevel - 1].HashDataSize; + break; + case SectionType.Bktr: + offset = sect.Offset + sect.Header.Bktr.IvfcHeader.LevelHeaders[Romfs.IvfcMaxLevel - 1] .LogicalOffset; - size = (long)sect.Header.Romfs.IvfcHeader.LevelHeaders[Romfs.IvfcMaxLevel - 1].HashDataSize; + size = sect.Header.Bktr.IvfcHeader.LevelHeaders[Romfs.IvfcMaxLevel - 1].HashDataSize; break; default: throw new ArgumentOutOfRangeException(); @@ -88,12 +97,12 @@ namespace libhac case SectionCryptType.CTR: return new RandomAccessSectorStream(new AesCtrStream(Stream, DecryptedKeys[2], offset, size, offset, sect.Header.Ctr), false); case SectionCryptType.BKTR: - break; + return new RandomAccessSectorStream(new AesCtrStream(Stream, DecryptedKeys[2], offset, size, offset, sect.Header.Ctr), false); default: throw new ArgumentOutOfRangeException(); } - return Stream; + return new SubStream(Stream, offset, size); } private void DecryptHeader(Keyset keyset, Stream stream) @@ -135,12 +144,41 @@ namespace libhac if (sect.Type == SectionType.Pfs0) { - sect.Pfs0 = header.Pfs0; + sect.Pfs0 = new Pfs0Section(); + sect.Pfs0.Superblock = header.Pfs0; + } + else if (sect.Type == SectionType.Romfs) + { + ProcessIvfcSection(sect); } return sect; } + private void ProcessIvfcSection(NcaSection sect) + { + sect.Romfs = new RomfsSection(); + sect.Romfs.Superblock = sect.Header.Romfs; + var headers = sect.Romfs.Superblock.IvfcHeader.LevelHeaders; + + for (int i = 0; i < Romfs.IvfcMaxLevel; i++) + { + var level = new IvfcLevel(); + sect.Romfs.IvfcLevels[i] = level; + var header = headers[i]; + level.DataOffset = header.LogicalOffset; + level.DataSize = header.HashDataSize; + level.HashBlockSize = 1 << header.BlockSize; + level.HashBlockCount = Util.DivideByRoundUp(level.DataSize, level.HashBlockSize); + level.HashSize = level.HashBlockCount * 0x20; + + if (i != 0) + { + level.HashOffset = sect.Romfs.IvfcLevels[i - 1].DataOffset; + } + } + } + private void ValidateSuperblockHash(int index) { if (Sections[index] == null) throw new ArgumentOutOfRangeException(nameof(index)); @@ -165,10 +203,14 @@ namespace libhac case SectionType.Romfs: var ivfc = sect.Header.Romfs.IvfcHeader; expected = ivfc.MasterHash; - offset = (long)ivfc.LevelHeaders[0].LogicalOffset; - size = 1 << (int)ivfc.LevelHeaders[0].BlockSize; + offset = ivfc.LevelHeaders[0].LogicalOffset; + size = 1 << ivfc.LevelHeaders[0].BlockSize; break; case SectionType.Bktr: + var ivfcBktr = sect.Header.Bktr.IvfcHeader; + expected = ivfcBktr.MasterHash; + offset = ivfcBktr.LevelHeaders[0].LogicalOffset; + size = 1 << ivfcBktr.LevelHeaders[0].BlockSize; break; } @@ -183,7 +225,91 @@ namespace libhac actual = hash.ComputeHash(hashTable); } - sect.SuperblockHashValidity = Util.ArraysEqual(expected, actual) ? Validity.Valid : Validity.Invalid; + var validity = Util.ArraysEqual(expected, actual) ? Validity.Valid : Validity.Invalid; + + sect.SuperblockHashValidity = validity; + if (sect.Type == SectionType.Romfs) sect.Romfs.IvfcLevels[0].HashValidity = validity; + } + + public void VerifySection(int index, IProgressReport logger = null) + { + if (Sections[index] == null) throw new ArgumentOutOfRangeException(nameof(index)); + var sect = Sections[index]; + var stream = OpenSection(index, true); + logger?.LogMessage($"Verifying section {index}..."); + + switch (sect.Type) + { + case SectionType.Invalid: + break; + case SectionType.Pfs0: + VerifyPfs0(stream, sect.Pfs0, logger); + break; + case SectionType.Romfs: + VerifyIvfc(stream, sect.Romfs.IvfcLevels, logger); + break; + case SectionType.Bktr: + break; + } + } + + private void VerifyPfs0(Stream section, Pfs0Section pfs0, IProgressReport logger = null) + { + var sb = pfs0.Superblock; + var table = new byte[sb.HashTableSize]; + section.Position = sb.HashTableOffset; + section.Read(table, 0, table.Length); + + pfs0.Validity = VerifyHashTable(section, table, sb.Pfs0Offset, sb.Pfs0Size, sb.BlockSize, false, logger); + } + + private void VerifyIvfc(Stream section, IvfcLevel[] levels, IProgressReport logger = null) + { + for (int i = 1; i < levels.Length; i++) + { + logger?.LogMessage($" Verifying IVFC Level {i}..."); + var level = levels[i]; + var table = new byte[level.HashSize]; + section.Position = level.HashOffset; + section.Read(table, 0, table.Length); + level.HashValidity = + VerifyHashTable(section, table, level.DataOffset, level.DataSize, level.HashBlockSize, true, logger); + } + } + + private Validity VerifyHashTable(Stream section, byte[] hashTable, long dataOffset, long dataLen, long blockSize, bool isFinalBlockFull, IProgressReport logger = null) + { + const int hashSize = 0x20; + var currentBlock = new byte[blockSize]; + var expectedHash = new byte[hashSize]; + var blockCount = Util.DivideByRoundUp(dataLen, blockSize); + int curBlockSize = (int)blockSize; + section.Position = dataOffset; + logger?.SetTotal(blockCount); + + using (SHA256 sha256 = SHA256.Create()) + { + for (long i = 0; i < blockCount; i++) + { + var remaining = (dataLen - i * blockSize); + if (remaining < blockSize) + { + Array.Clear(currentBlock, 0, currentBlock.Length); + if (!isFinalBlockFull) curBlockSize = (int)remaining; + } + Array.Copy(hashTable, i * hashSize, expectedHash, 0, hashSize); + section.Read(currentBlock, 0, curBlockSize); + var actualHash = sha256.ComputeHash(currentBlock, 0, curBlockSize); + + if (!Util.ArraysEqual(expectedHash, actualHash)) + { + return Validity.Invalid; + } + logger?.ReportAdd(1); + } + } + + return Validity.Valid; } public void Dispose() @@ -205,18 +331,20 @@ namespace libhac public long Size { get; set; } public Validity SuperblockHashValidity { get; set; } - public Pfs0Superblock Pfs0 { get; set; } + public Pfs0Section Pfs0 { get; set; } + public RomfsSection Romfs { get; set; } } public static class NcaExtensions { public static void ExportSection(this Nca nca, int index, string filename, bool raw = false, IProgressReport logger = null) { - if(index < 0 || index > 3) throw new IndexOutOfRangeException(); + if (index < 0 || index > 3) throw new IndexOutOfRangeException(); if (nca.Sections[index] == null) return; var section = nca.OpenSection(index, raw); - Directory.CreateDirectory(Path.GetDirectoryName(filename)); + var dir = Path.GetDirectoryName(filename); + if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir); using (var outFile = new FileStream(filename, FileMode.Create, FileAccess.ReadWrite)) { @@ -226,7 +354,7 @@ namespace libhac public static void ExtractSection(this Nca nca, int index, string outputDir, IProgressReport logger = null) { - if(index < 0 || index > 3) throw new IndexOutOfRangeException(); + if (index < 0 || index > 3) throw new IndexOutOfRangeException(); if (nca.Sections[index] == null) return; var section = nca.Sections[index]; @@ -248,5 +376,136 @@ namespace libhac break; } } + + public static string Dump(this Nca nca) + { + int colLen = 36; + var sb = new StringBuilder(); + sb.AppendLine(); + + sb.AppendLine("NCA:"); + PrintItem("Magic:", nca.Header.Magic); + PrintItem("Fixed-Key Signature:", nca.Header.Signature1); + PrintItem("NPDM Signature:", nca.Header.Signature2); + PrintItem("Content Size:", $"0x{nca.Header.NcaSize:x12}"); + PrintItem("TitleID:", $"{nca.Header.TitleId:X16}"); + PrintItem("SDK Version:", nca.Header.SdkVersion); + PrintItem("Distribution type:", nca.Header.Distribution); + PrintItem("Content Type:", nca.Header.ContentType); + PrintItem("Master Key Revision:", $"{nca.CryptoType} ({Util.GetKeyRevisionSummary(nca.CryptoType)})"); + PrintItem("Encryption Type:", $"{(nca.HasRightsId ? "Titlekey crypto" : "Standard crypto")}"); + + if (nca.HasRightsId) + { + PrintItem("Rights ID:", nca.Header.RightsId); + } + else + { + PrintItem("Key Area Encryption Key:", nca.Header.KaekInd); + sb.AppendLine("Key Area (Encrypted):"); + for (int i = 0; i < 4; i++) + { + PrintItem($" Key {i} (Encrypted):", nca.Header.EncryptedKeys[i]); + } + + sb.AppendLine("Key Area (Decrypted):"); + for (int i = 0; i < 4; i++) + { + PrintItem($" Key {i} (Decrypted):", nca.DecryptedKeys[i]); + } + } + + PrintSections(); + + return sb.ToString(); + + void PrintSections() + { + sb.AppendLine("Sections:"); + + for (int i = 0; i < 4; i++) + { + NcaSection sect = nca.Sections[i]; + if (sect == null) continue; + + sb.AppendLine($" Section {i}:"); + PrintItem(" Offset:", $"0x{sect.Offset:x12}"); + PrintItem(" Size:", $"0x{sect.Size:x12}"); + PrintItem(" Partition Type:", sect.Type); + PrintItem(" Section CTR:", sect.Header.Ctr); + + switch (sect.Type) + { + case SectionType.Pfs0: + PrintPfs0(sect); + break; + case SectionType.Romfs: + PrintRomfs(sect); + break; + case SectionType.Bktr: + break; + default: + sb.AppendLine(" Unknown/invalid superblock!"); + break; + } + } + } + + void PrintPfs0(NcaSection sect) + { + var sBlock = sect.Pfs0.Superblock; + PrintItem($" Superblock Hash{sect.SuperblockHashValidity.GetValidityString()}:", sBlock.MasterHash); + sb.AppendLine($" Hash Table{sect.Pfs0.Validity.GetValidityString()}:"); + + PrintItem(" Offset:", $"0x{sBlock.HashTableOffset:x12}"); + PrintItem(" Size:", $"0x{sBlock.HashTableSize:x12}"); + PrintItem(" Block Size:", $"0x{sBlock.BlockSize:x}"); + PrintItem(" PFS0 Offset:", $"0x{sBlock.Pfs0Offset:x12}"); + PrintItem(" PFS0 Size:", $"0x{sBlock.Pfs0Size:x12}"); + } + + void PrintRomfs(NcaSection sect) + { + var sBlock = sect.Romfs.Superblock; + var levels = sect.Romfs.IvfcLevels; + + PrintItem($" Superblock Hash{sect.SuperblockHashValidity.GetValidityString()}:", sBlock.IvfcHeader.MasterHash); + PrintItem(" Magic:", sBlock.IvfcHeader.Magic); + PrintItem(" ID:", $"{sBlock.IvfcHeader.Id:x8}"); + + for (int i = 0; i < Romfs.IvfcMaxLevel; i++) + { + var level = levels[i]; + sb.AppendLine($" Level {i}{level.HashValidity.GetValidityString()}:"); + PrintItem(" Data Offset:", $"0x{level.DataOffset:x12}"); + PrintItem(" Data Size:", $"0x{level.DataSize:x12}"); + PrintItem(" Hash Offset:", $"0x{level.HashOffset:x12}"); + PrintItem(" Hash BlockSize:", $"0x{level.HashBlockSize:x8}"); + } + + } + + void PrintItem(string prefix, object data) + { + if (data is byte[] byteData) + { + sb.MemDump(prefix.PadRight(colLen), byteData); + } + else + { + sb.AppendLine(prefix.PadRight(colLen) + data); + } + } + } + + public static string GetValidityString(this Validity validity) + { + switch (validity) + { + case Validity.Invalid: return " (FAIL)"; + case Validity.Valid: return " (GOOD)"; + default: return string.Empty; + } + } } } diff --git a/libhac/NcaStructs.cs b/libhac/NcaStructs.cs index e47f1e75..dda42dc8 100644 --- a/libhac/NcaStructs.cs +++ b/libhac/NcaStructs.cs @@ -8,13 +8,13 @@ namespace libhac public byte[] Signature1; // RSA-PSS signature over header with fixed key. public byte[] Signature2; // RSA-PSS signature over header with key in NPDM. public string Magic; - public byte Distribution; // System vs gamecard. + public DistributionType Distribution; // System vs gamecard. public ContentType ContentType; public byte CryptoType; // Which keyblob (field 1) public byte KaekInd; // Which kaek index? public ulong NcaSize; // Entire archive size. public ulong TitleId; - public uint SdkVersion; // What SDK was this built with? + public TitleVersion SdkVersion; // What SDK was this built with? public byte CryptoType2; // Which keyblob (field 2) public byte[] RightsId; public string Name; @@ -33,7 +33,7 @@ namespace libhac head.Signature2 = reader.ReadBytes(0x100); head.Magic = reader.ReadAscii(4); if (head.Magic != "NCA3") throw new InvalidDataException("Not an NCA3 file"); - head.Distribution = reader.ReadByte(); + head.Distribution = (DistributionType)reader.ReadByte(); head.ContentType = (ContentType)reader.ReadByte(); head.CryptoType = reader.ReadByte(); head.KaekInd = reader.ReadByte(); @@ -41,7 +41,7 @@ namespace libhac head.TitleId = reader.ReadUInt64(); reader.BaseStream.Position += 4; - head.SdkVersion = reader.ReadUInt32(); + head.SdkVersion = new TitleVersion(reader.ReadUInt32()); head.CryptoType2 = reader.ReadByte(); reader.BaseStream.Position += 0xF; @@ -188,16 +188,16 @@ namespace libhac public class IvfcLevelHeader { - public ulong LogicalOffset; - public ulong HashDataSize; - public uint BlockSize; + public long LogicalOffset; + public long HashDataSize; + public int BlockSize; public uint Reserved; public IvfcLevelHeader(BinaryReader reader) { - LogicalOffset = reader.ReadUInt64(); - HashDataSize = reader.ReadUInt64(); - BlockSize = reader.ReadUInt32(); + LogicalOffset = reader.ReadInt64(); + HashDataSize = reader.ReadInt64(); + BlockSize = reader.ReadInt32(); Reserved = reader.ReadUInt32(); } } @@ -255,7 +255,19 @@ namespace libhac return $"{Major}.{Minor}.{Patch}.{Revision}"; } } - + + public class Pfs0Section + { + public Pfs0Superblock Superblock { get; set; } + public Validity Validity { get; set; } + } + + public class RomfsSection + { + public RomfsSuperblock Superblock { get; set; } + public IvfcLevel[] IvfcLevels { get; set; } = new IvfcLevel[Romfs.IvfcMaxLevel]; + } + public enum ContentType { Program, @@ -266,6 +278,12 @@ namespace libhac Unknown } + public enum DistributionType + { + Download, + Gamecard + } + public enum SectionCryptType { None = 1, diff --git a/libhac/Pfs0.cs b/libhac/Pfs0.cs index 7e1c3c22..019edb36 100644 --- a/libhac/Pfs0.cs +++ b/libhac/Pfs0.cs @@ -137,7 +137,8 @@ namespace libhac { var stream = pfs0.OpenFile(file); var outName = Path.Combine(outDir, file.Name); - Directory.CreateDirectory(Path.GetDirectoryName(outName)); + var dir = Path.GetDirectoryName(outName); + if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir); using (var outFile = new FileStream(outName, FileMode.Create, FileAccess.ReadWrite)) { diff --git a/libhac/Romfs.cs b/libhac/Romfs.cs index 6e9fb99c..0691637a 100644 --- a/libhac/Romfs.cs +++ b/libhac/Romfs.cs @@ -230,11 +230,13 @@ namespace libhac public class IvfcLevel { - public ulong DataOffset { get; set; } - public ulong DataSize { get; set; } - public ulong HashOffset { get; set; } - public ulong HashBlockSize { get; set; } - public ulong HashBlockCount { get; set; } + public long DataOffset { get; set; } + public long DataSize { get; set; } + public long HashOffset { get; set; } + public long HashSize { get; set; } + public long HashBlockSize { get; set; } + public long HashBlockCount { get; set; } + public Validity HashValidity { get; set; } } public static class RomfsExtensions @@ -245,7 +247,8 @@ namespace libhac { var stream = romfs.OpenFile(file); var outName = outDir + file.FullPath; - Directory.CreateDirectory(Path.GetDirectoryName(outName)); + var dir = Path.GetDirectoryName(outName); + if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir); using (var outFile = new FileStream(outName, FileMode.Create, FileAccess.ReadWrite)) { diff --git a/libhac/SdFs.cs b/libhac/SdFs.cs index 0bcd66c3..9aa1dc82 100644 --- a/libhac/SdFs.cs +++ b/libhac/SdFs.cs @@ -77,7 +77,7 @@ namespace libhac Console.WriteLine($"{ex.Message} {file}"); } - if (nca != null) Ncas.Add(nca.NcaId, nca); + if (nca?.NcaId != null) Ncas.Add(nca.NcaId, nca); } } @@ -129,9 +129,7 @@ namespace libhac { var romfs = new Romfs(title.ControlNca.OpenSection(0, false)); var control = romfs.GetFile("/control.nacp"); - Directory.CreateDirectory("control"); - File.WriteAllBytes($"control/{title.Id:X16}.nacp", control); - + var reader = new BinaryReader(new MemoryStream(control)); title.Control = new Nacp(reader); diff --git a/libhac/Util.cs b/libhac/Util.cs index 733d727a..a44d7288 100644 --- a/libhac/Util.cs +++ b/libhac/Util.cs @@ -287,6 +287,54 @@ namespace libhac return value + multiple - value % multiple; } + + public static int DivideByRoundUp(int value, int divisor) => (value + divisor - 1) / divisor; + public static long DivideByRoundUp(long value, long divisor) => (value + divisor - 1) / divisor; + + public static void MemDump(this StringBuilder sb, string prefix, byte[] data) + { + + int max = 32; + var remaining = data.Length; + bool first = true; + int offset = 0; + + while (remaining > 0) + { + max = Math.Min(max, remaining); + + if (first) + { + sb.Append(prefix); + first = false; + } + else + { + sb.Append(' ', prefix.Length); + } + + for (int i = 0; i < max; i++) + { + sb.Append($"{data[offset++]:X2}"); + } + + sb.AppendLine(); + remaining -= max; + } + } + + public static string GetKeyRevisionSummary(int revision) + { + switch (revision) + { + case 0: return "1.0.0-2.3.0"; + case 1: return "3.0.0"; + case 2: return "3.0.1-3.0.2"; + case 3: return "4.0.0-4.1.0"; + case 4: return "5.0.0"; + default: return "Unknown"; + } + } } public class ByteArray128BitComparer : EqualityComparer<byte[]>