Add notes and reconfigure based on IO porting

This commit is contained in:
Matt Nadareski
2026-05-12 21:55:11 -04:00
parent 2a0e8e2eb0
commit b805c91cb3
14 changed files with 380 additions and 437 deletions

View File

@@ -1,94 +0,0 @@
using System;
using Xunit;
namespace SabreTools.Wrappers.Test
{
[Collection("NintendoDisc")]
public class NintendoDiscTests
{
#region DecryptTitleKey
[Fact]
public void DecryptTitleKey_EmptyKeys_Null()
{
NintendoDisc.RetailCommonKey = [];
NintendoDisc.KoreanCommonKey = [];
Assert.Null(NintendoDisc.DecryptTitleKey(new byte[16], new byte[8], 0));
}
[Fact]
public void DecryptTitleKey_NullEncKey_Null()
{
NintendoDisc.RetailCommonKey = new byte[16];
NintendoDisc.KoreanCommonKey = new byte[16];
Assert.Null(NintendoDisc.DecryptTitleKey(null, new byte[8], 0));
}
[Fact]
public void DecryptTitleKey_WrongLengthEncKey_Null()
{
NintendoDisc.RetailCommonKey = new byte[16];
NintendoDisc.KoreanCommonKey = new byte[16];
Assert.Null(NintendoDisc.DecryptTitleKey(new byte[8], new byte[8], 0));
}
[Fact]
public void DecryptTitleKey_NullTitleId_Null()
{
NintendoDisc.RetailCommonKey = new byte[16];
NintendoDisc.KoreanCommonKey = new byte[16];
Assert.Null(NintendoDisc.DecryptTitleKey(new byte[16], null, 0));
}
[Fact]
public void DecryptTitleKey_WrongLengthTitleId_Null()
{
NintendoDisc.RetailCommonKey = new byte[16];
NintendoDisc.KoreanCommonKey = new byte[16];
Assert.Null(NintendoDisc.DecryptTitleKey(new byte[16], new byte[4], 0));
}
[Theory]
[InlineData(-1)]
[InlineData(2)]
public void DecryptTitleKey_UnknownIndex_Null(int index)
{
Assert.Null(NintendoDisc.DecryptTitleKey(new byte[16], new byte[8], index));
}
[Fact]
public void DecryptTitleKey_WithInjectedKey_RoundTrips()
{
byte[] commonKey =
[
0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xF0, 0x0D,
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
];
byte[] plainTitleKey =
[
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10,
];
byte[] titleId = [0x00, 0x01, 0x00, 0x45, 0x52, 0x53, 0x42, 0x00];
byte[] iv = new byte[16];
Array.Copy(titleId, 0, iv, 0, 8);
byte[] encTitleKey = AesCbc.Encrypt(plainTitleKey, commonKey, iv)
?? throw new InvalidOperationException("AesCbc.Encrypt returned null");
NintendoDisc.RetailCommonKey = commonKey;
NintendoDisc.KoreanCommonKey = commonKey;
byte[]? decrypted = NintendoDisc.DecryptTitleKey(encTitleKey, titleId, 0);
Assert.NotNull(decrypted);
Assert.Equal(plainTitleKey, decrypted);
}
#endregion
}
}

View File

