mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-07-08 18:06:41 +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>
200 lines
7.8 KiB
C#
200 lines
7.8 KiB
C#
using System;
|
||
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
|
||
using System.IO;
|
||
using SabreTools.IO.Extensions;
|
||
using SharpCompress.Compressors;
|
||
using SharpCompress.Compressors.BZip2;
|
||
using SharpCompress.Compressors.LZMA;
|
||
using SharpCompress.Compressors.ZStandard;
|
||
#endif
|
||
using SabreTools.Data.Models.WIA;
|
||
|
||
namespace SabreTools.Wrappers
|
||
{
|
||
/// <summary>
|
||
/// Compress and decompress helpers for WIA / RVZ group and table data.
|
||
/// Mirrors Dolphin's WIACompression.cpp: Bzip2, LZMA (raw, no stream header), LZMA2, and Zstd.
|
||
/// </summary>
|
||
internal static class WiaRvzCompressionHelper
|
||
{
|
||
// Dictionary sizes per compression level 1–9 (index 0 unused).
|
||
// Mirrors Dolphin WIACompression.cpp dict_size choices.
|
||
private static readonly int[] DictSizes =
|
||
{
|
||
0, // 0: unused
|
||
1 << 16, // 1: 64 KiB
|
||
1 << 20, // 2: 1 MiB
|
||
1 << 22, // 3: 4 MiB
|
||
1 << 22, // 4: 4 MiB
|
||
1 << 23, // 5: 8 MiB
|
||
1 << 23, // 6: 8 MiB
|
||
1 << 24, // 7: 16 MiB
|
||
1 << 25, // 8: 32 MiB
|
||
1 << 26, // 9: 64 MiB
|
||
};
|
||
|
||
private static int GetDictSize(int level) =>
|
||
DictSizes[Math.Max(1, Math.Min(9, level))];
|
||
|
||
// Returns the raw LZMA2 dict-size property byte for a given dictionary size.
|
||
private static uint Lzma2DictSize(byte p) => (uint)((2 | (p & 1)) << ((p / 2) + 11));
|
||
|
||
private static byte EncodeLzma2DictSize(uint d)
|
||
{
|
||
byte e = 0;
|
||
while (e < 40 && d > Lzma2DictSize(e))
|
||
e++;
|
||
return e;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Fills the compressor-data bytes for <c>WiaHeader2.CompressorData</c> /
|
||
/// <c>WiaHeader2.CompressorDataSize</c>.
|
||
/// LZMA: 5 bytes. LZMA2: 1 byte. Others: 0 bytes.
|
||
/// </summary>
|
||
internal static void GetCompressorData(WiaRvzCompressionType type, int level,
|
||
out byte[] propData, out byte propSize)
|
||
{
|
||
propData = new byte[7];
|
||
int dictSize = GetDictSize(level);
|
||
|
||
switch (type)
|
||
{
|
||
case WiaRvzCompressionType.LZMA:
|
||
propData[0] = 0x5D; // propByte for default pb=2,lp=0,lc=3
|
||
propData[1] = (byte)dictSize;
|
||
propData[2] = (byte)(dictSize >> 8);
|
||
propData[3] = (byte)(dictSize >> 16);
|
||
propData[4] = (byte)(dictSize >> 24);
|
||
propSize = 5;
|
||
break;
|
||
|
||
case WiaRvzCompressionType.LZMA2:
|
||
propData[0] = EncodeLzma2DictSize((uint)dictSize);
|
||
propSize = 1;
|
||
break;
|
||
|
||
default:
|
||
propSize = 0;
|
||
break;
|
||
}
|
||
}
|
||
|
||
/// <summary>Compress <paramref name="data"/> using the specified algorithm.</summary>
|
||
internal static byte[] Compress(WiaRvzCompressionType type, byte[] data, int offset,
|
||
int length, int level, byte[] compressorData, byte compressorDataSize)
|
||
{
|
||
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
|
||
switch (type)
|
||
{
|
||
case WiaRvzCompressionType.Bzip2:
|
||
return CompressBzip2(data, offset, length);
|
||
case WiaRvzCompressionType.LZMA:
|
||
return CompressLzma(data, offset, length, level, isLzma2: false);
|
||
case WiaRvzCompressionType.LZMA2:
|
||
return CompressLzma(data, offset, length, level, isLzma2: true);
|
||
case WiaRvzCompressionType.Zstd:
|
||
return CompressZstd(data, offset, length, level);
|
||
default:
|
||
throw new ArgumentException($"Cannot compress type {type}", nameof(type));
|
||
}
|
||
#else
|
||
throw new PlatformNotSupportedException("WIA/RVZ compression requires .NET 4.6.2 or later.");
|
||
#endif
|
||
}
|
||
|
||
/// <summary>Decompress <paramref name="data"/> using the specified algorithm.</summary>
|
||
internal static byte[] Decompress(WiaRvzCompressionType type, byte[] data, int offset,
|
||
int length, byte[] compressorData, byte compressorDataSize)
|
||
{
|
||
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
|
||
switch (type)
|
||
{
|
||
case WiaRvzCompressionType.Bzip2:
|
||
return DecompressBzip2(data, offset, length);
|
||
case WiaRvzCompressionType.LZMA:
|
||
{
|
||
byte[] props = new byte[compressorDataSize];
|
||
Array.Copy(compressorData, props, compressorDataSize);
|
||
return DecompressLzma(data, offset, length, props, isLzma2: false);
|
||
}
|
||
case WiaRvzCompressionType.LZMA2:
|
||
{
|
||
byte[] props = new byte[compressorDataSize];
|
||
Array.Copy(compressorData, props, compressorDataSize);
|
||
return DecompressLzma(data, offset, length, props, isLzma2: true);
|
||
}
|
||
case WiaRvzCompressionType.Zstd:
|
||
return DecompressZstd(data, offset, length);
|
||
default:
|
||
throw new ArgumentException($"Cannot decompress type {type}", nameof(type));
|
||
}
|
||
#else
|
||
throw new PlatformNotSupportedException("WIA/RVZ decompression requires .NET 4.6.2 or later.");
|
||
#endif
|
||
}
|
||
|
||
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
|
||
|
||
private static byte[] CompressBzip2(byte[] data, int offset, int length)
|
||
{
|
||
using var outMs = new MemoryStream();
|
||
using (var bz2 = BZip2Stream.Create(outMs, CompressionMode.Compress, false, true))
|
||
{
|
||
bz2.Write(data, offset, length);
|
||
}
|
||
return outMs.ToArray();
|
||
}
|
||
|
||
private static byte[] DecompressBzip2(byte[] data, int offset, int length)
|
||
{
|
||
using var inMs = new MemoryStream(data, offset, length);
|
||
using var bz2 = BZip2Stream.Create(inMs, CompressionMode.Decompress, false, false);
|
||
using var outMs = new MemoryStream();
|
||
bz2.BlockCopy(outMs);
|
||
return outMs.ToArray();
|
||
}
|
||
|
||
private static byte[] CompressLzma(byte[] data, int offset, int length, int level, bool isLzma2)
|
||
{
|
||
int dictSize = GetDictSize(level);
|
||
using var outMs = new MemoryStream();
|
||
using (var lzma = LzmaStream.Create(new LzmaEncoderProperties(true, dictSize), isLzma2, outMs))
|
||
{
|
||
lzma.Write(data, offset, length);
|
||
}
|
||
return outMs.ToArray();
|
||
}
|
||
|
||
private static byte[] DecompressLzma(byte[] data, int offset, int length, byte[] props, bool isLzma2)
|
||
{
|
||
using var inMs = new MemoryStream(data, offset, length);
|
||
using var lzma = LzmaStream.Create(props, inMs, length, -1, null, isLzma2, false);
|
||
using var outMs = new MemoryStream();
|
||
lzma.BlockCopy(outMs);
|
||
return outMs.ToArray();
|
||
}
|
||
|
||
private static byte[] CompressZstd(byte[] data, int offset, int length, int level)
|
||
{
|
||
using var outMs = new MemoryStream();
|
||
using (var zstd = new ZStandardStream(outMs, CompressionMode.Compress))
|
||
{
|
||
zstd.Write(data, offset, length);
|
||
}
|
||
return outMs.ToArray();
|
||
}
|
||
|
||
private static byte[] DecompressZstd(byte[] data, int offset, int length)
|
||
{
|
||
using var inMs = new MemoryStream(data, offset, length);
|
||
using var zstd = new ZStandardStream(inMs);
|
||
using var outMs = new MemoryStream();
|
||
zstd.BlockCopy(outMs);
|
||
return outMs.ToArray();
|
||
}
|
||
|
||
#endif
|
||
}
|
||
}
|