mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-07-08 18:06:41 +00:00
1404 lines
54 KiB
C#
1404 lines
54 KiB
C#
using System;
|
|
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;
|
|
#endif
|
|
using SabreTools.Security.Cryptography;
|
|
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
|
|
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;
|
|
|
|
namespace SabreTools.Wrappers
|
|
{
|
|
public partial class WIA : WrapperBase<DiscImage>
|
|
{
|
|
#region Descriptive Properties
|
|
|
|
/// <inheritdoc/>
|
|
public override string DescriptionString => "WIA / RVZ Compressed GameCube / Wii Disc Image";
|
|
|
|
#endregion
|
|
|
|
#region Extension Properties
|
|
|
|
/// <inheritdoc cref="DiscImage.GroupEntries"/>
|
|
public WiaGroupEntry[]? GroupEntries => Model.GroupEntries;
|
|
|
|
/// <inheritdoc cref="DiscImage.Header1"/>
|
|
public WiaHeader1 Header1 => Model.Header1;
|
|
|
|
/// <inheritdoc cref="DiscImage.Header2"/>
|
|
public WiaHeader2 Header2 => Model.Header2;
|
|
|
|
/// <summary>True if this is an RVZ file; false if this is a WIA file.</summary>
|
|
public bool IsRvz => Header1.Magic == RvzMagic;
|
|
|
|
/// <inheritdoc cref="DiscImage.PartitionEntries"/>
|
|
public PartitionEntry[]? PartitionEntries => Model.PartitionEntries;
|
|
|
|
/// <inheritdoc cref="DiscImage.RawDataEntries"/>
|
|
public RawDataEntry[] RawDataEntries => Model.RawDataEntries;
|
|
|
|
/// <inheritdoc cref="DiscImage.RvzGroupEntries"/>
|
|
public RvzGroupEntry[]? RvzGroupEntries => Model.RvzGroupEntries;
|
|
|
|
/// <summary>
|
|
/// Total uncompressed ISO size in bytes
|
|
/// </summary>
|
|
public ulong IsoFileSize => Header1.IsoFileSize;
|
|
|
|
/// <summary>
|
|
/// Disc header parsed from the 128-byte raw disc header stored in Header2.
|
|
/// </summary>
|
|
public DiscHeader? DiscHeader
|
|
{
|
|
get
|
|
{
|
|
if (field is not null)
|
|
return field;
|
|
|
|
byte[]? raw = Header2.DiscHeader;
|
|
if (raw is null || raw.Length < 0x20)
|
|
return null;
|
|
|
|
using var ms = new MemoryStream(raw);
|
|
field = Serialization.Readers.NintendoDisc.ParseDiscHeader(ms);
|
|
return field;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Constructors
|
|
|
|
/// <inheritdoc/>
|
|
public WIA(DiscImage model, byte[] data) : base(model, data) { }
|
|
|
|
/// <inheritdoc/>
|
|
public WIA(DiscImage model, byte[] data, int offset) : base(model, data, offset) { }
|
|
|
|
/// <inheritdoc/>
|
|
public WIA(DiscImage model, byte[] data, int offset, int length) : base(model, data, offset, length) { }
|
|
|
|
/// <inheritdoc/>
|
|
public WIA(DiscImage model, Stream data) : base(model, data) { }
|
|
|
|
/// <inheritdoc/>
|
|
public WIA(DiscImage model, Stream data, long offset) : base(model, data, offset) { }
|
|
|
|
/// <inheritdoc/>
|
|
public WIA(DiscImage model, Stream data, long offset, long length) : base(model, data, offset, length) { }
|
|
|
|
#endregion
|
|
|
|
#region Static Constructors
|
|
|
|
/// <summary>
|
|
/// Create a WIA/RVZ wrapper from a byte array and offset
|
|
/// </summary>
|
|
/// <param name="data">Byte array representing the WIA or RVZ image</param>
|
|
/// <param name="offset">Offset within the array to parse</param>
|
|
/// <returns>A WIA wrapper on success, null on failure</returns>
|
|
public static WIA? Create(byte[]? data, int offset)
|
|
{
|
|
// If the data is invalid
|
|
if (data is null || data.Length == 0)
|
|
return null;
|
|
|
|
// If the offset is out of bounds
|
|
if (offset < 0 || offset >= data.Length)
|
|
return null;
|
|
|
|
// Create a memory stream and use that
|
|
var dataStream = new MemoryStream(data, offset, data.Length - offset);
|
|
return Create(dataStream);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a WIA/RVZ wrapper from a Stream
|
|
/// </summary>
|
|
/// <param name="data">Stream representing the WIA or RVZ image</param>
|
|
/// <returns>A WIA wrapper on success, null on failure</returns>
|
|
public static WIA? Create(Stream? data)
|
|
{
|
|
// If the data is invalid
|
|
if (data is null || !data.CanRead)
|
|
return null;
|
|
|
|
try
|
|
{
|
|
long currentOffset = data.Position;
|
|
|
|
var model = new WiaReader().Deserialize(data);
|
|
if (model is null)
|
|
return null;
|
|
|
|
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
|
|
// The reader parsed the compressed table blobs as raw bytes.
|
|
// Re-read and decompress them here now that we have the compression parameters.
|
|
DecompressTables(model, data, currentOffset);
|
|
#endif
|
|
|
|
return new WIA(model, data, currentOffset);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
|
|
/// <summary>
|
|
/// Re-reads the partition entries, raw data entries, and group entries from the source
|
|
/// stream, decompresses them using the algorithm specified in Header2, and replaces the
|
|
/// (garbage) values that the reader left in the model.
|
|
/// </summary>
|
|
private static void DecompressTables(DiscImage model, Stream data, long baseOffset)
|
|
{
|
|
var comp = model.Header2.CompressionType;
|
|
|
|
// None / Purge tables are stored as plain big-endian structs — already parsed correctly.
|
|
if (comp == WiaRvzCompressionType.None || comp == WiaRvzCompressionType.Purge)
|
|
return;
|
|
|
|
var compData = model.Header2.CompressorData ?? new byte[7];
|
|
byte compDataSize = model.Header2.CompressorDataSize;
|
|
|
|
// --- Raw data entries (stored compressed) ---
|
|
if (model.Header2.NumberOfRawDataEntries > 0
|
|
&& model.Header2.RawDataEntriesOffset > 0
|
|
&& model.Header2.RawDataEntriesSize > 0)
|
|
{
|
|
int count = (int)model.Header2.NumberOfRawDataEntries;
|
|
int compressedSize = (int)model.Header2.RawDataEntriesSize;
|
|
int expectedSize = count * RawDataEntrySize;
|
|
|
|
data.Seek(baseOffset + (long)model.Header2.RawDataEntriesOffset, SeekOrigin.Begin);
|
|
byte[] buf = new byte[compressedSize];
|
|
int read = data.Read(buf, 0, compressedSize);
|
|
if (read < compressedSize)
|
|
return;
|
|
|
|
byte[] plain = Decompress(comp, buf, 0, compressedSize, compData, compDataSize);
|
|
if (plain is null || plain.Length < expectedSize)
|
|
return;
|
|
|
|
model.RawDataEntries = ParseRawDataEntries(plain, count);
|
|
}
|
|
|
|
// --- Group entries (stored compressed) ---
|
|
if (model.Header2.NumberOfGroupEntries > 0
|
|
&& model.Header2.GroupEntriesOffset > 0
|
|
&& model.Header2.GroupEntriesSize > 0)
|
|
{
|
|
int count = (int)model.Header2.NumberOfGroupEntries;
|
|
int compressedSize = (int)model.Header2.GroupEntriesSize;
|
|
int entrySize = model.Header1.Magic == RvzMagic ? RvzGroupEntrySize : WiaGroupEntrySize;
|
|
int expectedSize = count * entrySize;
|
|
|
|
data.Seek(baseOffset + (long)model.Header2.GroupEntriesOffset, SeekOrigin.Begin);
|
|
byte[] buf = new byte[compressedSize];
|
|
int read = data.Read(buf, 0, compressedSize);
|
|
if (read < compressedSize)
|
|
return;
|
|
|
|
byte[] plain = Decompress(comp, buf, 0, compressedSize, compData, compDataSize);
|
|
if (plain is null || plain.Length < expectedSize)
|
|
return;
|
|
|
|
if (model.Header1.Magic == RvzMagic)
|
|
model.RvzGroupEntries = ParseRvzGroupEntries(plain, count);
|
|
else
|
|
model.GroupEntries = ParseWiaGroupEntries(plain, count);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses raw data entries from a plain (already decompressed) byte array.
|
|
/// </summary>
|
|
private static RawDataEntry[] ParseRawDataEntries(byte[] plain, int count)
|
|
{
|
|
var entries = new RawDataEntry[count];
|
|
|
|
int offset = 0;
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
entries[i] = WiaReader.ParseRawDataEntry(plain, ref offset);
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses WIA group entries from a plain (already decompressed) byte array.
|
|
/// </summary>
|
|
private static WiaGroupEntry[] ParseWiaGroupEntries(byte[] plain, int count)
|
|
{
|
|
var entries = new WiaGroupEntry[count];
|
|
|
|
int offset = 0;
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
entries[i] = WiaReader.ParseWiaGroupEntry(plain, ref offset);
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses RVZ group entries from a plain (already decompressed) byte array.
|
|
/// </summary>
|
|
private static RvzGroupEntry[] ParseRvzGroupEntries(byte[] plain, int count)
|
|
{
|
|
var entries = new RvzGroupEntry[count];
|
|
|
|
int offset = 0;
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
entries[i] = WiaReader.ParseRvzGroupEntry(plain, ref offset);
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
#endif
|
|
|
|
#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.
|
|
private uint _cachedRawGroupIndex = uint.MaxValue;
|
|
private byte[]? _cachedRawGroup;
|
|
private uint _cachedEncGroupIndex = uint.MaxValue;
|
|
private byte[]? _cachedEncGroup;
|
|
|
|
/// <summary>
|
|
/// Returns a NintendoDisc wrapper backed by a virtual stream that decompresses
|
|
/// WIA/RVZ groups on demand, avoiding loading the entire ISO into memory.
|
|
/// </summary>
|
|
public NintendoDisc? GetInnerWrapper()
|
|
{
|
|
if (Header1.IsoFileSize == 0)
|
|
return null;
|
|
|
|
var vStream = new WiaVirtualStream(this);
|
|
var disc = NintendoDisc.Create(vStream);
|
|
if (disc is null)
|
|
return null;
|
|
|
|
// For Wii discs: WIA/RVZ stores partition data already decrypted.
|
|
// Wire a pre-decrypted reader so NintendoDisc.Extraction bypasses its
|
|
// AES-CBC decrypt pass and reads directly from our decompressed groups.
|
|
if (PartitionEntries is not null && PartitionEntries.Length > 0)
|
|
disc._preDecryptedReader = PreDecryptedReader;
|
|
|
|
return disc;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used by <see cref="NintendoDisc._preDecryptedReader"/>.
|
|
/// Matches <paramref name="absDataOffset"/> (absolute ISO offset of the encrypted data
|
|
/// area) to the corresponding WIA <see cref="PartitionEntry"/> by comparing it with
|
|
/// de.FirstSector * 0x8000, then delegates to
|
|
/// <see cref="ReadDecryptedPartitionBytes"/>.
|
|
/// </summary>
|
|
private byte[]? PreDecryptedReader(long absDataOffset, long partitionDataOffset, int length)
|
|
{
|
|
if (PartitionEntries is null)
|
|
return null;
|
|
|
|
foreach (var entry in PartitionEntries)
|
|
{
|
|
// The data area of this partition starts at de.FirstSector * 0x8000
|
|
long deIsoStart = (long)entry.DataEntry0.FirstSector * WiiBlockSize;
|
|
long deIsoEnd = deIsoStart + ((long)entry.DataEntry0.NumberOfSectors * WiiBlockSize);
|
|
|
|
if (absDataOffset >= deIsoStart && absDataOffset < deIsoEnd)
|
|
return ReadDecryptedPartitionBytes(entry, partitionDataOffset, length);
|
|
|
|
if (entry.DataEntry1 is { NumberOfSectors: > 0 })
|
|
{
|
|
long de1Start = (long)entry.DataEntry1.FirstSector * WiiBlockSize;
|
|
long de1End = de1Start + ((long)entry.DataEntry1.NumberOfSectors * WiiBlockSize);
|
|
if (absDataOffset >= de1Start && absDataOffset < de1End)
|
|
return ReadDecryptedPartitionBytes(entry, partitionDataOffset, length);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads <paramref name="count"/> bytes of the virtual decompressed ISO at
|
|
/// <paramref name="offset"/> into <paramref name="buffer"/>, decompressing
|
|
/// WIA/RVZ groups on demand. Returns the number of bytes read.
|
|
/// </summary>
|
|
internal int ReadVirtual(long offset, byte[] buffer, int bufferOffset, int count)
|
|
{
|
|
long isoSize = (long)Header1.IsoFileSize;
|
|
if (offset >= isoSize || count <= 0)
|
|
return 0;
|
|
|
|
count = (int)Math.Min(count, isoSize - offset);
|
|
int totalRead = 0;
|
|
|
|
while (totalRead < count)
|
|
{
|
|
long pos = offset + totalRead;
|
|
int got = ReadVirtualChunk(pos, buffer, bufferOffset + totalRead, count - totalRead);
|
|
if (got <= 0)
|
|
{
|
|
// Advance past one "zero" byte to avoid infinite loops over gaps.
|
|
buffer[bufferOffset + totalRead] = 0;
|
|
totalRead++;
|
|
}
|
|
else
|
|
{
|
|
totalRead += got;
|
|
}
|
|
}
|
|
|
|
return totalRead;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads bytes for one contiguous segment of the virtual ISO starting at <paramref name="pos"/>.
|
|
/// Returns 0 if the position is not covered by any known data entry (caller fills with zeros).
|
|
/// </summary>
|
|
private int ReadVirtualChunk(long pos, byte[] buffer, int bufferOffset, int count)
|
|
{
|
|
// 1. Disc header (first 0x80 bytes stored verbatim in Header2.DiscHeader)
|
|
if (pos < DiscHeaderStoredSize && Header2.DiscHeader.Length > 0)
|
|
{
|
|
int available = (int)Math.Min(DiscHeaderStoredSize - pos, count);
|
|
int srcAvail = Math.Min(available, Header2.DiscHeader.Length - (int)pos);
|
|
if (srcAvail > 0)
|
|
Array.Copy(Header2.DiscHeader, (int)pos, buffer, bufferOffset, srcAvail);
|
|
|
|
if (available > srcAvail)
|
|
Array.Clear(buffer, bufferOffset + srcAvail, available - srcAvail);
|
|
|
|
return available;
|
|
}
|
|
|
|
uint chunkSize = Header2.ChunkSize;
|
|
var comp = Header2.CompressionType;
|
|
byte[] compData = Header2.CompressorData;
|
|
byte compDataSize = Header2.CompressorDataSize;
|
|
|
|
// 2. Raw data entries (non-partition disc data)
|
|
if (RawDataEntries.Length > 0)
|
|
{
|
|
foreach (var entry in RawDataEntries)
|
|
{
|
|
if (entry.DataSize == 0 || entry.NumberOfGroups == 0)
|
|
continue;
|
|
|
|
long rdeStart = (long)entry.DataOffset;
|
|
long rdeEnd = rdeStart + (long)entry.DataSize;
|
|
if (pos < rdeStart || pos >= rdeEnd)
|
|
continue;
|
|
|
|
long skippedData = rdeStart % 0x8000;
|
|
long adjustedBase = rdeStart - skippedData;
|
|
long adjustedPos = pos - adjustedBase;
|
|
uint g = (uint)(adjustedPos / chunkSize);
|
|
int offsetInGroup = (int)(adjustedPos % chunkSize);
|
|
|
|
if (g >= entry.NumberOfGroups)
|
|
continue;
|
|
|
|
uint groupFileIdx = entry.GroupIndex + g;
|
|
byte[]? groupBytes = GetCachedRawGroup(groupFileIdx, comp, compData, compDataSize, chunkSize);
|
|
if (groupBytes is null)
|
|
return 0;
|
|
|
|
int available = groupBytes.Length - offsetInGroup;
|
|
if (available <= 0)
|
|
return 0;
|
|
|
|
int remainingInEntry = (int)Math.Min(rdeEnd - pos, count);
|
|
|
|
// Also clamp to the end of this group
|
|
long groupIsoEnd = adjustedBase + ((long)(g + 1) * chunkSize);
|
|
int remainingInGroup = (int)Math.Min(groupIsoEnd - pos, remainingInEntry);
|
|
int toCopy = Math.Min(available, remainingInGroup);
|
|
if (toCopy <= 0)
|
|
return 0;
|
|
|
|
Array.Copy(groupBytes, offsetInGroup, buffer, bufferOffset, toCopy);
|
|
return toCopy;
|
|
}
|
|
}
|
|
|
|
// 3. Partition data entries (Wii encrypted partition data)
|
|
if (PartitionEntries is not null && PartitionEntries.Length > 0)
|
|
{
|
|
foreach (var pe in PartitionEntries)
|
|
{
|
|
int ret = ReadPartitionChunk(pe.DataEntry0,
|
|
pe.PartitionKey,
|
|
pos,
|
|
buffer,
|
|
bufferOffset,
|
|
count,
|
|
comp,
|
|
compData,
|
|
compDataSize,
|
|
chunkSize);
|
|
if (ret > 0)
|
|
return ret;
|
|
|
|
ret = ReadPartitionChunk(pe.DataEntry1,
|
|
pe.PartitionKey,
|
|
pos,
|
|
buffer,
|
|
bufferOffset,
|
|
count,
|
|
comp,
|
|
compData,
|
|
compDataSize,
|
|
chunkSize);
|
|
if (ret > 0)
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads <paramref name="length"/> bytes of decrypted Wii partition data beginning at
|
|
/// <paramref name="partDataOffset"/>, a byte offset in the 0x7C00-block partition-data space.
|
|
/// Spans across both DataEntry0 and DataEntry1 of the partition entry.
|
|
/// Maps directly to the decompressed WIA/RVZ group data — no re-encryption is performed.
|
|
/// </summary>
|
|
internal byte[]? ReadDecryptedPartitionBytes(PartitionEntry pe, long partDataOffset, int length)
|
|
{
|
|
if (length <= 0 || pe is null)
|
|
return null;
|
|
|
|
uint chunkSize = Header2.ChunkSize;
|
|
var comp = Header2.CompressionType;
|
|
byte[] compData = Header2.CompressorData ?? new byte[7];
|
|
byte compDataSize = Header2.CompressorDataSize;
|
|
int blocksPerGroup = (int)(chunkSize / WiiBlockSize);
|
|
|
|
byte[] result = new byte[length];
|
|
int produced = 0;
|
|
|
|
// DataEntry0 covers [0 .. de0.NumberOfSectors * 0x7C00) in partition-data space
|
|
var de0 = pe.DataEntry0;
|
|
long de0DataSize = (long)de0.NumberOfSectors * WiiBlockDataSize;
|
|
|
|
// DataEntry1 (if present) immediately follows
|
|
var de1 = pe.DataEntry1;
|
|
long de1DataSize = de1 is not null ? (long)de1.NumberOfSectors * WiiBlockDataSize : 0;
|
|
|
|
while (produced < length)
|
|
{
|
|
long off = partDataOffset + produced;
|
|
|
|
// Determine which DataEntry covers this offset
|
|
PartitionDataEntry de;
|
|
long deRelOff; // offset within this DataEntry's decrypted data space
|
|
if (off < de0DataSize)
|
|
{
|
|
de = de0;
|
|
deRelOff = off;
|
|
}
|
|
else if (de1 is not null && de1.NumberOfGroups > 0 && off < de0DataSize + de1DataSize)
|
|
{
|
|
de = de1;
|
|
deRelOff = off - de0DataSize;
|
|
}
|
|
else
|
|
{
|
|
break; // beyond available data
|
|
}
|
|
|
|
long blockNum = deRelOff / WiiBlockDataSize;
|
|
int offsetInBlock = (int)(deRelOff % WiiBlockDataSize);
|
|
long groupRelative = blockNum / blocksPerGroup;
|
|
int blockInGroup = (int)(blockNum % blocksPerGroup);
|
|
|
|
if (groupRelative >= de.NumberOfGroups)
|
|
break;
|
|
|
|
uint groupFileIdx = de.GroupIndex + (uint)groupRelative;
|
|
long dataOffsetForLfg = groupRelative * blocksPerGroup * WiiBlockDataSize;
|
|
|
|
byte[]? decrypted = ReadDecryptedGroupData(groupFileIdx,
|
|
comp,
|
|
compData,
|
|
compDataSize,
|
|
blocksPerGroup,
|
|
WiiBlockDataSize,
|
|
dataOffsetForLfg);
|
|
if (decrypted is null)
|
|
break;
|
|
|
|
int offsetInGroup = (blockInGroup * WiiBlockDataSize) + offsetInBlock;
|
|
int available = decrypted.Length - offsetInGroup;
|
|
if (available <= 0)
|
|
break;
|
|
|
|
int remainingInGroup = (blocksPerGroup * WiiBlockDataSize) - offsetInGroup;
|
|
int toCopy = Math.Min(length - produced, Math.Min(available, remainingInGroup));
|
|
if (toCopy <= 0)
|
|
break;
|
|
|
|
Array.Copy(decrypted, offsetInGroup, result, produced, toCopy);
|
|
produced += toCopy;
|
|
}
|
|
|
|
if (produced <= 0)
|
|
return null;
|
|
if (produced < length)
|
|
Array.Resize(ref result, produced);
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="de"></param>
|
|
/// <param name="partitionKey"></param>
|
|
/// <param name="pos"></param>
|
|
/// <param name="buffer"></param>
|
|
/// <param name="bufferOffset"></param>
|
|
/// <param name="count"></param>
|
|
/// <param name="comp"></param>
|
|
/// <param name="compData"></param>
|
|
/// <param name="compDataSize"></param>
|
|
/// <param name="chunkSize"></param>
|
|
/// <returns></returns>
|
|
private int ReadPartitionChunk(PartitionDataEntry de,
|
|
byte[] partitionKey,
|
|
long pos,
|
|
byte[] buffer,
|
|
int bufferOffset,
|
|
int count,
|
|
WiaRvzCompressionType comp,
|
|
byte[] compData,
|
|
byte compDataSize,
|
|
uint chunkSize)
|
|
{
|
|
if (de.NumberOfSectors == 0 || de.NumberOfGroups == 0)
|
|
return 0;
|
|
|
|
if (chunkSize == 0)
|
|
return 0;
|
|
|
|
int blocksPerGroup = (int)(chunkSize / WiiBlockSize);
|
|
long isoDataStart = (long)de.FirstSector * WiiBlockSize;
|
|
long isoDataEnd = isoDataStart + ((long)de.NumberOfSectors * WiiBlockSize);
|
|
|
|
if (pos < isoDataStart || pos >= isoDataEnd)
|
|
return 0;
|
|
|
|
long offsetInPartition = pos - isoDataStart;
|
|
long blockNum = offsetInPartition / WiiBlockSize;
|
|
int offsetInBlock = (int)(offsetInPartition % WiiBlockSize);
|
|
|
|
long groupNum = blockNum / blocksPerGroup;
|
|
int blockInGroup = (int)(blockNum % blocksPerGroup);
|
|
|
|
if (groupNum >= de.NumberOfGroups)
|
|
return 0;
|
|
|
|
uint groupFileIdx = de.GroupIndex + (uint)groupNum;
|
|
byte[]? encryptedGroup = GetCachedEncGroup(groupFileIdx, de, partitionKey, comp, compData, compDataSize, blocksPerGroup);
|
|
if (encryptedGroup is null)
|
|
return 0;
|
|
|
|
int offsetInEncGroup = (blockInGroup * WiiBlockSize) + offsetInBlock;
|
|
int available = encryptedGroup.Length - offsetInEncGroup;
|
|
if (available <= 0)
|
|
return 0;
|
|
|
|
long remainingInEntry = isoDataEnd - pos;
|
|
|
|
// Stay within this group
|
|
long groupIsoEnd = isoDataStart + ((groupNum + 1) * blocksPerGroup * WiiBlockSize);
|
|
long remainingInGroup = groupIsoEnd - pos;
|
|
int toCopy = (int)Math.Min(count, Math.Min(Math.Min(available, remainingInEntry), remainingInGroup));
|
|
if (toCopy <= 0)
|
|
return 0;
|
|
|
|
Array.Copy(encryptedGroup, offsetInEncGroup, buffer, bufferOffset, toCopy);
|
|
return toCopy;
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="groupFileIdx"></param>
|
|
/// <param name="comp"></param>
|
|
/// <param name="compData"></param>
|
|
/// <param name="compDataSize"></param>
|
|
/// <param name="chunkSize"></param>
|
|
/// <returns></returns>
|
|
private byte[]? GetCachedRawGroup(uint groupFileIdx,
|
|
WiaRvzCompressionType comp,
|
|
byte[] compData,
|
|
byte compDataSize,
|
|
uint chunkSize)
|
|
{
|
|
if (_cachedRawGroupIndex == groupFileIdx)
|
|
return _cachedRawGroup;
|
|
|
|
byte[]? group = ReadGroupRaw(groupFileIdx, comp, compData, compDataSize, chunkSize);
|
|
_cachedRawGroupIndex = groupFileIdx;
|
|
_cachedRawGroup = group;
|
|
return group;
|
|
}
|
|
|
|
/// <summary>
|
|
///
|
|
/// </summary>
|
|
/// <param name="groupFileIdx"></param>
|
|
/// <param name="de"></param>
|
|
/// <param name="partitionKey"></param>
|
|
/// <param name="comp"></param>
|
|
/// <param name="compData"></param>
|
|
/// <param name="compDataSize"></param>
|
|
/// <param name="blocksPerGroup"></param>
|
|
/// <returns></returns>
|
|
private byte[]? GetCachedEncGroup(uint groupFileIdx,
|
|
PartitionDataEntry de,
|
|
byte[] partitionKey,
|
|
WiaRvzCompressionType comp,
|
|
byte[] compData,
|
|
byte compDataSize,
|
|
int blocksPerGroup)
|
|
{
|
|
if (_cachedEncGroupIndex == groupFileIdx)
|
|
return _cachedEncGroup;
|
|
|
|
long dataOffsetForLfg = (groupFileIdx - de.GroupIndex) * blocksPerGroup * 0x7C00;
|
|
byte[]? decrypted = ReadDecryptedGroupData(groupFileIdx, comp, compData, compDataSize, blocksPerGroup, 0x7C00, dataOffsetForLfg);
|
|
if (decrypted is null)
|
|
return null;
|
|
|
|
byte[] encrypted = EncryptWiiGroup(decrypted, partitionKey, blocksPerGroup);
|
|
_cachedEncGroupIndex = groupFileIdx;
|
|
_cachedEncGroup = encrypted;
|
|
return encrypted;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads and decompresses one raw (non-partition) group.
|
|
/// Returns chunkSize bytes of raw ISO data, or null on failure.
|
|
/// </summary>
|
|
private byte[]? ReadGroupRaw(uint groupIdx,
|
|
WiaRvzCompressionType comp,
|
|
byte[] compressorData,
|
|
byte compressorDataSize,
|
|
uint chunkSize)
|
|
{
|
|
if (IsRvz)
|
|
{
|
|
if (RvzGroupEntries is null || groupIdx >= RvzGroupEntries.Length)
|
|
return null;
|
|
|
|
var ge = RvzGroupEntries[groupIdx];
|
|
bool isRvzCompressed = (ge.DataSize & 0x80000000u) != 0;
|
|
uint dataSize = ge.DataSize & 0x7FFFFFFFu;
|
|
if (dataSize == 0)
|
|
return new byte[chunkSize];
|
|
|
|
byte[] fileData = ReadRangeFromSource((long)ge.DataOffset << 2, (int)dataSize);
|
|
return DecompressGroupBytes(fileData,
|
|
0,
|
|
(int)dataSize,
|
|
comp,
|
|
compressorData,
|
|
compressorDataSize,
|
|
(int)chunkSize,
|
|
IsRvz,
|
|
isRvzCompressed,
|
|
ge.RvzPackedSize,
|
|
groupIdx * chunkSize,
|
|
isWiiPartition: false,
|
|
chunkSize);
|
|
}
|
|
else
|
|
{
|
|
if (GroupEntries is null || groupIdx >= GroupEntries.Length)
|
|
return null;
|
|
|
|
var ge = GroupEntries[groupIdx];
|
|
if (ge.DataSize == 0)
|
|
return new byte[chunkSize];
|
|
|
|
byte[] fileData = ReadRangeFromSource((long)ge.DataOffset << 2, (int)ge.DataSize);
|
|
return DecompressGroupBytes(fileData,
|
|
0,
|
|
(int)ge.DataSize,
|
|
comp,
|
|
compressorData,
|
|
compressorDataSize,
|
|
(int)chunkSize,
|
|
IsRvz,
|
|
false,
|
|
0,
|
|
0,
|
|
false,
|
|
chunkSize);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads and decompresses a Wii partition group, returning the hash-stripped decrypted data.
|
|
/// </summary>
|
|
private byte[]? ReadDecryptedGroupData(uint groupIdx,
|
|
WiaRvzCompressionType comp,
|
|
byte[] compressorData,
|
|
byte compressorDataSize,
|
|
int blocksPerGroup,
|
|
int blockDataSize,
|
|
long dataOffsetForLfg)
|
|
{
|
|
int decryptedGroupSize = blocksPerGroup * blockDataSize;
|
|
|
|
if (IsRvz)
|
|
{
|
|
if (RvzGroupEntries is null || groupIdx >= RvzGroupEntries.Length)
|
|
return null;
|
|
|
|
var ge = RvzGroupEntries[groupIdx];
|
|
bool isRvzCompressed = (ge.DataSize & 0x80000000u) != 0;
|
|
uint dataSize = ge.DataSize & 0x7FFFFFFFu;
|
|
if (dataSize == 0)
|
|
return new byte[decryptedGroupSize];
|
|
|
|
byte[] fileData = ReadRangeFromSource((long)ge.DataOffset << 2, (int)dataSize);
|
|
return DecompressGroupBytes(fileData,
|
|
0,
|
|
(int)dataSize,
|
|
comp,
|
|
compressorData,
|
|
compressorDataSize,
|
|
decryptedGroupSize,
|
|
IsRvz,
|
|
isRvzCompressed,
|
|
ge.RvzPackedSize,
|
|
dataOffsetForLfg,
|
|
true,
|
|
Header2.ChunkSize);
|
|
}
|
|
else
|
|
{
|
|
if (GroupEntries is null || groupIdx >= GroupEntries.Length)
|
|
return null;
|
|
|
|
var ge = GroupEntries[groupIdx];
|
|
if (ge.DataSize == 0)
|
|
return new byte[decryptedGroupSize];
|
|
|
|
byte[] fileData2 = ReadRangeFromSource((long)ge.DataOffset << 2, (int)ge.DataSize);
|
|
return DecompressGroupBytes(fileData2,
|
|
0,
|
|
(int)ge.DataSize,
|
|
comp,
|
|
compressorData,
|
|
compressorDataSize,
|
|
decryptedGroupSize,
|
|
IsRvz,
|
|
false,
|
|
0,
|
|
0L,
|
|
true,
|
|
Header2.ChunkSize);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decompresses raw group bytes according to the WIA compression type and strips any
|
|
/// exception-list header, returning the plain data payload.
|
|
/// </summary>
|
|
private static byte[]? DecompressGroupBytes(byte[] fileData,
|
|
int offset,
|
|
int length,
|
|
WiaRvzCompressionType comp,
|
|
byte[] compressorData,
|
|
byte compressorDataSize,
|
|
int expectedSize,
|
|
bool isRvz,
|
|
bool isRvzCompressed,
|
|
uint rvzPackedSize,
|
|
long dataOffsetForLfg,
|
|
bool isWiiPartition,
|
|
uint chunkSize = 2 * 1024 * 1024)
|
|
{
|
|
if (fileData is null || fileData.Length < length)
|
|
return null;
|
|
|
|
// Mirrors DolphinIsoLib WiaRvzReader::ReadGroupCore logic:
|
|
// Decompress first (Bzip2/LZMA/LZMA2/Zstd), then RVZ-unpack junk regions if present.
|
|
bool shouldDecompress = comp > WiaRvzCompressionType.Purge && (!isRvz || isRvzCompressed);
|
|
|
|
if (comp == WiaRvzCompressionType.None)
|
|
{
|
|
// NONE: exception lists precede data with 4-byte alignment for Wii partitions
|
|
int dataStart = isWiiPartition ? SkipExceptionLists(fileData, offset, length, chunkSize) : offset;
|
|
int mainLen = length - (dataStart - offset);
|
|
byte[] noneData = new byte[expectedSize];
|
|
Array.Copy(fileData, dataStart, noneData, 0, Math.Min(mainLen, expectedSize));
|
|
return noneData;
|
|
}
|
|
else if (comp == WiaRvzCompressionType.Purge)
|
|
{
|
|
// Exception list precedes the Purge payload; capture it for SHA-1, then decompress.
|
|
int purgeStart = isWiiPartition ? SkipExceptionLists(fileData, offset, length, chunkSize) : offset;
|
|
int exceptionLen = purgeStart - offset;
|
|
byte[]? exceptionBytes = exceptionLen > 0 ? new byte[exceptionLen] : null;
|
|
if (exceptionBytes is not null)
|
|
Array.Copy(fileData, offset, exceptionBytes, 0, exceptionLen);
|
|
|
|
int purgeLen = length - exceptionLen;
|
|
return IO.Compression.PURGE.Decompressor.Decompress(fileData, purgeStart, purgeLen, expectedSize, exceptionBytes);
|
|
}
|
|
else
|
|
{
|
|
// Bzip2 / LZMA / LZMA2 / Zstd — delegate to compression helpers
|
|
byte[]? workingData;
|
|
if (shouldDecompress)
|
|
{
|
|
try
|
|
{
|
|
workingData = Decompress(comp, fileData, offset, length, compressorData, compressorDataSize);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (workingData is null)
|
|
return null;
|
|
}
|
|
else
|
|
{
|
|
workingData = fileData;
|
|
}
|
|
|
|
// RVZ-pack step: junk regions are stored as LFG seeds rather than raw bytes.
|
|
if (isRvz && rvzPackedSize > 0)
|
|
{
|
|
// Exception lists are always present for Wii partition groups.
|
|
// When compressed (shouldDecompress=true), they are NOT padded to 4-byte alignment.
|
|
// When uncompressed (shouldDecompress=false), they ARE padded to 4-byte alignment.
|
|
int rvzDataStart = isWiiPartition
|
|
? (shouldDecompress
|
|
? SkipExceptionListsNoAlign(workingData, 0, workingData.Length, chunkSize)
|
|
: SkipExceptionLists(workingData, 0, workingData.Length, chunkSize))
|
|
: 0;
|
|
int rvzDataLen = workingData.Length - rvzDataStart;
|
|
byte[] rvzPayload = new byte[rvzDataLen];
|
|
Array.Copy(workingData, rvzDataStart, rvzPayload, 0, rvzDataLen);
|
|
|
|
var rvzDecomp = new IO.Compression.RVZPack.Decompressor(rvzPayload, rvzPackedSize, dataOffsetForLfg);
|
|
byte[] unpacked = new byte[expectedSize];
|
|
int bytesRead = rvzDecomp.Decompress(unpacked, 0, expectedSize);
|
|
if (bytesRead < expectedSize)
|
|
Array.Resize(ref unpacked, bytesRead);
|
|
|
|
return unpacked;
|
|
}
|
|
|
|
// Skip exception lists always present for Wii partition groups.
|
|
// Compressed groups: no 4-byte alignment. Uncompressed groups: 4-byte aligned.
|
|
int dataStart = isWiiPartition
|
|
? (shouldDecompress
|
|
? SkipExceptionListsNoAlign(workingData, 0, workingData.Length, chunkSize)
|
|
: SkipExceptionLists(workingData, 0, workingData.Length, chunkSize))
|
|
: 0;
|
|
int mainLen = workingData.Length - dataStart;
|
|
byte[] data = new byte[expectedSize];
|
|
Array.Copy(workingData, dataStart, data, 0, Math.Min(mainLen, expectedSize));
|
|
return data;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Skips the packed exception-list header at the start of group data (NONE/Purge path).
|
|
/// Exception lists are 4-byte-aligned after the last list.
|
|
/// Returns the offset of the first data byte.
|
|
/// </summary>
|
|
private static int SkipExceptionLists(byte[] data, int offset, int length, uint chunkSize = 2 * 1024 * 1024)
|
|
{
|
|
// Number of exception lists = max(1, chunkSize / WiiGroupSize).
|
|
// For WIA chunkSize==2MiB this is always 1.
|
|
// For RVZ sub-2MiB chunks this is also 1 (chunkSize <= groupSize).
|
|
const uint WiiGroupSize = 2 * 1024 * 1024; // 0x200000
|
|
int numLists = Math.Max(1, (int)(chunkSize / WiiGroupSize));
|
|
|
|
int pos = offset;
|
|
for (int i = 0; i < numLists && pos + 2 <= offset + length; i++)
|
|
{
|
|
ushort count = (ushort)((data[pos] << 8) | data[pos + 1]);
|
|
pos += 2;
|
|
|
|
// Each exception entry is 2 + 20 = 22 bytes
|
|
pos += count * 22;
|
|
|
|
// 4-byte alignment after last list
|
|
if (i == numLists - 1)
|
|
pos = (pos + 3) & ~3;
|
|
}
|
|
|
|
return pos;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Skips exception lists in compressed group data (Bzip2/LZMA/etc.) where
|
|
/// lists are NOT 4-byte aligned.
|
|
/// </summary>
|
|
private static int SkipExceptionListsNoAlign(byte[] data, int offset, int length, uint chunkSize = 2 * 1024 * 1024)
|
|
{
|
|
const uint WiiGroupSize = 2 * 1024 * 1024;
|
|
int numLists = Math.Max(1, (int)(chunkSize / WiiGroupSize));
|
|
|
|
int pos = offset;
|
|
for (int i = 0; i < numLists && pos + 2 <= offset + length; i++)
|
|
{
|
|
ushort count = (ushort)((data[pos] << 8) | data[pos + 1]);
|
|
pos += 2;
|
|
pos += count * 22;
|
|
}
|
|
|
|
return pos;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Re-encrypts one decrypted hash-stripped Wii group back into standard ISO-layout
|
|
/// encrypted 0x8000-byte blocks. Mirrors Dolphin's VolumeWii::EncryptGroup.
|
|
/// </summary>
|
|
internal static byte[] EncryptWiiGroup(byte[] decryptedData, byte[] key, int blocksPerGroup)
|
|
{
|
|
const int H0Count = 31;
|
|
const int H1Count = 8;
|
|
const int H2Count = 8;
|
|
const int HashLen = 20;
|
|
|
|
// --- Build H0 / H1 / H2 hash arrays ---
|
|
byte[][][] h0 = new byte[blocksPerGroup][][];
|
|
for (int b = 0; b < blocksPerGroup; b++)
|
|
{
|
|
h0[b] = new byte[H0Count][];
|
|
int blockBase = b * WiiBlockDataSize;
|
|
for (int h = 0; h < H0Count; h++)
|
|
{
|
|
int src = blockBase + (h * 0x400);
|
|
int len = Math.Min(0x400, decryptedData.Length - src);
|
|
h0[b][h] = ComputeSha1(decryptedData, src < decryptedData.Length ? src : 0, Math.Max(0, len));
|
|
}
|
|
}
|
|
|
|
// H1[h1Group][slot] = SHA1 of block (h1Group*8+slot)'s 31 H0 hashes
|
|
byte[][][] h1 = new byte[H1Count][][];
|
|
for (int g = 0; g < H1Count; g++)
|
|
{
|
|
h1[g] = new byte[H1Count][];
|
|
for (int s = 0; s < H1Count; s++)
|
|
{
|
|
int blockIdx = (g * H1Count) + s;
|
|
if (blockIdx >= blocksPerGroup)
|
|
{
|
|
h1[g][s] = new byte[HashLen];
|
|
continue;
|
|
}
|
|
|
|
byte[] h0Concat = new byte[H0Count * HashLen];
|
|
for (int i = 0; i < H0Count; i++)
|
|
{
|
|
Array.Copy(h0[blockIdx][i], 0, h0Concat, i * HashLen, HashLen);
|
|
}
|
|
|
|
h1[g][s] = ComputeSha1(h0Concat, 0, h0Concat.Length);
|
|
}
|
|
}
|
|
|
|
// H2[h2Idx] = SHA1 of H1 group h2Idx's 8 hashes (same for every block)
|
|
byte[][] h2 = new byte[H2Count][];
|
|
for (int i = 0; i < H2Count; i++)
|
|
{
|
|
int grp = Math.Min(i, h1.Length - 1);
|
|
byte[] h1Concat = new byte[H1Count * HashLen];
|
|
for (int s = 0; s < H1Count; s++)
|
|
{
|
|
Array.Copy(h1[grp][s], 0, h1Concat, s * HashLen, HashLen);
|
|
}
|
|
|
|
h2[i] = ComputeSha1(h1Concat, 0, h1Concat.Length);
|
|
}
|
|
|
|
byte[] result = new byte[blocksPerGroup * WiiBlockSize];
|
|
|
|
for (int b = 0; b < blocksPerGroup; b++)
|
|
{
|
|
// Serialize hash block
|
|
byte[] hashBlock = new byte[WiiBlockHashSize];
|
|
int off = 0;
|
|
|
|
// H0 (31 * 20 = 0x26C)
|
|
for (int i = 0; i < H0Count; i++)
|
|
{
|
|
Array.Copy(h0[b][i], 0, hashBlock, off, HashLen);
|
|
off += HashLen;
|
|
}
|
|
|
|
off += 0x14; // padding0
|
|
|
|
// H1 for this block's group (8 * 20 = 0xA0)
|
|
int h1Grp = b / H1Count;
|
|
if (h1Grp < h1.Length)
|
|
{
|
|
for (int i = 0; i < H1Count; i++)
|
|
{
|
|
Array.Copy(h1[h1Grp][i], 0, hashBlock, off, HashLen);
|
|
off += HashLen;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
off += H1Count * HashLen;
|
|
}
|
|
|
|
off += 0x20; // padding1
|
|
|
|
// H2 (8 * 20 = 0xA0)
|
|
for (int i = 0; i < H2Count; i++) { Array.Copy(h2[i], 0, hashBlock, off, HashLen); off += HashLen; }
|
|
// Note: off is now 0x3D4; IV will sit at 0x3D0 after encryption
|
|
|
|
// Encrypt hash block with IV = zero
|
|
byte[] encHashBlock = AESCBC.Encrypt(hashBlock, key, new byte[16]) ?? new byte[WiiBlockHashSize];
|
|
|
|
// Extract IV for data block from offset 0x3D0 of the encrypted hash block
|
|
byte[] iv = new byte[16];
|
|
Array.Copy(encHashBlock, 0x3D0, iv, 0, 16);
|
|
|
|
// Encrypt data block
|
|
int dataSrc = b * WiiBlockDataSize;
|
|
int dataLen = Math.Min(WiiBlockDataSize, decryptedData.Length - dataSrc);
|
|
byte[] dataBlock = new byte[WiiBlockDataSize];
|
|
if (dataLen > 0)
|
|
Array.Copy(decryptedData, dataSrc, dataBlock, 0, dataLen);
|
|
|
|
byte[] encDataBlock = AESCBC.Encrypt(dataBlock, key, iv) ?? new byte[WiiBlockDataSize];
|
|
|
|
int dest = b * WiiBlockSize;
|
|
Array.Copy(encHashBlock, 0, result, dest, WiiBlockHashSize);
|
|
Array.Copy(encDataBlock, 0, result, dest + WiiBlockHashSize, WiiBlockDataSize);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a segmented SHA-1 hash for input data
|
|
/// </summary>
|
|
private static byte[] ComputeSha1(byte[] data, int offset, int count)
|
|
{
|
|
if (count == 0)
|
|
return new byte[20];
|
|
|
|
using var sha1 = new HashWrapper(HashType.SHA1);
|
|
sha1.Process(data, offset, count);
|
|
sha1.Terminate();
|
|
return sha1.CurrentHashBytes ?? new byte[20];
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|