@@ -7,7 +7,6 @@ using static SabreTools.Data.Models.NintendoDisc.Constants;
namespace SabreTools.Wrappers.Test
{
[Collection("NintendoDisc")]
public class WIATests
{
/// <summary>
@@ -40,12 +39,6 @@ namespace SabreTools.Wrappers.Test
#endregion
public WIATests()
{
NintendoDisc.RetailCommonKey = TestCommonKey;
NintendoDisc.KoreanCommonKey = TestCommonKey;
}
[Fact]
public void NullArray_Null()
{
@@ -103,7 +96,7 @@ namespace SabreTools.Wrappers.Test
/// We can create a real wrapper via the round-trip helper and then call
/// DumpIso with a null path — that must return false.
/// </summary>
[Fact]
[Fact(Skip = "Common keys are validated so this cannot pass")]
public void DumpIso_NullPath_ReturnsFalse()
{
var wia = BuildMinimalWiiWia();
@@ -116,7 +109,7 @@ namespace SabreTools.Wrappers.Test
/// Builds a synthetic Wii disc with 2 fake partitions (each 1 WiiGroup = 64 × 0x8000 bytes of
/// known plaintext encrypted with an arbitrary key), converts it to WIA (NONE compression),
/// reads it back through <see cref="WIA.DumpIso"/>, then decrypts every Wii data block in the
/// dumped ISO using <see cref="NintendoDisc.DecryptBlock"/> and asserts the decrypted bytes
/// dumped ISO using <see cref="WiiDecrypter.DecryptBlock"/> and asserts the decrypted bytes
/// match the original plaintext.
///
/// This exercises both directions:
@@ -124,13 +117,13 @@ namespace SabreTools.Wrappers.Test
/// • WIA read path (<see cref="WiaVirtualStream"/>) re-encrypts WIA decrypted groups back to
/// ISO-layout AES-CBC blocks via GetCachedEncGroup / EncryptWiiGroup
///
/// Anti-bias: the final decryption uses <see cref="NintendoDisc.DecryptBlock"/> — a single-block
/// Anti-bias: the final decryption uses <see cref="WiiDecrypter.DecryptBlock"/> — a single-block
/// AES-CBC call that is completely independent of EncryptWiiGroup — so a symmetric bug
/// (broken encrypt paired with broken decrypt) would still fail the plaintext comparison.
/// The title key is encrypted via <see cref="AesCbc.Encrypt"/> (BouncyCastle), while the
/// verification uses <see cref="NintendoDisc.DecryptBlock"/> — a different code path.
/// verification uses <see cref="WiiDecrypter.DecryptBlock"/> — a different code path.
/// </summary>
[Fact]
[Fact(Skip = "Common keys are validated so this cannot pass")]
public void Wii_WiaNoneRoundTrip_Succeeds()
{
// ---- Build synthetic Wii ISO ----
@@ -225,13 +218,17 @@ namespace SabreTools.Wrappers.Test
if (nd is null)
return null;
// TODO: Force this?
nd.WiiDecrypter.RetailCommonKey = TestCommonKey;
nd.WiiDecrypter.KoreanCommonKey = TestCommonKey;
var ms = new MemoryStream();
bool ok = WIA.ConvertFromDiscToStream(nd, ms,
isRvz: false,
compressionType: Data.Models.WIA.WiaRvzCompressionType.None,
compressionLevel: 5,
chunkSize: Data.Models.WIA.Constants.DefaultChunkSize,
out _);
out var exception);
if (!ok)
return null;
@@ -338,7 +335,7 @@ namespace SabreTools.Wrappers.Test
/// <summary>
/// Decrypts each block of one WII partition in the dumped ISO using only
/// <see cref="NintendoDisc.DecryptBlock"/> (a single-block AES-CBC call that is
/// <see cref="WiiDecrypter.DecryptBlock"/> (a single-block AES-CBC call that is
/// completely independent of EncryptWiiGroup) and asserts the decrypted
/// block data matches the corresponding slice of <paramref name="expectedPlaintext"/>.
/// </summary>
@@ -363,7 +360,7 @@ namespace SabreTools.Wrappers.Test
byte[] encData = new byte[blockDataSize];
Array.Copy(iso, blockOff + 0x400, encData, 0, blockDataSize);
byte[]? dec = NintendoDisc.DecryptBlock(encData, titleKey, iv);
byte[]? dec = WiiDecrypter.DecryptBlock(encData, titleKey, iv);
Assert.NotNull(dec);
// Compare against known plaintext slice

View File

@@ -2,7 +2,7 @@ using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
// TODO: Move to IO
// TODO: Remove when IO is updated
namespace SabreTools.Wrappers
{
/// <summary>

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
// TODO: Remove when IO is updated
namespace SabreTools.Wrappers
{
/// <summary>

View File

@@ -1,7 +1,7 @@
using System;
using SabreTools.Numerics.Extensions;
// TODO: Move to IO
// TODO: Remove when IO is updated
namespace SabreTools.Wrappers
{
/// <summary>

View File

@@ -3,7 +3,7 @@ using System.IO;
using SabreTools.Hashing;
using SabreTools.Numerics.Extensions;
// TODO: Move to IO
// TODO: Remove when IO is updated
namespace SabreTools.Wrappers
{
/// <summary>

View File

@@ -2,7 +2,7 @@ using System;
using SabreTools.Hashing;
using SabreTools.Numerics.Extensions;
// TODO: Move to IO
// TODO: Remove when IO is updated
namespace SabreTools.Wrappers
{
/// <summary>

View File

@@ -1,7 +1,7 @@
using System;
using SabreTools.Numerics.Extensions;
// TODO: Move to IO
// TODO: Remove when IO is updated
namespace SabreTools.Wrappers
{
/// <summary>

View File

@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.IO;
using SabreTools.Numerics.Extensions;
// TODO: Move to IO
// TODO: Remove when IO is updated
namespace SabreTools.Wrappers
{
/// <summary>

View File

@@ -1,295 +0,0 @@
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;
// TODO: Move to IO
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
{
/// <summary>
/// Dictionary sizes per compression level 1-9 (index 0 unused).
/// </summary>
/// <remarks>Mirrors Dolphin WIACompression.cpp dict_size choices.</remarks>
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
];
/// <summary>
///
/// </summary>
/// <param name="level"></param>
/// <returns></returns>
private static int GetDictSize(int level)
=> DictSizes[Math.Max(1, Math.Min(9, level))];
/// <summary>
/// Returns the raw LZMA2 dict-size property byte for a given dictionary size.
/// </summary>
private static uint Lzma2DictSize(byte p) => (uint)((2 | (p & 1)) << ((p / 2) + 11));
/// <summary>
///
/// </summary>
/// <param name="d"></param>
/// <returns></returns>
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 <see cref="WiaHeader2.CompressorData"/>
/// and <see cref="WiaHeader2.CompressorDataSize"/>.
/// 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;
// All cases below default to 0
case WiaRvzCompressionType.None:
case WiaRvzCompressionType.Purge:
case WiaRvzCompressionType.Bzip2:
case WiaRvzCompressionType.Zstd:
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);
// Do not use compression
case WiaRvzCompressionType.None:
case WiaRvzCompressionType.Purge:
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[] lzmaProps = new byte[compressorDataSize];
Array.Copy(compressorData, lzmaProps, compressorDataSize);
return DecompressLzma(data, offset, length, lzmaProps, isLzma2: false);
case WiaRvzCompressionType.LZMA2:
byte[] lzma2Props = new byte[compressorDataSize];
Array.Copy(compressorData, lzma2Props, compressorDataSize);
return DecompressLzma(data, offset, length, lzma2Props, isLzma2: true);
case WiaRvzCompressionType.Zstd:
return DecompressZstd(data, offset, length);
// Do not use compression
case WiaRvzCompressionType.None:
case WiaRvzCompressionType.Purge:
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
/// <summary>
/// Compress data using bzip2
/// </summary>
/// <param name="data">Source data</param>
/// <param name="offset">Offset into the source data</param>
/// <param name="length">Length within the source data</param>
/// <returns>Compressed data</returns>
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();
}
/// <summary>
/// Decompress data using bzip2
/// </summary>
/// <param name="data">Source data</param>
/// <param name="offset">Offset into the source data</param>
/// <param name="length">Length within the source data</param>
/// <returns>Uncompressed data</returns>
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();
}
/// <summary>
/// Compress data using LZMA/LZMA2
/// </summary>
/// <param name="data">Source data</param>
/// <param name="offset">Offset into the source data</param>
/// <param name="length">Length within the source data</param>
/// <param name="level">LZMA/LZMA2 level</param>
/// <param name="isLzma2">Indicates if LZMA2 is used</param>
/// <returns>Compressed data</returns>
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();
}
/// <summary>
/// Decompress data using LZMA/LZMA2
/// </summary>
/// <param name="data">Source data</param>
/// <param name="offset">Offset into the source data</param>
/// <param name="length">Length within the source data</param>
/// <param name="props">LZMA properties</param>
/// <param name="isLzma2">Indicates if LZMA2 is used</param>
/// <returns>Uncompressed data</returns>
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();
}
/// <summary>
/// Compress data using Zstd
/// </summary>
/// <param name="data">Source data</param>
/// <param name="offset">Offset into the source data</param>
/// <param name="length">Length within the source data</param>
/// <param name="level">Zstd level</param>
/// <returns>Compressed data</returns>
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, level))
{
zstd.Write(data, offset, length);
}
return outMs.ToArray();
}
/// <summary>
/// Decompress data using Zstd
/// </summary>
/// <param name="data">Source data</param>
/// <param name="offset">Offset into the source data</param>
/// <param name="length">Length within the source data</param>
/// <returns>Uncompressed data</returns>
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
}
}

View File

@@ -1,32 +1,53 @@
using System;
using SabreTools.Data.Models.NintendoDisc;
using SabreTools.Hashing;
// TODO: Move to IO
// TODO: Remove when IO is updated
namespace SabreTools.Wrappers
{
public partial class NintendoDisc
public class WiiDecrypter
{
/// <summary>
/// Retail common key
/// Retail common key (index 0)
/// </summary>
public static byte[] KoreanCommonKey = [];
public byte[] RetailCommonKey
{
get;
set
{
if (ValidateRetailCommonKey(value))
field = value;
}
} = [];
/// <summary>
/// Korean common key (index 1)
/// </summary>
public byte[] KoreanCommonKey
{
get;
set
{
if (ValidateKoreanCommonKey(value))
field = value;
}
} = [];
#region Internal Test Values
/// <summary>
/// Korean common key SHA-256 hash
/// </summary>
private const string KoreanCommonKeySHA256 = "b9f42ca27a1e178f0f14ebf1a05d486fa8db8d08875336c4e6e8dfae29f2901c";
/// <summary>
/// Retail common key
/// </summary>
public static byte[] RetailCommonKey = [];
/// <summary>
/// Retail common key SHA-256 hash
/// </summary>
private const string RetailCommonKeySHA256 = "de38aeab4fe0c36d828a47e6fd315100e7ce234d3b00aa25e6ad6f5ff2824af8";
#endregion
#region Decryption
/// <summary>
/// Decrypt a Wii partition title key from the ticket data.
/// </summary>
@@ -34,7 +55,7 @@ namespace SabreTools.Wrappers
/// <param name="titleId">8-byte title ID from ticket offset 0x1DC (big-endian)</param>
/// <param name="commonKeyIndex">Common key index to use for decryption</param>
/// <returns>Decrypted 16-byte title key, or null if no key is available for the given index</returns>
public static byte[]? DecryptTitleKey(byte[]? encryptedTitleKey, byte[]? titleId, int commonKeyIndex)
public byte[]? DecryptTitleKey(byte[]? encryptedTitleKey, byte[]? titleId, int commonKeyIndex)
{
if (encryptedTitleKey is null || encryptedTitleKey.Length != 16)
return null;
@@ -49,6 +70,23 @@ namespace SabreTools.Wrappers
else
return null;
return DecryptTitleKey(encryptedTitleKey, titleId, commonKey);
}
/// <summary>
/// Decrypt a Wii partition title key from the ticket data.
/// </summary>
/// <param name="encryptedTitleKey">16-byte encrypted title key from ticket offset 0x1BF</param>
/// <param name="titleId">8-byte title ID from ticket offset 0x1DC (big-endian)</param>
/// <param name="commonKey">Common key to use for decryption</param>
/// <returns>Decrypted 16-byte title key, or null if no key is available for the given index</returns>
public static byte[]? DecryptTitleKey(byte[]? encryptedTitleKey, byte[]? titleId, byte[] commonKey)
{
if (encryptedTitleKey is null || encryptedTitleKey.Length != 16)
return null;
if (titleId is null || titleId.Length != 8)
return null;
if (commonKey.Length != 16)
return null;
@@ -68,7 +106,7 @@ namespace SabreTools.Wrappers
/// <returns>Decrypted 0x7C00-byte block data, or null on error</returns>
public static byte[]? DecryptBlock(byte[] encryptedData, byte[] titleKey, byte[] iv)
{
if (encryptedData is null || encryptedData.Length != Constants.WiiBlockDataSize)
if (encryptedData is null || encryptedData.Length != 0x7C00)
return null;
if (titleKey is null || titleKey.Length != 16)
return null;
@@ -78,6 +116,10 @@ namespace SabreTools.Wrappers
return AesCbc.Decrypt(encryptedData, titleKey, iv);
}
#endregion
#region Validation Methods
/// <summary>
/// Validate the Korean common key based on hash and length
/// </summary>
@@ -109,5 +151,7 @@ namespace SabreTools.Wrappers
string? actualHash = HashTool.GetByteArrayHash(commonKey, HashType.SHA256);
return string.Equals(actualHash, RetailCommonKeySHA256, StringComparison.OrdinalIgnoreCase);
}
#endregion
}
}

View File

@@ -10,6 +10,12 @@ namespace SabreTools.Wrappers
{
public partial class NintendoDisc : IExtractable
{
/// <summary>
/// Wii decrypter required for title key and block processing
/// </summary>
/// <remarks>Both keys should be set before using</remarks>
public readonly WiiDecrypter WiiDecrypter = new();
/// <inheritdoc/>
public bool Extract(string outputDirectory, bool includeDebug)
{
@@ -163,7 +169,7 @@ namespace SabreTools.Wrappers
Array.Copy(ticketData, Constants.TicketTitleIdOffset, titleId, 0, 8);
byte commonKeyIdx = ticketData[Constants.TicketCommonKeyIndexOffset];
byte[]? titleKey = DecryptTitleKey(encTitleKey, titleId, commonKeyIdx);
byte[]? titleKey = WiiDecrypter.DecryptTitleKey(encTitleKey, titleId, commonKeyIdx);
if (titleKey is null)
return;
@@ -589,7 +595,7 @@ namespace SabreTools.Wrappers
byte[] encData = new byte[Constants.WiiBlockDataSize];
Array.Copy(encBlock, Constants.WiiBlockHeaderSize, encData, 0, Constants.WiiBlockDataSize);
byte[]? decData = DecryptBlock(encData, titleKey, iv);
byte[]? decData = WiiDecrypter.DecryptBlock(encData, titleKey, iv);
if (decData is null)
break;

View File

@@ -189,7 +189,7 @@ namespace SabreTools.Wrappers
var rawDedupMap = new Dictionary<WiaDedupKey2, RvzGroupEntry>();
FileSystemTableReader? gcFst = isRvz ? BuildFileSystemTableReader(source) : null;
WiaRvzCompressionHelper.GetCompressorData(compressionType, compressionLevel, out byte[] propData, out byte propSize);
GetCompressorData(compressionType, compressionLevel, out byte[] propData, out byte propSize);
uint groupIdx = 0;
long srcOff = rawDataStart;
@@ -264,7 +264,7 @@ namespace SabreTools.Wrappers
{
var gi = work[w];
if (gi.MainData is not null && !gi.IsDedupHit)
gi.CompressedData = WiaRvzCompressionHelper.Compress(ct, gi.MainData, 0, gi.MainData.Length, cl, pd, ps);
gi.CompressedData = Compress(ct, gi.MainData, 0, gi.MainData.Length, cl, pd, ps);
});
}
#endif
@@ -391,7 +391,7 @@ namespace SabreTools.Wrappers
var rawRegions = BuildRawRegions(partitions, isoSize);
WiaRvzCompressionHelper.GetCompressorData(compressionType, compressionLevel, out byte[] propData, out byte propSize);
GetCompressorData(compressionType, compressionLevel, out byte[] propData, out byte propSize);
int groupEntrySize = isRvz ? RvzGroupEntrySize : WiaGroupEntrySize;
uint totalGroups = CalcTotalGroups(partitions, rawRegions, chunkSize);
@@ -717,7 +717,7 @@ namespace SabreTools.Wrappers
pw.MainDataBytes,
0,
pw.MainDataBytes.Length);
pw.CompressedData = WiaRvzCompressionHelper.Compress(compressionType, toCompress, 0, toCompress.Length, cl, pd, ps);
pw.CompressedData = Compress(compressionType, toCompress, 0, toCompress.Length, cl, pd, ps);
}
else if (compressionType == WiaRvzCompressionType.Purge)
{
@@ -942,7 +942,7 @@ namespace SabreTools.Wrappers
byte[]? compressed = null;
if (compressionType > WiaRvzCompressionType.Purge)
{
byte[] c2 = WiaRvzCompressionHelper.Compress(compressionType, mainData, 0, mainData.Length, compressionLevel, propData, propSize);
byte[] c2 = Compress(compressionType, mainData, 0, mainData.Length, compressionLevel, propData, propSize);
if (!isRvz || c2.Length < mainData.Length)
compressed = c2;
}
@@ -1023,7 +1023,7 @@ namespace SabreTools.Wrappers
byte[] encData = new byte[WiiBlockDataSize];
Array.Copy(encGroup, off + WiiBlockHeaderSize, encData, 0, WiiBlockDataSize);
byte[]? dec = NintendoDisc.DecryptBlock(encData, titleKey, iv);
byte[]? dec = WiiDecrypter.DecryptBlock(encData, titleKey, iv);
if (dec is null)
return null;
@@ -1139,7 +1139,7 @@ namespace SabreTools.Wrappers
Array.Copy(hdr, 0x1DC, titleId, 0, 8);
byte ckIdx = hdr[0x1F1];
byte[]? titleKey = NintendoDisc.DecryptTitleKey(encKey, titleId, ckIdx);
byte[]? titleKey = source.WiiDecrypter.DecryptTitleKey(encKey, titleId, ckIdx);
if (titleKey is null)
continue;
@@ -1412,7 +1412,7 @@ namespace SabreTools.Wrappers
if (compressionType == WiaRvzCompressionType.Purge)
return PurgeCompressor.Compress(data, 0, data.Length);
if (compressionType > WiaRvzCompressionType.Purge)
return WiaRvzCompressionHelper.Compress(compressionType, data, 0, data.Length, compressionLevel, propData, propSize);
return Compress(compressionType, data, 0, data.Length, compressionLevel, propData, propSize);
return data;
}

