Mimic hactool output. Verify hashes

This commit is contained in:
Alex Barney 2018-07-05 16:37:30 -05:00
parent 20be7206a0
commit c4efec762f
9 changed files with 420 additions and 52 deletions

View file

@ -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...] <path>");
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 <file> 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 <title id> 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();

View file

@ -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];

View file

@ -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
}
}
}

View file

@ -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;
}
}
}
}

View file

@ -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();
}
}
@ -256,6 +256,18 @@ namespace libhac
}
}
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,

View file

@ -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))
{

View file

@ -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))
{

View file

@ -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,8 +129,6 @@ 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);

View file

@ -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[]>