mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-07-09 02:16:55 +00:00
* Add GCZ, WIA/RVZ, and NintendoDisc (GameCube/Wii) format support
Port of DolphinIsoLib into SabreTools.Serialization architecture:
- Data.Models: GCZ/, WIA/, NintendoDisc/ model subdirectories (15 files)
- Serialization.Readers: GCZ, WIA, NintendoDisc readers
- Serialization.Writers: GCZ, WIA writers (structural metadata; full round-trip TODO)
- Wrappers: NintendoDisc, GCZ, WIA wrappers with Encryption partial class
- Wrappers: WiaRvzCompressionHelper (BZip2/LZMA/LZMA2/Zstd, net462+ guarded)
- WrapperType + WrapperFactory: GCZ, WIA, NintendoDisc entries added
GetInnerWrapper() decompression and NintendoDisc.Extraction FST extraction
are stubbed with TODO comments pending full implementation.
* Implement GetInnerWrapper for GCZ and WIA, full NintendoDisc extraction
* Add GCZ/WIA/RVZ write pipeline and Nintendo disc compression helpers
* Add WIA/RVZ table decompression, NintendoDisc/GCZ printing, NintendoDisc detection for .iso files
* Fix NintendoDisc header layout, GC magic, and add embedded disc header to WIA/GCZ printing
- Fix GCMagicWord: 0xC23D3C1F -> 0xC2339F3D (confirmed from Dolphin DiscUtils.h)
- Fix disc header field layout to match Dolphin's confirmed offsets:
MakerCode is bytes 4-5 of the 6-char GameId (no separate field at 0x006),
DiscNumber at 0x006, DiscVersion at 0x007, unused region is 14 bytes (0x00A-0x017)
- Update NintendoDisc reader: derive MakerCode from GameId[4..5], fix skip count
- Add ParseDiscHeaderOnly() to reader for partial (short) stream parsing
- Guard DisableHash/DisableEnc reads at the 0x080 boundary for 128-byte embedded headers
- Guard DOL/FST skip for streams shorter than full 0x440 boot block
- Fix WrapperFactory: NintendoDisc magic detection now precedes .iso -> ISO9660 fallback
- Add GameId-prefix heuristic in WrapperFactory for GC discs lacking magic word
- Add GameId-prefix platform fallback in reader for GC discs without GCMagicWord
- Add DiscHeader property to WIA wrapper (parsed from Header2.DiscHeader bytes)
- Add DiscHeader property to GCZ wrapper (decompresses first block only)
- Add ReadDiscHeader() helper to GCZ for lightweight first-block decompression
- Print embedded disc header (Game ID, Maker, Disc/Rev, Title) in WIA.Printing.cs and GCZ.Printing.cs
* Fix Wii partition extraction: correct IV, FST size shift, partition naming
- Block decryption: IV is at raw block offset 0x3D0 (still-encrypted),
matching Dolphin/DolphinIsoLib WiiPartitionDecryptor.DecryptBlock exactly.
- FST size field at boot.bin 0x428 is also stored >>2 on Wii; apply <<2
to get true byte size.
- Partition folder naming now matches DolphinIsoLib WiiDiscExtractor exactly:
type 0->GM+n, 1->UP+n, 2->CH+n, printable ASCII unknown->raw 4-char string,
non-printable->P{index}. SSBB VC channels extract as HA8E, HA9E, etc.
- ExtractionTool peek buffer increased from 16 to 32 bytes.
Verified: SSBB GM0 extracts 5524 files, boot.bin/fst.bin byte-identical
to Dolphin reference extraction.
* Fix FST extraction: create zero-byte files instead of skipping them
Files with fileSize=0 in the FST were silently skipped. Now they are
created as empty files, matching Dolphin/DolphinIsoLib behavior.
Verified: SSBB now extracts 5958 files with 0 missing, 0 extra,
0 size mismatches, and 0 hash mismatches vs DolphinIsoLib reference.
* Add GCZ/WIA/RVZ virtual stream extraction via NintendoDisc wrapper
* Address PR #85 review comments (Copilot + mnadareski)
* Address PR #85 review comments
* Replace custom endian helpers and SHA1 with SabreTools.IO equivalents
* Update GCZ.Printing.cs
* Update NintendoDisc.Printing.cs
* Update WIA.Printing.cs
* Add WIA/RVZ Wii partition crypto round-trip support
- Add AesCbc internal helper (BouncyCastle AES-CBC encrypt/decrypt)
- Add NintendoDisc.CommonKeyProvider hook for injectable test keys
- Fix sha1.Terminate() missing in all three ComputeSha1 helpers in WIA.cs
- Fix Wii partition dataOff alignment to 0x8000 boundary
- Add WIA.EncryptWiiGroup (internal) for re-encrypting plaintext groups
- Add WIA.DumpIso to WIA.Writing.cs (WIA/RVZ -> flat ISO conversion)
- Add WiaVirtualStream on-demand group decompression
- Add _preDecryptedReader bypass on NintendoDisc for WIA extraction path
- Add WIATests.cs with Wii crypto round-trip test using synthetic data
- Move DumpIso from WIA.Extraction.cs to WIA.Writing.cs
- Bump DumpIso read buffer from 1 MiB to 2 MiB (aligns to WIA chunk size)
- Add InternalsVisibleTo SabreTools.Wrappers.Test in csproj
* Remove hardcoded Wii common keys from NintendoDisc.Encryption
- Delete the embedded WiiCommonKeyRetail and WiiCommonKeyKorean byte
arrays from NintendoDisc.Encryption.cs.
- Make CommonKeyProvider public so any caller (not just tests) can
inject keys; DecryptTitleKey now returns null when no key is
available for the requested index rather than falling back to
hardcoded values.
- Add NintendoDiscEncryptionTests.cs:
- Argument guard and no-provider tests for DecryptTitleKey.
- Fake-key round-trip test (encrypt then decrypt with injected key).
- Integration test that reads TestData/NintendoDisc/keys.json,
verifies each key against hardcoded SHA256 constants, and skips
silently if the file is absent or the keys do not match.
- LoadKeyProvider helper (named JSON format, index-keyed).
- Add [Collection(NintendoDisc)] to both NintendoDiscEncryptionTests
and WIATests to prevent parallel access to the static
CommonKeyProvider from racing between test classes.
- Add TestData/NintendoDisc/keys.json.example documenting the
expected key file format.
- Add Newtonsoft.Json reference to SabreTools.Wrappers.Test.csproj.
* Didn't actually commit the changes. My bad. Fixed.
* Edited a comment
* Added in XUnit outputs that show up in Test Viewer in VS
---------
Co-authored-by: Matt Nadareski <mnadareski@outlook.com>
275 lines
9.9 KiB
C#
275 lines
9.9 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Text;
|
|
using SabreTools.CommandLine;
|
|
using SabreTools.CommandLine.Inputs;
|
|
using SabreTools.Hashing;
|
|
using SabreTools.IO.Extensions;
|
|
using SabreTools.Numerics.Extensions;
|
|
using SabreTools.Wrappers;
|
|
|
|
namespace InfoPrint.Features
|
|
{
|
|
internal sealed class MainFeature : Feature
|
|
{
|
|
#region Feature Definition
|
|
|
|
public const string DisplayName = "main";
|
|
|
|
/// <remarks>Flags are unused</remarks>
|
|
private static readonly string[] _flags = [];
|
|
|
|
/// <remarks>Description is unused</remarks>
|
|
private const string _description = "";
|
|
|
|
#endregion
|
|
|
|
#region Inputs
|
|
|
|
private const string _debugName = "debug";
|
|
internal readonly FlagInput DebugInput = new(_debugName, ["-d", "--debug"], "Enable debug mode");
|
|
|
|
private const string _fileOnlyName = "file-only";
|
|
internal readonly FlagInput FileOnlyInput = new(_fileOnlyName, ["-f", "--file"], "Print to file only");
|
|
|
|
private const string _hashName = "hash";
|
|
internal readonly FlagInput HashInput = new(_hashName, ["-c", "--hash"], "Output file hashes");
|
|
|
|
private const string _jsonName = "json";
|
|
internal readonly FlagInput JsonInput = new(_jsonName, ["-j", "--json"], "Print info as JSON");
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Enable debug output for relevant operations
|
|
/// </summary>
|
|
public bool Debug { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Output information to file only, skip printing to console
|
|
/// </summary>
|
|
public bool FileOnly { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Print external file hashes
|
|
/// </summary>
|
|
public bool Hash { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Enable JSON output
|
|
/// </summary>
|
|
public bool Json { get; private set; }
|
|
|
|
public MainFeature()
|
|
: base(DisplayName, _flags, _description)
|
|
{
|
|
RequiresInputs = true;
|
|
|
|
Add(DebugInput);
|
|
Add(HashInput);
|
|
Add(FileOnlyInput);
|
|
Add(JsonInput);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override bool Execute()
|
|
{
|
|
// Get the options from the arguments
|
|
Debug = GetBoolean(_debugName);
|
|
Hash = GetBoolean(_hashName);
|
|
FileOnly = GetBoolean(_fileOnlyName);
|
|
Json = GetBoolean(_jsonName);
|
|
|
|
// Loop through the input paths
|
|
for (int i = 0; i < Inputs.Count; i++)
|
|
{
|
|
string arg = Inputs[i];
|
|
PrintPathInfo(arg);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override bool VerifyInputs() => Inputs.Count > 0;
|
|
|
|
/// <summary>
|
|
/// Wrapper to print information for a single path
|
|
/// </summary>
|
|
/// <param name="path">File or directory path</param>
|
|
private void PrintPathInfo(string path)
|
|
{
|
|
Console.WriteLine($"Checking possible path: {path}");
|
|
|
|
// Check if the file or directory exists
|
|
if (File.Exists(path))
|
|
{
|
|
PrintFileInfo(path);
|
|
}
|
|
else if (Directory.Exists(path))
|
|
{
|
|
foreach (string file in path.SafeEnumerateFiles("*", SearchOption.AllDirectories))
|
|
{
|
|
PrintFileInfo(file);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"{path} does not exist, skipping...");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Print information for a single file, if possible
|
|
/// </summary>
|
|
/// <param name="file">File path</param>
|
|
private void PrintFileInfo(string file)
|
|
{
|
|
Console.WriteLine($"Attempting to print info for {file}");
|
|
|
|
// Get the base info output name
|
|
string filenameBase = $"info-{DateTime.Now:yyyy-MM-dd_HHmmss.ffff}";
|
|
|
|
// If we have the hash flag
|
|
if (Hash)
|
|
{
|
|
var hashBuilder = PrintHashInfo(file);
|
|
if (hashBuilder is not null)
|
|
{
|
|
// Create the output data
|
|
string hashData = hashBuilder.ToString();
|
|
|
|
// Write the output data
|
|
using var hsw = new StreamWriter(File.OpenWrite($"{filenameBase}.hashes"));
|
|
hsw.WriteLine(hashData);
|
|
hsw.Flush();
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
using Stream stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
|
|
// Read the first 32 bytes
|
|
byte[] magic = stream.PeekBytes(32);
|
|
|
|
// Get the file type
|
|
string extension = Path.GetExtension(file).TrimStart('.');
|
|
WrapperType ft = WrapperFactory.GetFileType(magic ?? [], extension);
|
|
|
|
// Print out the file format
|
|
Console.WriteLine($"File format found: {ft}");
|
|
|
|
// Setup the wrapper to print
|
|
var wrapper = WrapperFactory.CreateWrapper(ft, stream);
|
|
|
|
// If we don't have a wrapper
|
|
if (wrapper is null)
|
|
{
|
|
Console.WriteLine($"Either {ft} is not supported or something went wrong during parsing!");
|
|
Console.WriteLine();
|
|
return;
|
|
}
|
|
|
|
// If the wrapper is not printable
|
|
if (wrapper is not IPrintable printable)
|
|
{
|
|
Console.WriteLine($"{ft} is not supported for printing!");
|
|
Console.WriteLine();
|
|
return;
|
|
}
|
|
|
|
// If we have the JSON flag
|
|
if (Json)
|
|
{
|
|
// Create the output data
|
|
string serializedData = printable.ExportJSON();
|
|
|
|
// Write the output data
|
|
using var jsw = new StreamWriter(File.OpenWrite($"{filenameBase}.json"));
|
|
jsw.WriteLine(serializedData);
|
|
jsw.Flush();
|
|
}
|
|
|
|
// Create the output data
|
|
var builder = new StringBuilder();
|
|
printable.PrintInformation(builder);
|
|
|
|
// Only print to console if enabled
|
|
if (!FileOnly)
|
|
Console.WriteLine(builder);
|
|
|
|
using var sw = new StreamWriter(File.OpenWrite($"{filenameBase}.txt"));
|
|
sw.WriteLine(file);
|
|
sw.WriteLine();
|
|
sw.WriteLine(builder.ToString());
|
|
sw.Flush();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine(Debug ? ex : "[Exception opening file, please try again]");
|
|
Console.WriteLine();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Print hash information for a single file, if possible
|
|
/// </summary>
|
|
/// <param name="file">File path</param>
|
|
/// <returns>StringBuilder representing the hash information, if possible</returns>
|
|
private StringBuilder? PrintHashInfo(string file)
|
|
{
|
|
// Ignore missing files
|
|
if (!File.Exists(file))
|
|
return null;
|
|
|
|
Console.WriteLine($"Attempting to hash {file}, this may take a while...");
|
|
|
|
try
|
|
{
|
|
// Get all file hashes for flexibility
|
|
var hashes = HashTool.GetFileHashes(file);
|
|
if (hashes is null)
|
|
{
|
|
if (Debug) Console.WriteLine($"Hashes for {file} could not be retrieved");
|
|
return null;
|
|
}
|
|
|
|
// Output subset of available hashes
|
|
var builder = new StringBuilder();
|
|
if (hashes.TryGetValue(HashType.CRC16, out string? crc16) && crc16 is not null)
|
|
builder.AppendLine($"CRC-16 checksum: {crc16}");
|
|
if (hashes.TryGetValue(HashType.CRC32, out string? crc32) && crc32 is not null)
|
|
builder.AppendLine($"CRC-32 checksum: {crc32}");
|
|
if (hashes.TryGetValue(HashType.CRC64, out string? crc64) && crc64 is not null)
|
|
builder.AppendLine($"CRC-64 checksum: {crc64}");
|
|
if (hashes.TryGetValue(HashType.MD2, out string? md2) && md2 is not null)
|
|
builder.AppendLine($"MD2 hash: {md2}");
|
|
if (hashes.TryGetValue(HashType.MD4, out string? md4) && md4 is not null)
|
|
builder.AppendLine($"MD4 hash: {md4}");
|
|
if (hashes.TryGetValue(HashType.MD5, out string? md5) && md5 is not null)
|
|
builder.AppendLine($"MD5 hash: {md5}");
|
|
if (hashes.TryGetValue(HashType.RIPEMD128, out string? ripemd128) && ripemd128 is not null)
|
|
builder.AppendLine($"RIPEMD-128 hash: {ripemd128}");
|
|
if (hashes.TryGetValue(HashType.RIPEMD160, out string? ripemd160) && ripemd160 is not null)
|
|
builder.AppendLine($"RIPEMD-160 hash: {ripemd160}");
|
|
if (hashes.TryGetValue(HashType.SHA1, out string? sha1) && sha1 is not null)
|
|
builder.AppendLine($"SHA-1 hash: {sha1}");
|
|
if (hashes.TryGetValue(HashType.SHA256, out string? sha256) && sha256 is not null)
|
|
builder.AppendLine($"SHA-256 hash: {sha256}");
|
|
if (hashes.TryGetValue(HashType.SHA384, out string? sha384) && sha384 is not null)
|
|
builder.AppendLine($"SHA-384 hash: {sha384}");
|
|
if (hashes.TryGetValue(HashType.SHA512, out string? sha512) && sha512 is not null)
|
|
builder.AppendLine($"SHA-512 hash: {sha512}");
|
|
|
|
return builder;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine(Debug ? ex : "[Exception opening file, please try again]");
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|