View File

@@ -3,6 +3,13 @@ using System.IO;
using SabreTools.Data.Models.NintendoDisc;
using SabreTools.Data.Models.WIA;
using SabreTools.Hashing;
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
using SabreTools.IO.Extensions;
using SharpCompress.Compressors;
using SharpCompress.Compressors.BZip2;
using SharpCompress.Compressors.LZMA;
using SharpCompress.Compressors.ZStandard;
#endif
using static SabreTools.Data.Models.NintendoDisc.Constants;
using static SabreTools.Data.Models.WIA.Constants;
using WiaReader = SabreTools.Serialization.Readers.WIA;
@@ -178,7 +185,7 @@ namespace SabreTools.Wrappers
if (read < compressedSize)
return;
byte[] plain = WiaRvzCompressionHelper.Decompress(comp, buf, 0, compressedSize, compData, compDataSize);
byte[] plain = Decompress(comp, buf, 0, compressedSize, compData, compDataSize);
if (plain is null || plain.Length < expectedSize)
return;
@@ -201,7 +208,7 @@ namespace SabreTools.Wrappers
if (read < compressedSize)
return;
byte[] plain = WiaRvzCompressionHelper.Decompress(comp, buf, 0, compressedSize, compData, compDataSize);
byte[] plain = Decompress(comp, buf, 0, compressedSize, compData, compDataSize);
if (plain is null || plain.Length < expectedSize)
return;
@@ -263,6 +270,284 @@ namespace SabreTools.Wrappers
#endregion
#region Compression
/// <summary>
/// Dictionary sizes per compression level 1-9 (index 0 unused).
/// </summary>
/// <remarks>Mirrors Dolphin WIACompression.cpp dict_size choices.</remarks>
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
];
/// <summary>
///
/// </summary>
/// <param name="level"></param>
/// <returns></returns>
private static int GetDictSize(int level)
=> DictSizes[Math.Max(1, Math.Min(9, level))];
/// <summary>
/// Returns the raw LZMA2 dict-size property byte for a given dictionary size.
/// </summary>
private static uint Lzma2DictSize(byte p) => (uint)((2 | (p & 1)) << ((p / 2) + 11));
/// <summary>
///
/// </summary>
/// <param name="d"></param>
/// <returns></returns>
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 <see cref="WiaHeader2.CompressorData"/>
/// and <see cref="WiaHeader2.CompressorDataSize"/>.
/// 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;
// All cases below default to 0
case WiaRvzCompressionType.None:
case WiaRvzCompressionType.Purge:
case WiaRvzCompressionType.Bzip2:
case WiaRvzCompressionType.Zstd:
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);
// Do not use compression
case WiaRvzCompressionType.None:
case WiaRvzCompressionType.Purge:
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[] lzmaProps = new byte[compressorDataSize];
Array.Copy(compressorData, lzmaProps, compressorDataSize);
return DecompressLzma(data, offset, length, lzmaProps, isLzma2: false);
case WiaRvzCompressionType.LZMA2:
byte[] lzma2Props = new byte[compressorDataSize];
Array.Copy(compressorData, lzma2Props, compressorDataSize);
return DecompressLzma(data, offset, length, lzma2Props, isLzma2: true);
case WiaRvzCompressionType.Zstd:
return DecompressZstd(data, offset, length);
// Do not use compression
case WiaRvzCompressionType.None:
case WiaRvzCompressionType.Purge:
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
/// <summary>
/// Compress data using bzip2
/// </summary>
/// <param name="data">Source data</param>
/// <param name="offset">Offset into the source data</param>
/// <param name="length">Length within the source data</param>
/// <returns>Compressed data</returns>
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();
}
/// <summary>
/// Decompress data using bzip2
/// </summary>
/// <param name="data">Source data</param>
/// <param name="offset">Offset into the source data</param>
/// <param name="length">Length within the source data</param>
/// <returns>Uncompressed data</returns>
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();
}
/// <summary>
/// Compress data using LZMA/LZMA2
/// </summary>
/// <param name="data">Source data</param>
/// <param name="offset">Offset into the source data</param>
/// <param name="length">Length within the source data</param>
/// <param name="level">LZMA/LZMA2 level</param>
/// <param name="isLzma2">Indicates if LZMA2 is used</param>
/// <returns>Compressed data</returns>
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();
}
/// <summary>
/// Decompress data using LZMA/LZMA2
/// </summary>
/// <param name="data">Source data</param>
/// <param name="offset">Offset into the source data</param>
/// <param name="length">Length within the source data</param>
/// <param name="props">LZMA properties</param>
/// <param name="isLzma2">Indicates if LZMA2 is used</param>
/// <returns>Uncompressed data</returns>
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();
}
/// <summary>
/// Compress data using Zstd
/// </summary>
/// <param name="data">Source data</param>
/// <param name="offset">Offset into the source data</param>
/// <param name="length">Length within the source data</param>
/// <param name="level">Zstd level</param>
/// <returns>Compressed data</returns>
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, level))
{
zstd.Write(data, offset, length);
}
return outMs.ToArray();
}
/// <summary>
/// Decompress data using Zstd
/// </summary>
/// <param name="data">Source data</param>
/// <param name="offset">Offset into the source data</param>
/// <param name="length">Length within the source data</param>
/// <returns>Uncompressed data</returns>
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
#endregion
#region Inner Wrapper
// Cache for on-demand decompression in ReadVirtual.
@@ -862,14 +1147,13 @@ namespace SabreTools.Wrappers
}
else
{
// Bzip2 / LZMA / LZMA2 / Zstd — delegate to WiaRvzCompressionHelper
// Bzip2 / LZMA / LZMA2 / Zstd — delegate to compression helpers
byte[]? workingData;
if (shouldDecompress)
{
try
{
workingData = WiaRvzCompressionHelper.Decompress(
comp, fileData, offset, length, compressorData, compressorDataSize);
workingData = Decompress(comp, fileData, offset, length, compressorData, compressorDataSize);
}
catch
{