mirror of
https://github.com/SabreTools/SabreTools.Serialization.git
synced 2026-07-08 18:06:41 +00:00
First round of cleanup for DolphinLib additions
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
using System.IO;
|
||||
using SabreTools.IO.Compression.Deflate;
|
||||
using SabreTools.IO.Extensions;
|
||||
using static SabreTools.Data.Models.GCZ.Constants;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
public partial class GCZ : IExtractable
|
||||
@@ -5,9 +10,87 @@ namespace SabreTools.Wrappers
|
||||
/// <inheritdoc/>
|
||||
public bool Extract(string outputDirectory, bool includeDebug)
|
||||
{
|
||||
// Decompress GCZ to obtain the inner disc image, then delegate extraction.
|
||||
var inner = GetInnerWrapper();
|
||||
return inner?.Extract(outputDirectory, includeDebug) ?? false;
|
||||
if (inner is null)
|
||||
return false;
|
||||
|
||||
return inner.Extract(outputDirectory, includeDebug);
|
||||
}
|
||||
|
||||
#region Inner Wrapper
|
||||
|
||||
/// <summary>
|
||||
/// Returns a NintendoDisc wrapper backed by a virtual stream that decompresses
|
||||
/// GCZ blocks on demand, avoiding loading the entire ISO into memory.
|
||||
/// </summary>
|
||||
public NintendoDisc? GetInnerWrapper()
|
||||
{
|
||||
if (BlockPointers.Length == 0)
|
||||
return null;
|
||||
if (Header.DataSize == 0)
|
||||
return null;
|
||||
|
||||
var stream = new GczVirtualStream(this);
|
||||
return NintendoDisc.Create(stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses a single GCZ block by index and returns its raw bytes.
|
||||
/// Returns null on failure; returns a zero-filled block if the compressed size is zero.
|
||||
/// </summary>
|
||||
internal byte[]? DecompressBlock(int blockIndex)
|
||||
{
|
||||
if (blockIndex < 0 || blockIndex >= BlockPointers.Length)
|
||||
return null;
|
||||
|
||||
ulong ptr = BlockPointers[blockIndex];
|
||||
long blockFileOffset = DataOffset + (long)(ptr & ~UncompressedFlag);
|
||||
|
||||
ulong nextRaw = (blockIndex + 1 < BlockPointers.Length)
|
||||
? BlockPointers[blockIndex + 1] & ~UncompressedFlag
|
||||
: CompressedDataSize;
|
||||
|
||||
int compSize = (int)(nextRaw - (ptr & ~UncompressedFlag));
|
||||
if (compSize <= 0)
|
||||
return new byte[BlockSize];
|
||||
|
||||
byte[] raw = ReadRangeFromSource(blockFileOffset, compSize);
|
||||
if (raw.Length != compSize)
|
||||
return null;
|
||||
|
||||
// Verify Adler-32 checksum on the compressed (raw) data before decompressing
|
||||
if (BlockHashes is not null && blockIndex < BlockHashes.Length)
|
||||
{
|
||||
uint actual = Adler.Adler32(1, raw, 0, raw.Length);
|
||||
if (actual != BlockHashes[blockIndex])
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the data is raw, just return
|
||||
if ((ptr & UncompressedFlag) != 0)
|
||||
return raw;
|
||||
|
||||
// GCZ blocks are zlib-framed: 2-byte header + deflate data + 4-byte Adler-32 trailer.
|
||||
// Strip the frame and feed raw deflate data to DeflateStream.
|
||||
if (raw.Length < 6)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
using var cs = new MemoryStream(raw, 2, raw.Length - 6);
|
||||
using var ds = new DeflateStream(cs, CompressionMode.Decompress);
|
||||
using var os = new MemoryStream();
|
||||
|
||||
ds.BlockCopy(os, blockSize: 4096);
|
||||
|
||||
return os.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Text;
|
||||
using SabreTools.Data.Models.GCZ;
|
||||
using SabreTools.Data.Models.NintendoDisc;
|
||||
using SabreTools.Text.Extensions;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
@@ -18,25 +20,41 @@ namespace SabreTools.Wrappers
|
||||
{
|
||||
builder.AppendLine("GCZ Information:");
|
||||
builder.AppendLine("-------------------------");
|
||||
builder.AppendLine(Header.MagicCookie, "Magic Cookie");
|
||||
builder.AppendLine(Header.SubType, "Sub-Type");
|
||||
builder.AppendLine(Header.CompressedDataSize, "Compressed Data Size");
|
||||
builder.AppendLine(Header.DataSize, "Uncompressed Data Size");
|
||||
builder.AppendLine(Header.BlockSize, "Block Size");
|
||||
builder.AppendLine(Header.NumBlocks, "Block Count");
|
||||
builder.AppendLine();
|
||||
|
||||
var discHeader = DiscHeader;
|
||||
if (discHeader is not null)
|
||||
Print(builder, Header);
|
||||
Print(builder, DiscHeader);
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, GczHeader header)
|
||||
{
|
||||
builder.AppendLine(" Header:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
|
||||
builder.AppendLine(header.MagicCookie, "Magic Cookie");
|
||||
builder.AppendLine(header.SubType, "Sub-Type");
|
||||
builder.AppendLine(header.CompressedDataSize, "Compressed Data Size");
|
||||
builder.AppendLine(header.DataSize, "Uncompressed Data Size");
|
||||
builder.AppendLine(header.BlockSize, "Block Size");
|
||||
builder.AppendLine(header.NumBlocks, "Block Count");
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, DiscHeader? header)
|
||||
{
|
||||
builder.AppendLine(" Embedded Disc Header:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
if (header is null)
|
||||
{
|
||||
builder.AppendLine("Embedded Disc Header:");
|
||||
builder.AppendLine(discHeader.GameId, " Game ID");
|
||||
builder.AppendLine(discHeader.MakerCode, " Maker Code");
|
||||
builder.AppendLine(discHeader.DiscNumber, " Disc Number");
|
||||
builder.AppendLine(discHeader.DiscVersion, " Disc Version");
|
||||
builder.AppendLine(discHeader.GameTitle, " Game Title");
|
||||
builder.AppendLine(" No embedded disc header");
|
||||
builder.AppendLine();
|
||||
return;
|
||||
}
|
||||
|
||||
builder.AppendLine(header.GameId, " Game ID");
|
||||
builder.AppendLine(header.DiscNumber, " Disc Number");
|
||||
builder.AppendLine(header.DiscVersion, " Disc Version");
|
||||
builder.AppendLine(header.GameTitle, " Game Title");
|
||||
builder.AppendLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,14 @@
|
||||
using System;
|
||||
using SabreTools.IO.Compression.Deflate;
|
||||
using System.IO;
|
||||
using SabreTools.Data.Models.GCZ;
|
||||
using SabreTools.IO.Compression.Deflate;
|
||||
using SabreTools.IO.Extensions;
|
||||
using SabreTools.Numerics.Extensions;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
public partial class GCZ : IWritable
|
||||
{
|
||||
/// <summary>
|
||||
/// Compress a NintendoDisc wrapper to a GCZ file at the given path.
|
||||
/// </summary>
|
||||
/// <param name="source">Decompressed disc image to compress.</param>
|
||||
/// <param name="outputPath">Destination file path.</param>
|
||||
/// <param name="blockSize">
|
||||
/// GCZ block size: 32 KiB, 64 KiB, or 128 KiB.
|
||||
/// Defaults to <see cref="Constants.DefaultBlockSize"/> (32 KiB).
|
||||
/// </param>
|
||||
/// <returns>True on success, false on failure.</returns>
|
||||
public static bool ConvertFromDisc(NintendoDisc source, string outputPath,
|
||||
uint blockSize = Constants.DefaultBlockSize)
|
||||
{
|
||||
if (source is null)
|
||||
return false;
|
||||
if (string.IsNullOrEmpty(outputPath))
|
||||
return false;
|
||||
if (blockSize != Constants.BlockSize32K &&
|
||||
blockSize != Constants.BlockSize64K &&
|
||||
blockSize != Constants.BlockSize128K)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using var fs = File.Open(outputPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
|
||||
return WriteGcz(source, fs, blockSize);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Write(string outputPath, bool includeDebug)
|
||||
{
|
||||
@@ -53,19 +22,44 @@ namespace SabreTools.Wrappers
|
||||
outputPath = Path.GetFullPath(outputFilename);
|
||||
}
|
||||
|
||||
if (Model?.Header is null)
|
||||
{
|
||||
if (includeDebug) Console.WriteLine("Model was invalid, cannot write!");
|
||||
return false;
|
||||
}
|
||||
|
||||
var writer = new Serialization.Writers.GCZ { Debug = includeDebug };
|
||||
return writer.SerializeFile(Model, outputPath);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Core GCZ compression pipeline (ISO → GCZ)
|
||||
// -----------------------------------------------------------------------
|
||||
#region Core GCZ Compression Pipeline (ISO -> GCZ)
|
||||
|
||||
/// <summary>
|
||||
/// Compress a NintendoDisc wrapper to a GCZ file at the given path.
|
||||
/// </summary>
|
||||
/// <param name="source">Decompressed disc image to compress.</param>
|
||||
/// <param name="outputPath">Destination file path.</param>
|
||||
/// <param name="blockSize">
|
||||
/// GCZ block size: 32 KiB, 64 KiB, or 128 KiB.
|
||||
/// Defaults to <see cref="Constants.DefaultBlockSize"/> (32 KiB).
|
||||
/// </param>
|
||||
/// <returns>True on success, false on failure.</returns>
|
||||
public static bool ConvertFromDisc(NintendoDisc source, string outputPath, uint blockSize = Constants.DefaultBlockSize)
|
||||
{
|
||||
if (string.IsNullOrEmpty(outputPath))
|
||||
return false;
|
||||
|
||||
if (blockSize != Constants.BlockSize32K
|
||||
&& blockSize != Constants.BlockSize64K
|
||||
&& blockSize != Constants.BlockSize128K)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var fs = File.Open(outputPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
|
||||
return WriteGcz(source, fs, blockSize);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a GCZ image to <paramref name="destination"/> from a decompressed disc source.
|
||||
@@ -83,36 +77,36 @@ namespace SabreTools.Wrappers
|
||||
long headerPos = destination.Position;
|
||||
var header = new GczHeader
|
||||
{
|
||||
MagicCookie = Constants.MagicCookie,
|
||||
SubType = 0,
|
||||
MagicCookie = Constants.MagicCookie,
|
||||
SubType = 0,
|
||||
CompressedDataSize = 0,
|
||||
DataSize = (ulong)sourceSize,
|
||||
BlockSize = blockSize,
|
||||
NumBlocks = numBlocks,
|
||||
DataSize = (ulong)sourceSize,
|
||||
BlockSize = blockSize,
|
||||
NumBlocks = numBlocks,
|
||||
};
|
||||
WriteHeader(destination, header);
|
||||
Serialization.Writers.GCZ.WriteHeader(destination, header);
|
||||
|
||||
// ---- Step 2: Reserve block-pointer table (8 bytes each) ----
|
||||
long blockTablePos = destination.Position;
|
||||
var blockPointers = new ulong[numBlocks];
|
||||
destination.Position += (long)numBlocks * 8;
|
||||
var blockPointers = new ulong[numBlocks];
|
||||
destination.SeekIfPossible(numBlocks * 8, SeekOrigin.Current);
|
||||
|
||||
// ---- Step 3: Reserve block-hash table (4 bytes each) ----
|
||||
var blockHashes = new uint[numBlocks];
|
||||
destination.Position += (long)numBlocks * 4;
|
||||
destination.SeekIfPossible(numBlocks * 4, SeekOrigin.Current);
|
||||
|
||||
// ---- Step 4: Data section starts here ----
|
||||
long dataStartPos = destination.Position;
|
||||
var readBuf = new byte[blockSize];
|
||||
var compressBuf = new byte[(int)blockSize * 2];
|
||||
long dataStartPos = destination.Position;
|
||||
var readBuf = new byte[blockSize];
|
||||
var compressBuf = new byte[(int)blockSize * 2];
|
||||
|
||||
for (uint bi = 0; bi < numBlocks; bi++)
|
||||
for (int i = 0; i < numBlocks; i++)
|
||||
{
|
||||
long blockOffset = (long)bi * blockSize;
|
||||
int blockDataSize = (int)Math.Min(blockSize, sourceSize - blockOffset);
|
||||
long blockOffset = i * blockSize;
|
||||
int blockDataSize = (int)Math.Min(blockSize, sourceSize - blockOffset);
|
||||
|
||||
byte[]? raw = source.ReadData(blockOffset, blockDataSize);
|
||||
if (raw is null || raw.Length != blockDataSize)
|
||||
byte[] raw = source.ReadData(blockOffset, blockDataSize);
|
||||
if (raw.Length != blockDataSize)
|
||||
return false;
|
||||
|
||||
if (blockDataSize < readBuf.Length)
|
||||
@@ -123,20 +117,18 @@ namespace SabreTools.Wrappers
|
||||
// Record pointer as offset relative to data section start
|
||||
ulong blockPointer = (ulong)(destination.Position - dataStartPos);
|
||||
|
||||
int compressedSize;
|
||||
bool useCompression = TryCompressBlock(readBuf, blockDataSize, compressBuf, out compressedSize);
|
||||
|
||||
bool useCompression = TryCompressBlock(readBuf, blockDataSize, compressBuf, out int compressedSize);
|
||||
if (useCompression)
|
||||
{
|
||||
blockPointers[bi] = blockPointer;
|
||||
blockPointers[i] = blockPointer;
|
||||
destination.Write(compressBuf, 0, compressedSize);
|
||||
blockHashes[bi] = Adler.Adler32(1, compressBuf, 0, compressedSize);
|
||||
blockHashes[i] = Adler.Adler32(1, compressBuf, 0, compressedSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
blockPointers[bi] = blockPointer | Constants.UncompressedFlag;
|
||||
blockPointers[i] = blockPointer | Constants.UncompressedFlag;
|
||||
destination.Write(readBuf, 0, blockDataSize);
|
||||
blockHashes[bi] = Adler.Adler32(1, readBuf, 0, blockDataSize);
|
||||
blockHashes[i] = Adler.Adler32(1, readBuf, 0, blockDataSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,26 +137,30 @@ namespace SabreTools.Wrappers
|
||||
header.CompressedDataSize = (ulong)(finalEnd - dataStartPos);
|
||||
|
||||
// ---- Step 6: Write block-pointer table ----
|
||||
destination.Position = blockTablePos;
|
||||
destination.SeekIfPossible(blockTablePos, SeekOrigin.Begin);
|
||||
foreach (ulong ptr in blockPointers)
|
||||
WriteUInt64LE(destination, ptr);
|
||||
{
|
||||
destination.WriteLittleEndian(ptr);
|
||||
}
|
||||
|
||||
// ---- Step 7: Write block-hash table ----
|
||||
foreach (uint h in blockHashes)
|
||||
WriteUInt32LE(destination, h);
|
||||
{
|
||||
destination.WriteLittleEndian(h);
|
||||
}
|
||||
|
||||
// ---- Step 8: Patch header ----
|
||||
destination.Position = headerPos;
|
||||
WriteHeader(destination, header);
|
||||
destination.SeekIfPossible(headerPos, SeekOrigin.Begin);
|
||||
Serialization.Writers.GCZ.WriteHeader(destination, header);
|
||||
|
||||
destination.Position = finalEnd;
|
||||
destination.Flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Compression helpers
|
||||
// -----------------------------------------------------------------------
|
||||
#endregion
|
||||
|
||||
#region Compression Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to zlib-compress <paramref name="inputSize"/> bytes of <paramref name="input"/>
|
||||
@@ -172,57 +168,31 @@ namespace SabreTools.Wrappers
|
||||
/// when the result is smaller than 97 % of the original (Dolphin's threshold).
|
||||
/// GCZ uses the zlib framing: 2-byte header (0x78 0x9C) + deflate stream + 4-byte Adler-32 tail.
|
||||
/// </summary>
|
||||
private static bool TryCompressBlock(byte[] input, int inputSize, byte[] output, out int compressedSize)
|
||||
{
|
||||
using (var ms = new MemoryStream(output))
|
||||
{
|
||||
ms.WriteByte(0x78);
|
||||
ms.WriteByte(0x9C);
|
||||
|
||||
using (var ds = new DeflateStream(ms, CompressionMode.Compress, leaveOpen: true))
|
||||
private static bool TryCompressBlock(byte[] input, int inputSize, byte[] output, out int compressedSize)
|
||||
{
|
||||
ds.Write(input, 0, inputSize);
|
||||
using (var ms = new MemoryStream(output))
|
||||
{
|
||||
ms.WriteByte(0x78);
|
||||
ms.WriteByte(0x9C);
|
||||
|
||||
using (var ds = new DeflateStream(ms, CompressionMode.Compress, leaveOpen: true))
|
||||
{
|
||||
ds.Write(input, 0, inputSize);
|
||||
}
|
||||
|
||||
uint adler = Adler.Adler32(1, input, 0, inputSize);
|
||||
ms.WriteByte((byte)(adler >> 24));
|
||||
ms.WriteByte((byte)(adler >> 16));
|
||||
ms.WriteByte((byte)(adler >> 8));
|
||||
ms.WriteByte((byte)adler);
|
||||
|
||||
compressedSize = (int)ms.Position;
|
||||
}
|
||||
|
||||
int threshold = inputSize * 97 / 100;
|
||||
return compressedSize < threshold;
|
||||
}
|
||||
|
||||
uint adler = Adler.Adler32(1, input, 0, inputSize);
|
||||
ms.WriteByte((byte)(adler >> 24));
|
||||
ms.WriteByte((byte)(adler >> 16));
|
||||
ms.WriteByte((byte)(adler >> 8));
|
||||
ms.WriteByte((byte)adler);
|
||||
|
||||
compressedSize = (int)ms.Position;
|
||||
}
|
||||
|
||||
int threshold = inputSize * 97 / 100;
|
||||
return compressedSize < threshold;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Little-endian binary write helpers
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private static void WriteHeader(Stream s, GczHeader h)
|
||||
{
|
||||
WriteUInt32LE(s, h.MagicCookie);
|
||||
WriteUInt32LE(s, h.SubType);
|
||||
WriteUInt64LE(s, h.CompressedDataSize);
|
||||
WriteUInt64LE(s, h.DataSize);
|
||||
WriteUInt32LE(s, h.BlockSize);
|
||||
WriteUInt32LE(s, h.NumBlocks);
|
||||
}
|
||||
|
||||
private static void WriteUInt32LE(Stream s, uint v)
|
||||
{
|
||||
s.WriteByte((byte)v);
|
||||
s.WriteByte((byte)(v >> 8));
|
||||
s.WriteByte((byte)(v >> 16));
|
||||
s.WriteByte((byte)(v >> 24));
|
||||
}
|
||||
|
||||
private static void WriteUInt64LE(Stream s, ulong v)
|
||||
{
|
||||
WriteUInt32LE(s, (uint)v);
|
||||
WriteUInt32LE(s, (uint)(v >> 32));
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using SabreTools.Data.Models.GCZ;
|
||||
using SabreTools.Data.Models.NintendoDisc;
|
||||
using SabreTools.IO.Compression.Deflate;
|
||||
using SabreTools.IO.Extensions;
|
||||
using static SabreTools.Data.Models.GCZ.Constants;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
@@ -19,39 +21,24 @@ namespace SabreTools.Wrappers
|
||||
/// <inheritdoc cref="DiscImage.Header"/>
|
||||
public GczHeader Header => Model.Header;
|
||||
|
||||
/// <summary>
|
||||
/// Total decompressed size of the disc image in bytes
|
||||
/// </summary>
|
||||
public ulong DataSize => Model.Header.DataSize;
|
||||
/// <inheritdoc cref="GczHeader.CompressedDataSize"/>
|
||||
public ulong CompressedDataSize => Header.CompressedDataSize;
|
||||
|
||||
/// <summary>
|
||||
/// Number of compressed blocks in this image
|
||||
/// </summary>
|
||||
public uint NumBlocks => Model.Header.NumBlocks;
|
||||
/// <inheritdoc cref="GczHeader.DataSize"/>
|
||||
public ulong DataSize => Header.DataSize;
|
||||
|
||||
/// <summary>
|
||||
/// Size of each uncompressed block in bytes
|
||||
/// </summary>
|
||||
public uint BlockSize => Model.Header.BlockSize;
|
||||
/// <inheritdoc cref="GczHeader.NumBlocks"/>
|
||||
public uint NumBlocks => Header.NumBlocks;
|
||||
|
||||
/// <summary>
|
||||
/// Block pointer table — top bit indicates uncompressed flag
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="GczHeader.BlockSize"/>
|
||||
public uint BlockSize => Header.BlockSize;
|
||||
|
||||
/// <inheritdoc cref="DiscImage.BlockPointers"/>
|
||||
public ulong[] BlockPointers => Model.BlockPointers;
|
||||
|
||||
/// <summary>
|
||||
/// Adler-32 hashes of each uncompressed block
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="DiscImage.BlockHashes"/>
|
||||
public uint[] BlockHashes => Model.BlockHashes;
|
||||
|
||||
/// <summary>
|
||||
/// Byte offset within the GCZ file where the compressed block data begins.
|
||||
/// Computed as: <c>HeaderSize + (NumBlocks * 8) + (NumBlocks * 4)</c>.
|
||||
/// </summary>
|
||||
private long DataOffset => Data.Models.GCZ.Constants.HeaderSize
|
||||
+ ((long)Model.Header.NumBlocks * 8)
|
||||
+ ((long)Model.Header.NumBlocks * 4);
|
||||
|
||||
/// <summary>
|
||||
/// Disc header parsed by decompressing the first block of the GCZ image.
|
||||
/// </summary>
|
||||
@@ -59,16 +46,19 @@ namespace SabreTools.Wrappers
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_discHeaderCached)
|
||||
return _discHeader;
|
||||
_discHeader = ReadDiscHeader();
|
||||
_discHeaderCached = true;
|
||||
return _discHeader;
|
||||
if (field is not null)
|
||||
return field;
|
||||
|
||||
field = ReadDiscHeader();
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
private DiscHeader? _discHeader;
|
||||
private bool _discHeaderCached;
|
||||
/// <summary>
|
||||
/// Byte offset within the GCZ file where the compressed block data begins.
|
||||
/// Computed as: HeaderSize + (NumBlocks * 8) + (NumBlocks * 4).
|
||||
/// </summary>
|
||||
public long DataOffset => HeaderSize + (NumBlocks * 8) + (NumBlocks * 4);
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -146,83 +136,7 @@ namespace SabreTools.Wrappers
|
||||
|
||||
#endregion
|
||||
|
||||
#region Inner Wrapper
|
||||
|
||||
/// <summary>
|
||||
/// Returns a NintendoDisc wrapper backed by a virtual stream that decompresses
|
||||
/// GCZ blocks on demand, avoiding loading the entire ISO into memory.
|
||||
/// </summary>
|
||||
public NintendoDisc? GetInnerWrapper()
|
||||
{
|
||||
if (Model.BlockPointers is null || Model.BlockPointers.Length == 0)
|
||||
return null;
|
||||
|
||||
if (Model.Header.DataSize == 0)
|
||||
return null;
|
||||
|
||||
var vStream = new GczVirtualStream(this);
|
||||
return NintendoDisc.Create(vStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses a single GCZ block by index and returns its raw bytes.
|
||||
/// Returns null on failure; returns a zero-filled block if the compressed size is zero.
|
||||
/// </summary>
|
||||
internal byte[]? DecompressBlock(int blockIndex)
|
||||
{
|
||||
const ulong UncompressedFlag = 0x8000000000000000UL;
|
||||
|
||||
if (blockIndex < 0 || blockIndex >= Model.BlockPointers.Length)
|
||||
return null;
|
||||
|
||||
ulong ptr = Model.BlockPointers[blockIndex];
|
||||
bool uncompressed = (ptr & UncompressedFlag) != 0;
|
||||
long blockFileOffset = DataOffset + (long)(ptr & ~UncompressedFlag);
|
||||
|
||||
ulong nextRaw = (blockIndex + 1 < Model.BlockPointers.Length)
|
||||
? Model.BlockPointers[blockIndex + 1] & ~UncompressedFlag
|
||||
: Model.Header.CompressedDataSize;
|
||||
int compSize = (int)(nextRaw - (ptr & ~UncompressedFlag));
|
||||
|
||||
if (compSize <= 0)
|
||||
return new byte[Model.Header.BlockSize];
|
||||
|
||||
byte[] raw = ReadRangeFromSource(blockFileOffset, compSize);
|
||||
if (raw is null || raw.Length != compSize)
|
||||
return null;
|
||||
|
||||
// Verify Adler-32 checksum on the compressed (raw) data before decompressing
|
||||
if (Model.BlockHashes != null && blockIndex < Model.BlockHashes.Length)
|
||||
{
|
||||
uint actual = Adler.Adler32(1, raw, 0, raw.Length);
|
||||
if (actual != Model.BlockHashes[blockIndex])
|
||||
return null;
|
||||
}
|
||||
|
||||
if (uncompressed)
|
||||
return raw;
|
||||
|
||||
// GCZ blocks are zlib-framed: 2-byte header + deflate data + 4-byte Adler-32 trailer.
|
||||
// Strip the frame and feed raw deflate data to DeflateStream.
|
||||
if (raw.Length < 6)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
using var cs = new MemoryStream(raw, 2, raw.Length - 6);
|
||||
using var ds = new DeflateStream(cs, CompressionMode.Decompress);
|
||||
using var os = new MemoryStream();
|
||||
byte[] buf = new byte[4096];
|
||||
int n;
|
||||
while ((n = ds.Read(buf, 0, buf.Length)) > 0)
|
||||
os.Write(buf, 0, n);
|
||||
return os.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
#region Header
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses just the first block of the GCZ image to read the disc header,
|
||||
@@ -230,57 +144,54 @@ namespace SabreTools.Wrappers
|
||||
/// </summary>
|
||||
private DiscHeader? ReadDiscHeader()
|
||||
{
|
||||
const ulong UncompressedFlag = 0x8000000000000000UL;
|
||||
|
||||
if (Model.BlockPointers is null || Model.BlockPointers.Length == 0)
|
||||
if (BlockPointers is null || BlockPointers.Length == 0)
|
||||
return null;
|
||||
|
||||
ulong ptr = Model.BlockPointers[0];
|
||||
bool uncompressed = (ptr & UncompressedFlag) != 0;
|
||||
long blockFileOffset = DataOffset + (long)(ptr & ~UncompressedFlag);
|
||||
ulong pointer = BlockPointers[0];
|
||||
|
||||
ulong nextRaw = Model.BlockPointers.Length > 1
|
||||
? Model.BlockPointers[1] & ~UncompressedFlag
|
||||
: Model.Header.CompressedDataSize;
|
||||
int compSize = (int)(nextRaw - (ptr & ~UncompressedFlag));
|
||||
ulong nextRaw = BlockPointers.Length > 1
|
||||
? BlockPointers[1] & ~UncompressedFlag
|
||||
: Header.CompressedDataSize;
|
||||
|
||||
int compSize = (int)(nextRaw - (pointer & ~UncompressedFlag));
|
||||
if (compSize <= 0)
|
||||
return null;
|
||||
|
||||
long blockFileOffset = DataOffset + (long)(pointer & ~UncompressedFlag);
|
||||
byte[] raw = ReadRangeFromSource(blockFileOffset, compSize);
|
||||
if (raw is null || raw.Length != compSize)
|
||||
if (raw.Length != compSize)
|
||||
return null;
|
||||
|
||||
bool uncompressed = (pointer & UncompressedFlag) != 0;
|
||||
if (uncompressed)
|
||||
{
|
||||
using var ms2 = new MemoryStream(raw);
|
||||
var disc2 = new Serialization.Readers.NintendoDisc().Deserialize(ms2);
|
||||
return disc2?.Header;
|
||||
var disc = new Serialization.Readers.NintendoDisc().Deserialize(raw, offset: 0);
|
||||
return disc?.Header;
|
||||
}
|
||||
|
||||
if (raw.Length < 6)
|
||||
return null;
|
||||
|
||||
byte[] block;
|
||||
try
|
||||
else
|
||||
{
|
||||
using var cs = new MemoryStream(raw, 2, raw.Length - 6);
|
||||
using var ds = new DeflateStream(cs, CompressionMode.Decompress);
|
||||
using var os = new MemoryStream();
|
||||
byte[] buf = new byte[4096];
|
||||
int n;
|
||||
while ((n = ds.Read(buf, 0, buf.Length)) > 0)
|
||||
os.Write(buf, 0, n);
|
||||
block = os.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (raw.Length < 6)
|
||||
return null;
|
||||
|
||||
using var ms = new MemoryStream(block);
|
||||
var disc = new Serialization.Readers.NintendoDisc().Deserialize(ms);
|
||||
return disc?.Header;
|
||||
byte[] block;
|
||||
try
|
||||
{
|
||||
using var cs = new MemoryStream(raw, 2, raw.Length - 6);
|
||||
using var ds = new DeflateStream(cs, CompressionMode.Decompress);
|
||||
using var os = new MemoryStream();
|
||||
|
||||
ds.BlockCopy(os, blockSize: 4096);
|
||||
|
||||
block = os.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var disc = new Serialization.Readers.NintendoDisc().Deserialize(block, offset: 0);
|
||||
return disc?.Header;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -2,19 +2,20 @@ using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.Security;
|
||||
|
||||
// TODO: Move to IO
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
/// <summary>
|
||||
/// AES-128-CBC encrypt/decrypt helpers used by NintendoDisc and WIA/RVZ.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Implemented directly via BouncyCastle because <c>SabreTools.Security.Cryptography</c>
|
||||
/// currently only exposes AES-CTR. When an <c>AESCBC</c> wrapper is added to that
|
||||
/// Implemented directly via BouncyCastle because SabreTools.Security.Cryptography
|
||||
/// currently only exposes AES-CTR. When an AESCBC wrapper is added to that
|
||||
/// library, replace the bodies of <see cref="Decrypt"/> and <see cref="Encrypt"/> with
|
||||
/// the equivalent <c>AESCBC.Decrypt</c> / <c>AESCBC.Encrypt</c> calls and remove the
|
||||
/// the equivalent AESCBC.Decrypt / AESCBC.Encrypt calls and remove the
|
||||
/// BouncyCastle using directives from this file.
|
||||
/// </remarks>
|
||||
internal static class AesCbc
|
||||
public static class AesCbc
|
||||
{
|
||||
/// <summary>
|
||||
/// Decrypts <paramref name="data"/> with AES-128-CBC (no padding).
|
||||
@@ -60,11 +61,16 @@ namespace SabreTools.Wrappers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an AES/CBC cipher with a given key and initial value
|
||||
/// </summary>
|
||||
private static IBufferedCipher CreateCipher(bool forEncryption, byte[] key, byte[] iv)
|
||||
{
|
||||
var keyParam = new KeyParameter(key);
|
||||
var cipher = CipherUtilities.GetCipher("AES/CBC/NoPadding");
|
||||
|
||||
cipher.Init(forEncryption, new ParametersWithIV(keyParam, iv));
|
||||
|
||||
return cipher;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using SabreTools.Numerics.Extensions;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
@@ -7,31 +6,33 @@ namespace SabreTools.Wrappers
|
||||
/// Lightweight GameCube / Wii File-System Table (FST) reader used by
|
||||
/// <see cref="RvzPackEncoder"/> to distinguish real-file regions from junk.
|
||||
///
|
||||
/// Mirrors Dolphin's <c>FileSystemGCWii</c> offset-to-file-info cache
|
||||
/// (<c>m_offset_file_info_cache</c>).
|
||||
/// Mirrors Dolphin's FileSystemGCWii offset-to-file-info cache
|
||||
/// (m_offset_file_info_cache).
|
||||
/// </summary>
|
||||
internal sealed class GcFst
|
||||
public sealed class FileSystemTableReader
|
||||
{
|
||||
private const int EntrySize = 12;
|
||||
|
||||
/// <summary>File entry with start and end byte offsets on disc.</summary>
|
||||
internal struct FileEntry
|
||||
/// <summary>
|
||||
/// File entry with start and end byte offsets on disc
|
||||
/// </summary>
|
||||
public struct FileEntry
|
||||
{
|
||||
public long FileStart;
|
||||
public long FileEnd;
|
||||
}
|
||||
|
||||
// Sorted ascending by FileEnd for O(log n) upper_bound queries.
|
||||
/// <remarks>
|
||||
/// Sorted ascending by FileEnd for O(log n) upper_bound queries.
|
||||
/// </remarks>
|
||||
private readonly List<FileEntry> _files;
|
||||
|
||||
private GcFst(List<FileEntry> files)
|
||||
private FileSystemTableReader(List<FileEntry> files)
|
||||
{
|
||||
_files = files;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a raw FST binary blob and returns a <see cref="GcFst"/>,
|
||||
/// or <c>null</c> if the data is too short or structurally invalid.
|
||||
/// Parses a raw FST binary blob and returns a <see cref="FileSystemTableReader"/>,
|
||||
/// or null if the data is too short or structurally invalid.
|
||||
/// </summary>
|
||||
/// <param name="fstData">
|
||||
/// Raw FST bytes exactly as stored on disc (GameCube) or in decrypted
|
||||
@@ -41,50 +42,50 @@ namespace SabreTools.Wrappers
|
||||
/// Bit-shift to convert raw file-offset fields to byte addresses.
|
||||
/// 0 for GameCube (direct bytes); 2 for Wii (offset × 4).
|
||||
/// </param>
|
||||
public static GcFst? TryParse(byte[] fstData, int offsetShift)
|
||||
public static FileSystemTableReader? TryParse(byte[] fstData, int offsetShift)
|
||||
{
|
||||
if (fstData == null || fstData.Length < EntrySize)
|
||||
// Read the file system table
|
||||
var table = Serialization.Readers.NintendoDisc.ParseFileSystemTable(fstData);
|
||||
if (table is null)
|
||||
return null;
|
||||
|
||||
// Root entry (index 0): FILE_SIZE field = total number of FST entries.
|
||||
int rootOffset = 8;
|
||||
uint totalEntries = fstData.ReadUInt32BigEndian(ref rootOffset);
|
||||
if (totalEntries < 1 || ((long)totalEntries * EntrySize) > fstData.Length)
|
||||
return null;
|
||||
|
||||
var files = new List<FileEntry>((int)(totalEntries - 1));
|
||||
|
||||
for (uint i = 1; i < totalEntries; i++)
|
||||
// Filter out the entries to non-empty files only
|
||||
var filtered = new List<FileEntry>();
|
||||
foreach (var entry in table.Entries)
|
||||
{
|
||||
int off = (int)(i * EntrySize);
|
||||
int nameOffPos = off;
|
||||
int fileOffPos = off + 4;
|
||||
int fileSizePos = off + 8;
|
||||
uint nameOffField = fstData.ReadUInt32BigEndian(ref nameOffPos);
|
||||
uint fileOffField = fstData.ReadUInt32BigEndian(ref fileOffPos);
|
||||
uint fileSizeField = fstData.ReadUInt32BigEndian(ref fileSizePos);
|
||||
// Directory entry
|
||||
if ((entry.NameOffset & 0xFF000000) != 0)
|
||||
continue;
|
||||
|
||||
if ((nameOffField & 0xFF000000u) != 0) continue; // directory entry
|
||||
if (fileSizeField == 0) continue; // empty file
|
||||
// Empty file
|
||||
if (entry.FileSize == 0)
|
||||
continue;
|
||||
|
||||
long fileStart = (long)fileOffField << offsetShift;
|
||||
long fileEnd = fileStart + fileSizeField;
|
||||
files.Add(new FileEntry { FileStart = fileStart, FileEnd = fileEnd });
|
||||
long fileStart = entry.FileOffset << offsetShift;
|
||||
long fileEnd = fileStart + entry.FileSize;
|
||||
|
||||
var fileEntry = new FileEntry
|
||||
{
|
||||
FileStart = fileStart,
|
||||
FileEnd = fileEnd,
|
||||
};
|
||||
filtered.Add(fileEntry);
|
||||
}
|
||||
|
||||
// Sort ascending by FileEnd so binary-search upper_bound works correctly.
|
||||
files.Sort(delegate(FileEntry a, FileEntry b)
|
||||
filtered.Sort(delegate (FileEntry a, FileEntry b)
|
||||
{
|
||||
return a.FileEnd.CompareTo(b.FileEnd);
|
||||
});
|
||||
|
||||
return new GcFst(files);
|
||||
return new FileSystemTableReader(filtered);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file entry whose byte range contains <paramref name="discOffset"/>,
|
||||
/// or <c>null</c> if no file does.
|
||||
/// or null if no file does.
|
||||
/// </summary>
|
||||
/// TODO: Determine how to use List<T>.BinarySearch here
|
||||
public FileEntry? FindFileInfo(long discOffset)
|
||||
{
|
||||
if (_files.Count == 0)
|
||||
@@ -104,7 +105,7 @@ namespace SabreTools.Wrappers
|
||||
if (lo >= _files.Count)
|
||||
return null;
|
||||
|
||||
FileEntry e = _files[lo];
|
||||
var e = _files[lo];
|
||||
if (e.FileStart <= discOffset)
|
||||
return e;
|
||||
|
||||
@@ -113,7 +114,7 @@ namespace SabreTools.Wrappers
|
||||
|
||||
/// <summary>
|
||||
/// Returns the smallest FileEnd value strictly greater than
|
||||
/// <paramref name="discOffset"/>, or <c>null</c> if there is none.
|
||||
/// <paramref name="discOffset"/>, or null if there is none.
|
||||
/// </summary>
|
||||
public long? FindNextFileEnd(long discOffset)
|
||||
{
|
||||
@@ -135,7 +136,7 @@ namespace SabreTools.Wrappers
|
||||
|
||||
/// <summary>
|
||||
/// Returns the smallest FileStart value strictly greater than
|
||||
/// <paramref name="discOffset"/>, or <c>null</c> if there is none.
|
||||
/// <paramref name="discOffset"/>, or null if there is none.
|
||||
/// </summary>
|
||||
public long? FindNextFileStart(long discOffset)
|
||||
{
|
||||
@@ -166,6 +167,5 @@ namespace SabreTools.Wrappers
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,16 @@ namespace SabreTools.Wrappers
|
||||
/// A read-only seekable stream that decompresses GCZ blocks on demand.
|
||||
/// Avoids loading the entire decompressed disc image into memory.
|
||||
/// </summary>
|
||||
internal sealed class GczVirtualStream : Stream
|
||||
public sealed class GczVirtualStream : Stream
|
||||
{
|
||||
/// <summary>
|
||||
/// GCZ wrapper used for reading
|
||||
/// </summary>
|
||||
private readonly GCZ _gcz;
|
||||
|
||||
/// <summary>
|
||||
/// Virtual position within the stream
|
||||
/// </summary>
|
||||
private long _position;
|
||||
|
||||
// Single-block cache to avoid re-decompressing on adjacent reads within the same block.
|
||||
@@ -21,10 +28,19 @@ namespace SabreTools.Wrappers
|
||||
_gcz = gcz ?? throw new ArgumentNullException(nameof(gcz));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanRead => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanSeek => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanWrite => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override long Length => (long)_gcz.DataSize;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override long Position
|
||||
{
|
||||
get => _position;
|
||||
@@ -32,10 +48,12 @@ namespace SabreTools.Wrappers
|
||||
{
|
||||
if (value < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
|
||||
_position = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (buffer is null)
|
||||
@@ -78,25 +96,15 @@ namespace SabreTools.Wrappers
|
||||
return totalRead;
|
||||
}
|
||||
|
||||
private byte[]? GetBlock(int blockIndex)
|
||||
{
|
||||
if (_cachedBlockIndex == blockIndex)
|
||||
return _cachedBlock;
|
||||
|
||||
byte[]? block = _gcz.DecompressBlock(blockIndex);
|
||||
_cachedBlockIndex = blockIndex;
|
||||
_cachedBlock = block;
|
||||
return block;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
long newPos;
|
||||
switch (origin)
|
||||
{
|
||||
case SeekOrigin.Begin: newPos = offset; break;
|
||||
case SeekOrigin.Begin: newPos = offset; break;
|
||||
case SeekOrigin.Current: newPos = _position + offset; break;
|
||||
case SeekOrigin.End: newPos = Length + offset; break;
|
||||
case SeekOrigin.End: newPos = Length + offset; break;
|
||||
default: throw new ArgumentOutOfRangeException(nameof(origin));
|
||||
}
|
||||
|
||||
@@ -107,10 +115,29 @@ namespace SabreTools.Wrappers
|
||||
return _position;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Flush() { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
|
||||
|
||||
/// <summary>
|
||||
/// Decompress and retrieve a possibly-cached data block
|
||||
/// </summary>
|
||||
/// <param name="blockIndex">Block index to retrieve</param>
|
||||
/// <returns>Decompressed block data on success, null otherwise</returns>
|
||||
private byte[]? GetBlock(int blockIndex)
|
||||
{
|
||||
if (_cachedBlockIndex == blockIndex)
|
||||
return _cachedBlock;
|
||||
|
||||
byte[]? block = _gcz.DecompressBlock(blockIndex);
|
||||
_cachedBlockIndex = blockIndex;
|
||||
_cachedBlock = block;
|
||||
return block;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,72 +1,102 @@
|
||||
using System;
|
||||
using SabreTools.Numerics.Extensions;
|
||||
|
||||
// TODO: Move to IO
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
/// <summary>
|
||||
/// Lagged Fibonacci Generator matching Dolphin's LaggedFibonacciGenerator exactly.
|
||||
/// Used to regenerate Nintendo's deterministic "junk" padding data in disc images.
|
||||
/// RVZ format identifies junk regions and stores only a 68-byte seed (17 u32 words)
|
||||
/// RVZ format identifies junk regions and stores only a 68-byte seed (17 uint words)
|
||||
/// instead of the full data, enabling significant compression of padding areas.
|
||||
/// </summary>
|
||||
internal class LaggedFibonacciGenerator
|
||||
public class LaggedFibonacciGenerator
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
private const int LFG_K = 521;
|
||||
private const int LFG_J = 32;
|
||||
|
||||
/// <summary>Size of the LFG output buffer in bytes (LFG_K * 4 = 2084).</summary>
|
||||
public const int BUFFER_BYTES = LFG_K * 4;
|
||||
|
||||
/// <summary>Size of the seed in 32-bit words (68 bytes total).</summary>
|
||||
public const int SEED_SIZE = 17;
|
||||
|
||||
private readonly uint[] m_buffer = new uint[LFG_K];
|
||||
private int m_position_bytes = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the generator from a 17-element u32 seed array.
|
||||
/// Each seed word is treated as a raw LE u32 from the file (Dolphin: reinterpret_cast then swap32).
|
||||
///
|
||||
/// </summary>
|
||||
private const int LFG_J = 32;
|
||||
|
||||
/// <summary>
|
||||
/// Size of the LFG output buffer in bytes (LFG_K * 4 = 2084)
|
||||
/// </summary>
|
||||
public const int BUFFER_BYTES = LFG_K * 4;
|
||||
|
||||
/// <summary>
|
||||
/// Size of the seed in 32-bit words (68 bytes total)
|
||||
/// </summary>
|
||||
public const int SEED_SIZE = 17;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
private readonly uint[] _buffer = new uint[LFG_K];
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
private int _position = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the generator from a 17-element uint seed array.
|
||||
/// Each seed word is treated as a raw LE uint from the file (Dolphin: reinterpret_cast then swap32).
|
||||
/// </summary>
|
||||
public void SetSeed(uint[] seed)
|
||||
{
|
||||
if (seed == null || seed.Length < SEED_SIZE)
|
||||
throw new ArgumentException($"Seed must contain at least {SEED_SIZE} u32 values.", nameof(seed));
|
||||
throw new ArgumentException($"Seed must contain at least {SEED_SIZE} uint values.", nameof(seed));
|
||||
|
||||
m_position_bytes = 0;
|
||||
// Reinterpret LE bytes as BE (Dolphin swap32)
|
||||
_position = 0;
|
||||
for (int i = 0; i < SEED_SIZE; i++)
|
||||
m_buffer[i] = SwapU32(seed[i]); // reinterpret LE bytes as BE (Dolphin swap32)
|
||||
{
|
||||
_buffer[i] = SwapU32(seed[i]);
|
||||
}
|
||||
|
||||
Initialize(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the generator from a 68-byte seed (17 BE u32 values as in the RVZ file).
|
||||
/// Matches Dolphin: m_buffer[i] = Common::swap32(seed + i * 4).
|
||||
/// Initializes the generator from a 68-byte seed (17 BE uint values as in the RVZ file).
|
||||
/// </summary>
|
||||
/// <remarks>Matches Dolphin: m_buffer[i] = Common::swap32(seed + i * 4).</remarks>
|
||||
public void SetSeed(byte[] seedBytes)
|
||||
{
|
||||
if (seedBytes == null || seedBytes.Length < SEED_SIZE * 4)
|
||||
throw new ArgumentException($"Seed must be {SEED_SIZE * 4} bytes.", nameof(seedBytes));
|
||||
|
||||
m_position_bytes = 0;
|
||||
_position = 0;
|
||||
int offset = 0;
|
||||
for (int i = 0; i < SEED_SIZE; i++)
|
||||
m_buffer[i] = ReadBigEndianU32(seedBytes, i * 4);
|
||||
{
|
||||
_buffer[i] = seedBytes.ReadUInt32BigEndian(ref offset);
|
||||
}
|
||||
|
||||
Initialize(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Skips forward by <paramref name="count"/> bytes in the output stream.
|
||||
/// Matches Dolphin: LaggedFibonacciGenerator::Forward(size_t count).
|
||||
/// </summary>
|
||||
/// <remarks>Matches Dolphin: LaggedFibonacciGenerator::Forward(size_t count).</remarks>
|
||||
public void Forward(int count)
|
||||
{
|
||||
m_position_bytes += count;
|
||||
while (m_position_bytes >= BUFFER_BYTES)
|
||||
_position += count;
|
||||
while (_position >= BUFFER_BYTES)
|
||||
{
|
||||
ForwardStep();
|
||||
m_position_bytes -= BUFFER_BYTES;
|
||||
_position -= BUFFER_BYTES;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Generates <paramref name="count"/> junk bytes and returns them.</summary>
|
||||
/// <summary>
|
||||
/// Generates <paramref name="count"/> junk bytes and returns them.
|
||||
/// </summary>
|
||||
public byte[] GetBytes(int count)
|
||||
{
|
||||
byte[] output = new byte[count];
|
||||
@@ -78,20 +108,21 @@ namespace SabreTools.Wrappers
|
||||
/// Generates junk bytes into <paramref name="output"/> starting at <paramref name="outputOffset"/>.
|
||||
/// Matches Dolphin: LaggedFibonacciGenerator::GetBytes using memcpy pattern.
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
public void GetBytes(int count, byte[] output, int outputOffset)
|
||||
{
|
||||
while (count > 0)
|
||||
{
|
||||
int length = Math.Min(count, BUFFER_BYTES - m_position_bytes);
|
||||
Buffer.BlockCopy(m_buffer, m_position_bytes, output, outputOffset, length);
|
||||
m_position_bytes += length;
|
||||
int length = Math.Min(count, BUFFER_BYTES - _position);
|
||||
Buffer.BlockCopy(_buffer, _position, output, outputOffset, length);
|
||||
_position += length;
|
||||
count -= length;
|
||||
outputOffset += length;
|
||||
|
||||
if (m_position_bytes == BUFFER_BYTES)
|
||||
if (_position == BUFFER_BYTES)
|
||||
{
|
||||
ForwardStep();
|
||||
m_position_bytes = 0;
|
||||
_position = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,81 +131,98 @@ namespace SabreTools.Wrappers
|
||||
/// Returns a single junk byte at the current position, advancing by one byte.
|
||||
/// Matches Dolphin: LaggedFibonacciGenerator::GetByte.
|
||||
/// </summary>
|
||||
/// <remarks></remarks>
|
||||
internal byte GetByte()
|
||||
{
|
||||
int wordIdx = m_position_bytes / 4;
|
||||
int byteInWord = m_position_bytes % 4;
|
||||
byte result = (byte)(m_buffer[wordIdx] >> (byteInWord * 8)); // LE byte order
|
||||
int wordIdx = _position / 4;
|
||||
int byteInWord = _position % 4;
|
||||
byte result = (byte)(_buffer[wordIdx] >> (byteInWord * 8)); // LE byte order
|
||||
|
||||
m_position_bytes++;
|
||||
if (m_position_bytes == BUFFER_BYTES)
|
||||
_position++;
|
||||
if (_position == BUFFER_BYTES)
|
||||
{
|
||||
ForwardStep();
|
||||
m_position_bytes = 0;
|
||||
_position = 0;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Private forward/backward state steps
|
||||
// -------------------------------------------------------------------
|
||||
#region Private forward/backward state steps
|
||||
|
||||
/// <summary>
|
||||
/// Full buffer state step forward — Dolphin: Forward() (no args).
|
||||
/// Full buffer state step forward
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Dolphin: Forward() (no args).
|
||||
/// for i in [0,J): buf[i] ^= buf[i + K - J] (= buf[i + 489])
|
||||
/// for i in [J,K): buf[i] ^= buf[i - J] (= buf[i - 32])
|
||||
/// </summary>
|
||||
/// </remarks>
|
||||
private void ForwardStep()
|
||||
{
|
||||
for (int i = 0; i < LFG_J; i++)
|
||||
m_buffer[i] ^= m_buffer[i + LFG_K - LFG_J];
|
||||
{
|
||||
_buffer[i] ^= _buffer[i + LFG_K - LFG_J];
|
||||
}
|
||||
|
||||
for (int i = LFG_J; i < LFG_K; i++)
|
||||
m_buffer[i] ^= m_buffer[i - LFG_J];
|
||||
{
|
||||
_buffer[i] ^= _buffer[i - LFG_J];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Partial or full buffer state step backward — undoes ForwardStep.
|
||||
/// Dolphin: Backward(size_t start_word, size_t end_word).
|
||||
/// </summary>
|
||||
/// <remarks>Dolphin: Backward(size_t start_word, size_t end_word).</remarks>
|
||||
private void Backward(int startWord = 0, int endWord = LFG_K)
|
||||
{
|
||||
int loopEnd = Math.Max(LFG_J, startWord);
|
||||
|
||||
// Undo second loop of ForwardStep (reversed)
|
||||
for (int i = Math.Min(endWord, LFG_K); i > loopEnd; i--)
|
||||
m_buffer[i - 1] ^= m_buffer[i - 1 - LFG_J];
|
||||
{
|
||||
_buffer[i - 1] ^= _buffer[i - 1 - LFG_J];
|
||||
}
|
||||
|
||||
// Undo first loop of ForwardStep (reversed)
|
||||
for (int i = Math.Min(endWord, LFG_J); i > startWord; i--)
|
||||
m_buffer[i - 1] ^= m_buffer[i - 1 + LFG_K - LFG_J];
|
||||
{
|
||||
_buffer[i - 1] ^= _buffer[i - 1 + LFG_K - LFG_J];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recovers the original 17-word seed from the current buffer state and outputs it
|
||||
/// as LE u32 values into <paramref name="seedOut"/>.
|
||||
/// Dolphin: Reinitialize(u32 seed_out[]).
|
||||
/// as LE uint values into <paramref name="seedOut"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Dolphin: Reinitialize(uint seed_out[]).</remarks>
|
||||
private bool Reinitialize(uint[] seedOut)
|
||||
{
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
Backward();
|
||||
}
|
||||
|
||||
// Swap all words back to big-endian representation
|
||||
for (int i = 0; i < LFG_K; i++)
|
||||
m_buffer[i] = SwapU32(m_buffer[i]);
|
||||
{
|
||||
_buffer[i] = SwapU32(_buffer[i]);
|
||||
}
|
||||
|
||||
// Reconstruct bits 16-17 for the first SEED_SIZE words
|
||||
for (int i = 0; i < SEED_SIZE; i++)
|
||||
{
|
||||
m_buffer[i] = (m_buffer[i] & 0xFF00FFFF)
|
||||
| ((m_buffer[i] << 2) & 0x00FC0000)
|
||||
| (((m_buffer[i + 16] ^ m_buffer[i + 15]) << 9) & 0x00030000);
|
||||
_buffer[i] = (_buffer[i] & 0xFF00FFFF)
|
||||
| ((_buffer[i] << 2) & 0x00FC0000)
|
||||
| (((_buffer[i + 16] ^ _buffer[i + 15]) << 9) & 0x00030000);
|
||||
}
|
||||
|
||||
// Output seed as LE u32 values (swap32 converts BE→LE)
|
||||
// Output seed as LE uint values (swap32 converts BE->LE)
|
||||
for (int i = 0; i < SEED_SIZE; i++)
|
||||
seedOut[i] = SwapU32(m_buffer[i]);
|
||||
{
|
||||
seedOut[i] = SwapU32(_buffer[i]);
|
||||
}
|
||||
|
||||
return Initialize(true);
|
||||
}
|
||||
@@ -183,91 +231,98 @@ namespace SabreTools.Wrappers
|
||||
/// Fills m_buffer[SEED_SIZE..K-1] from the first SEED_SIZE words, applies the output
|
||||
/// transform, and runs 4× ForwardStep. When <paramref name="checkExisting"/> is true,
|
||||
/// verifies the data in m_buffer[SEED_SIZE..] matches the recurrence.
|
||||
/// Dolphin: Initialize(bool check_existing_data).
|
||||
/// </summary>
|
||||
/// <remarks>Dolphin: Initialize(bool check_existing_data).</remarks>
|
||||
private bool Initialize(bool checkExisting)
|
||||
{
|
||||
for (int i = SEED_SIZE; i < LFG_K; i++)
|
||||
{
|
||||
uint calculated = (m_buffer[i - 17] << 23)
|
||||
^ (m_buffer[i - 16] >> 9)
|
||||
^ m_buffer[i - 1];
|
||||
uint calculated = (_buffer[i - 17] << 23)
|
||||
^ (_buffer[i - 16] >> 9)
|
||||
^ _buffer[i - 1];
|
||||
|
||||
if (checkExisting)
|
||||
{
|
||||
uint actual = (m_buffer[i] & 0xFF00FFFF) | ((m_buffer[i] << 2) & 0x00FC0000);
|
||||
uint actual = (_buffer[i] & 0xFF00FFFF) | ((_buffer[i] << 2) & 0x00FC0000);
|
||||
if ((calculated & 0xFFFCFFFF) != actual)
|
||||
return false;
|
||||
}
|
||||
|
||||
m_buffer[i] = calculated;
|
||||
_buffer[i] = calculated;
|
||||
}
|
||||
|
||||
// Output transform: each word → swap32((x & 0xFF00FFFF) | ((x >> 2) & 0x00FF0000))
|
||||
// Output transform: each word -> swap32((x & 0xFF00FFFF) | ((x >> 2) & 0x00FF0000))
|
||||
for (int i = 0; i < LFG_K; i++)
|
||||
m_buffer[i] = SwapU32((m_buffer[i] & 0xFF00FFFF) | ((m_buffer[i] >> 2) & 0x00FF0000));
|
||||
{
|
||||
_buffer[i] = SwapU32((_buffer[i] & 0xFF00FFFF) | ((_buffer[i] >> 2) & 0x00FF0000));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
ForwardStep();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Static seed-recovery API (used by RvzPackDecompressor)
|
||||
// -------------------------------------------------------------------
|
||||
#endregion
|
||||
|
||||
#region Static seed-recovery API (used by RvzPackDecompressor)
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to recover a 17-word seed from disc data starting at
|
||||
/// <paramref name="dataStart"/> within <paramref name="data"/>.
|
||||
/// <paramref name="size"/> is the number of bytes to match (up to the next 32 KiB boundary).
|
||||
/// <paramref name="dataOffsetMod"/> is <c>discOffset % 0x8000</c> — the offset within
|
||||
/// <paramref name="dataOffsetMod"/> is discOffset % 0x8000 — the offset within
|
||||
/// the current LFG cycle.
|
||||
/// Returns the number of bytes that were successfully reconstructed (0 = not junk data).
|
||||
/// Matches Dolphin: LaggedFibonacciGenerator::GetSeed(u8*, size_t, size_t, u32[]).
|
||||
/// </summary>
|
||||
/// <returns>the number of bytes that were successfully reconstructed (0 = not junk data).</returns>
|
||||
/// <remarks>Matches Dolphin: LaggedFibonacciGenerator::GetSeed(u8*, size_t, size_t, uint[]).</remarks>
|
||||
public static int GetSeed(byte[] data, int dataStart, int size, int dataOffsetMod, uint[] seedOut)
|
||||
{
|
||||
if (size <= 0 || dataStart < 0 || dataStart + size > data.Length)
|
||||
return 0;
|
||||
|
||||
// Skip any bytes before the next u32-aligned boundary
|
||||
// Skip any bytes before the next uint-aligned boundary
|
||||
int bytesToSkip = (4 - (dataOffsetMod % 4)) % 4;
|
||||
if (bytesToSkip >= size)
|
||||
return 0;
|
||||
|
||||
int u32DataStart = dataStart + bytesToSkip;
|
||||
int u32Size = (size - bytesToSkip) / 4;
|
||||
int u32DataOffset = (dataOffsetMod + bytesToSkip) / 4;
|
||||
int uintDataStart = dataStart + bytesToSkip;
|
||||
int uintSize = (size - bytesToSkip) / 4;
|
||||
int uintDataOffset = (dataOffsetMod + bytesToSkip) / 4;
|
||||
|
||||
if (u32Size < LFG_K)
|
||||
if (uintSize < LFG_K)
|
||||
return 0;
|
||||
|
||||
// Read disc bytes as LE u32 values (Dolphin: reinterpret_cast<const u32*>)
|
||||
uint[] u32Data = new uint[u32Size];
|
||||
for (int i = 0; i < u32Size; i++)
|
||||
u32Data[i] = ReadLittleEndianU32(data, u32DataStart + (i * 4));
|
||||
// Read disc bytes as little-endian uint values (Dolphin: reinterpret_cast<const uint*>)
|
||||
uint[] uintData = new uint[uintSize];
|
||||
for (int i = 0; i < uintSize; i++)
|
||||
{
|
||||
uintData[i] = data.ReadUInt32LittleEndian(ref uintDataStart);
|
||||
}
|
||||
|
||||
LaggedFibonacciGenerator lfg = new LaggedFibonacciGenerator();
|
||||
if (!GetSeed_u32(u32Data, u32Size, u32DataOffset, lfg, seedOut))
|
||||
var lfg = new LaggedFibonacciGenerator();
|
||||
if (!GetSeed(uintData, uintSize, uintDataOffset, lfg, seedOut))
|
||||
return 0;
|
||||
|
||||
// Set position to data_offset % BUFFER_BYTES and count matching bytes from data[dataStart]
|
||||
lfg.m_position_bytes = dataOffsetMod % BUFFER_BYTES;
|
||||
lfg._position = dataOffsetMod % BUFFER_BYTES;
|
||||
|
||||
int reconstructed = 0;
|
||||
for (int i = 0; i < size && lfg.GetByte() == data[dataStart + i]; i++)
|
||||
{
|
||||
reconstructed++;
|
||||
}
|
||||
|
||||
return reconstructed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inner u32-level seed recovery.
|
||||
/// Dolphin: GetSeed(const u32* data, size_t size, size_t data_offset, LFG*, u32[]).
|
||||
/// Inner UInt32-level seed recovery.
|
||||
/// </summary>
|
||||
private static bool GetSeed_u32(uint[] data, int size, int dataOffset,
|
||||
LaggedFibonacciGenerator lfg, uint[] seedOut)
|
||||
/// <remarks>Dolphin: GetSeed(const uint* data, size_t size, size_t data_offset, LFG*, uint[]).</remarks>
|
||||
private static bool GetSeed(uint[] data, int size, int dataOffset, LaggedFibonacciGenerator lfg, uint[] seedOut)
|
||||
{
|
||||
if (size < LFG_K)
|
||||
return false;
|
||||
@@ -285,35 +340,38 @@ namespace SabreTools.Wrappers
|
||||
int dataOffsetDivK = dataOffset / LFG_K;
|
||||
|
||||
// Rotate data into buffer so buffer[dataOffsetModK] = data[0]
|
||||
Array.Copy(data, 0, lfg.m_buffer, dataOffsetModK, LFG_K - dataOffsetModK);
|
||||
Array.Copy(data, 0, lfg._buffer, dataOffsetModK, LFG_K - dataOffsetModK);
|
||||
if (dataOffsetModK > 0)
|
||||
Array.Copy(data, LFG_K - dataOffsetModK, lfg.m_buffer, 0, dataOffsetModK);
|
||||
Array.Copy(data, LFG_K - dataOffsetModK, lfg._buffer, 0, dataOffsetModK);
|
||||
|
||||
lfg.Backward(0, dataOffsetModK);
|
||||
|
||||
for (int i = 0; i < dataOffsetDivK; i++)
|
||||
{
|
||||
lfg.Backward();
|
||||
}
|
||||
|
||||
if (!lfg.Reinitialize(seedOut))
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < dataOffsetDivK; i++)
|
||||
{
|
||||
lfg.ForwardStep();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Endian helpers
|
||||
// -------------------------------------------------------------------
|
||||
#endregion
|
||||
|
||||
internal static uint ReadBigEndianU32(byte[] data, int offset) =>
|
||||
(uint)((data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]);
|
||||
#region Helpers
|
||||
|
||||
private static uint ReadLittleEndianU32(byte[] data, int offset) =>
|
||||
(uint)(data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24));
|
||||
/// <summary>
|
||||
/// Swap endinaness of a UInt32
|
||||
/// </summary>
|
||||
internal static uint SwapU32(uint value)
|
||||
=> (value << 24) | ((value << 8) & 0x00FF0000) | ((value >> 8) & 0x0000FF00) | (value >> 24);
|
||||
|
||||
internal static uint SwapU32(uint value) =>
|
||||
(value << 24) | ((value << 8) & 0x00FF0000) | ((value >> 8) & 0x0000FF00) | (value >> 24);
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SabreTools.Hashing;
|
||||
using SabreTools.Numerics.Extensions;
|
||||
|
||||
// TODO: Move to IO
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
/// <summary>
|
||||
@@ -20,6 +22,11 @@ namespace SabreTools.Wrappers
|
||||
/// </summary>
|
||||
internal static class PurgeCompressor
|
||||
{
|
||||
/// <summary>
|
||||
/// Zero-byte runs of this length or fewer are bridged
|
||||
/// </summary>
|
||||
private const int MaxGap = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Compress <paramref name="data"/>[<paramref name="offset"/> ..
|
||||
/// <paramref name="offset"/>+<paramref name="count"/>) into PURGE format.
|
||||
@@ -35,8 +42,6 @@ namespace SabreTools.Wrappers
|
||||
/// <returns>PURGE-compressed byte array (segments + 20-byte SHA-1).</returns>
|
||||
public static byte[] Compress(byte[] data, int offset, int count, byte[]? precedingBytes = null)
|
||||
{
|
||||
const int MaxGap = 8; // zero-byte runs of this length or fewer are bridged
|
||||
|
||||
var output = new MemoryStream((count / 2) + 32);
|
||||
|
||||
int end = offset + count;
|
||||
@@ -46,42 +51,46 @@ namespace SabreTools.Wrappers
|
||||
{
|
||||
// Skip leading zeros
|
||||
while (pos < end && data[pos] == 0)
|
||||
{
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (pos >= end)
|
||||
break;
|
||||
|
||||
// pos is now the start of a non-zero run (segment start)
|
||||
int segStart = pos;
|
||||
int segEnd = pos;
|
||||
int segEnd = pos;
|
||||
|
||||
// Extend the segment, bridging zero-gaps of <= MaxGap bytes
|
||||
while (segEnd < end)
|
||||
{
|
||||
// advance through non-zero bytes
|
||||
while (segEnd < end && data[segEnd] != 0)
|
||||
{
|
||||
segEnd++;
|
||||
}
|
||||
|
||||
// peek ahead: count zero bytes
|
||||
int zeroRun = 0;
|
||||
while (segEnd + zeroRun < end && data[segEnd + zeroRun] == 0)
|
||||
{
|
||||
zeroRun++;
|
||||
}
|
||||
|
||||
// If the gap is small enough (and there is more non-zero data after it),
|
||||
// bridge the gap by including it in the segment.
|
||||
if (zeroRun > 0 && zeroRun <= MaxGap && segEnd + zeroRun < end)
|
||||
{
|
||||
segEnd += zeroRun; // include zeros in segment, keep scanning
|
||||
}
|
||||
else
|
||||
{
|
||||
break; // end of segment
|
||||
}
|
||||
}
|
||||
|
||||
// Trim trailing zeros from segment end
|
||||
while (segEnd > segStart && data[segEnd - 1] == 0)
|
||||
{
|
||||
segEnd--;
|
||||
}
|
||||
|
||||
if (segEnd <= segStart)
|
||||
{
|
||||
@@ -90,11 +99,10 @@ namespace SabreTools.Wrappers
|
||||
}
|
||||
|
||||
uint segOffset = (uint)(segStart - offset);
|
||||
uint segSize = (uint)(segEnd - segStart);
|
||||
uint segSize = (uint)(segEnd - segStart);
|
||||
|
||||
// Write {u32 offsetBE, u32 sizeBE, data[segSize]}
|
||||
WriteBeU32(output, segOffset);
|
||||
WriteBeU32(output, segSize);
|
||||
output.WriteBigEndian(segOffset);
|
||||
output.WriteBigEndian(segSize);
|
||||
output.Write(data, segStart, (int)segSize);
|
||||
|
||||
pos = segEnd;
|
||||
@@ -112,24 +120,23 @@ namespace SabreTools.Wrappers
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the SHA-1 hash of preceeding bytes and segment data
|
||||
/// </summary>
|
||||
/// <param name="precedingBytes">Optional preceeding bytes</param>
|
||||
/// <param name="segments">Segment data</param>
|
||||
/// <returns>SHA-1 hash of the data, all 0x00 on error</returns>
|
||||
private static byte[] ComputeSha1(byte[]? precedingBytes, byte[] segments)
|
||||
{
|
||||
using var sha1 = new HashWrapper(HashType.SHA1);
|
||||
|
||||
if (precedingBytes != null && precedingBytes.Length > 0)
|
||||
if (precedingBytes is not null && precedingBytes.Length > 0)
|
||||
sha1.Process(precedingBytes, 0, precedingBytes.Length);
|
||||
|
||||
sha1.Process(segments, 0, segments.Length);
|
||||
sha1.Terminate();
|
||||
return sha1.CurrentHashBytes ?? new byte[20];
|
||||
}
|
||||
|
||||
private static void WriteBeU32(Stream s, uint value)
|
||||
{
|
||||
s.WriteByte((byte)(value >> 24));
|
||||
s.WriteByte((byte)(value >> 16));
|
||||
s.WriteByte((byte)(value >> 8));
|
||||
s.WriteByte((byte)value);
|
||||
return sha1.CurrentHashBytes ?? new byte[20];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using SabreTools.Hashing;
|
||||
using SabreTools.Numerics.Extensions;
|
||||
|
||||
// TODO: Move to IO
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
/// <summary>
|
||||
@@ -21,6 +22,7 @@ namespace SabreTools.Wrappers
|
||||
internal static class PurgeDecompressor
|
||||
{
|
||||
private const int SHA1_SIZE = 20;
|
||||
|
||||
private const int SEGMENT_HEADER_SIZE = 8; // u32 offset + u32 size
|
||||
|
||||
/// <summary>
|
||||
@@ -35,16 +37,18 @@ namespace SabreTools.Wrappers
|
||||
/// exception-list section for Wii partition groups. Pass null for non-Wii groups.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The decompressed byte array, or <c>null</c> if the data is malformed or the
|
||||
/// The decompressed byte array, or null if the data is malformed or the
|
||||
/// trailing SHA-1 does not match.
|
||||
/// </returns>
|
||||
public static byte[]? Decompress(
|
||||
byte[] input, int inputOffset, int inputLength,
|
||||
byte[] input,
|
||||
int inputOffset,
|
||||
int inputLength,
|
||||
int decompressedSize,
|
||||
byte[]? precedingBytes = null)
|
||||
{
|
||||
if (input is null) throw new ArgumentNullException(nameof(input));
|
||||
if (inputLength < SHA1_SIZE) return null;
|
||||
if (inputLength < SHA1_SIZE)
|
||||
return null;
|
||||
|
||||
byte[] output = new byte[decompressedSize];
|
||||
int pos = inputOffset;
|
||||
@@ -52,7 +56,7 @@ namespace SabreTools.Wrappers
|
||||
|
||||
using (var sha1 = new HashWrapper(HashType.SHA1))
|
||||
{
|
||||
if (precedingBytes != null && precedingBytes.Length > 0)
|
||||
if (precedingBytes is not null && precedingBytes.Length > 0)
|
||||
sha1.Process(precedingBytes, 0, precedingBytes.Length);
|
||||
|
||||
while (pos < dataEnd)
|
||||
@@ -62,7 +66,7 @@ namespace SabreTools.Wrappers
|
||||
|
||||
int headerPos = pos;
|
||||
uint segOffset = input.ReadUInt32BigEndian(ref headerPos);
|
||||
uint segSize = input.ReadUInt32BigEndian(ref headerPos);
|
||||
uint segSize = input.ReadUInt32BigEndian(ref headerPos);
|
||||
|
||||
sha1.Process(input, pos, SEGMENT_HEADER_SIZE);
|
||||
pos += SEGMENT_HEADER_SIZE;
|
||||
@@ -84,7 +88,9 @@ namespace SabreTools.Wrappers
|
||||
sha1.Terminate();
|
||||
|
||||
byte[]? computed = sha1.CurrentHashBytes;
|
||||
if (computed is null) return null;
|
||||
if (computed is null)
|
||||
return null;
|
||||
|
||||
for (int i = 0; i < SHA1_SIZE; i++)
|
||||
{
|
||||
if (computed[i] != input[dataEnd + i])
|
||||
143
SabreTools.Wrappers/Helpers/RvzPackDecompressor.cs
Normal file
143
SabreTools.Wrappers/Helpers/RvzPackDecompressor.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
using System;
|
||||
using SabreTools.Numerics.Extensions;
|
||||
|
||||
// TODO: Move to IO
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
/// <summary>
|
||||
/// Decompressor for RVZ packed format.
|
||||
/// RVZ uses run-length encoding to store real data and junk data efficiently:
|
||||
/// - Real data: size (4 bytes) + data bytes
|
||||
/// - Junk data: size with high bit set (4 bytes) + 68-byte seed -> regenerate using LFG
|
||||
/// </summary>
|
||||
internal class RvzPackDecompressor
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
private readonly byte[] _packedData;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
private readonly uint _rvzPackedSize;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
private long _dataOffset;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
private readonly LaggedFibonacciGenerator _lfg;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
private int _inPosition = 0;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
private uint _currentSize = 0;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
private bool _currentIsJunk = false;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new RVZ pack decompressor.
|
||||
/// </summary>
|
||||
/// <param name="packedData">The packed RVZ data</param>
|
||||
/// <param name="rvzPackedSize">Expected size of packed data (for validation)</param>
|
||||
/// <param name="dataOffset">Offset in the virtual disc (for LFG alignment)</param>
|
||||
public RvzPackDecompressor(byte[] packedData, uint rvzPackedSize, long dataOffset)
|
||||
{
|
||||
_packedData = packedData;
|
||||
_rvzPackedSize = rvzPackedSize;
|
||||
_dataOffset = dataOffset;
|
||||
_lfg = new LaggedFibonacciGenerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses the packed data into the output buffer.
|
||||
/// </summary>
|
||||
/// <param name="output">Destination buffer</param>
|
||||
/// <param name="outputOffset">Offset in destination buffer</param>
|
||||
/// <param name="count">Number of bytes to decompress</param>
|
||||
/// <returns>Number of bytes actually decompressed</returns>
|
||||
public int Decompress(byte[] output, int outputOffset, int count)
|
||||
{
|
||||
int totalWritten = 0;
|
||||
|
||||
while (totalWritten < count && !IsDone())
|
||||
{
|
||||
if (_currentSize == 0)
|
||||
{
|
||||
if (!ReadNextSegment())
|
||||
break;
|
||||
}
|
||||
|
||||
int bytesToWrite = Math.Min((int)_currentSize, count - totalWritten);
|
||||
if (_currentIsJunk)
|
||||
{
|
||||
_lfg.GetBytes(bytesToWrite, output, outputOffset + totalWritten);
|
||||
}
|
||||
else
|
||||
{
|
||||
Array.Copy(_packedData, _inPosition, output, outputOffset + totalWritten, bytesToWrite);
|
||||
_inPosition += bytesToWrite;
|
||||
}
|
||||
|
||||
_currentSize -= (uint)bytesToWrite;
|
||||
totalWritten += bytesToWrite;
|
||||
_dataOffset += bytesToWrite;
|
||||
}
|
||||
|
||||
return totalWritten;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if decompression is complete.
|
||||
/// </summary>
|
||||
public bool IsDone() => _currentSize == 0 && _inPosition >= _rvzPackedSize;
|
||||
|
||||
/// <summary>
|
||||
/// Read the next segment of packed data
|
||||
/// </summary>
|
||||
/// <returns>True if the segment was read and cached, false otherwise</returns>
|
||||
private bool ReadNextSegment()
|
||||
{
|
||||
if (_inPosition + 4 > _packedData.Length)
|
||||
return false;
|
||||
|
||||
// Size field is big-endian u32; high bit signals junk data
|
||||
uint sizeField = _packedData.ReadUInt32BigEndian(ref _inPosition);
|
||||
|
||||
_currentIsJunk = (sizeField & 0x80000000) != 0;
|
||||
_currentSize = sizeField & 0x7FFFFFFF;
|
||||
|
||||
if (_currentIsJunk)
|
||||
{
|
||||
if (_inPosition + (LaggedFibonacciGenerator.SEED_SIZE * 4) > _packedData.Length)
|
||||
return false;
|
||||
|
||||
byte[] seed = new byte[LaggedFibonacciGenerator.SEED_SIZE * 4];
|
||||
Array.Copy(_packedData, _inPosition, seed, 0, seed.Length);
|
||||
_inPosition += seed.Length;
|
||||
|
||||
_lfg.SetSeed(seed);
|
||||
|
||||
// Advance LFG to the correct position within the buffer.
|
||||
// Dolphin: lfg.m_position_bytes = data_offset % (LFG_K * sizeof(u32))
|
||||
int offsetInBuffer = (int)(_dataOffset % LaggedFibonacciGenerator.BUFFER_BYTES);
|
||||
if (offsetInBuffer > 0)
|
||||
_lfg.Forward(offsetInBuffer);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SabreTools.Numerics.Extensions;
|
||||
|
||||
// TODO: Move to IO
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
/// <summary>
|
||||
@@ -10,7 +11,7 @@ namespace SabreTools.Wrappers
|
||||
/// (Lagged Fibonacci Generator) junk regions with compact seed descriptors.
|
||||
///
|
||||
/// This is the exact inverse of <see cref="RvzPackDecompressor"/> and mirrors
|
||||
/// Dolphin's <c>RVZPack()</c> in <c>WIABlob.cpp</c>.
|
||||
/// Dolphin's RVZPack() in WIABlob.cpp.
|
||||
///
|
||||
/// Two-phase algorithm:
|
||||
/// <list type="number">
|
||||
@@ -22,34 +23,51 @@ namespace SabreTools.Wrappers
|
||||
/// </summary>
|
||||
internal static class RvzPackEncoder
|
||||
{
|
||||
// 17 u32s × 4 bytes = 68 bytes — minimum size to record a seed
|
||||
/// <remarks>
|
||||
/// 17 u32s × 4 bytes = 68 bytes — minimum size to record a seed
|
||||
/// </remarks>
|
||||
private const int SeedSizeBytes = LaggedFibonacciGenerator.SEED_SIZE * 4;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
private sealed class JunkRegion
|
||||
{
|
||||
public long StartOffset;
|
||||
public long StartOffset;
|
||||
|
||||
public uint[]? Seed;
|
||||
}
|
||||
|
||||
/// <summary>Result of packing a single chunk: compressed payload and its logical size.</summary>
|
||||
/// <summary>
|
||||
/// Result of packing a single chunk: compressed payload and its logical size.
|
||||
/// </summary>
|
||||
internal struct ChunkResult
|
||||
{
|
||||
/// <summary>Packed payload, or <c>null</c> if the chunk contains no junk.</summary>
|
||||
/// <summary>
|
||||
/// Packed payload, or null if the chunk contains no junk.
|
||||
/// </summary>
|
||||
public byte[]? Packed;
|
||||
/// <summary>Number of bytes the decompressor needs to consume from <see cref="Packed"/>.</summary>
|
||||
|
||||
/// <summary>
|
||||
/// Number of bytes the decompressor needs to consume from <see cref="Packed"/>.
|
||||
/// </summary>
|
||||
public uint RvzPackedSize;
|
||||
}
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// RVZ-pack a single chunk.
|
||||
/// Returns <c>null</c> if the chunk contains no junk (write raw instead).
|
||||
/// Returns null if the chunk contains no junk (write raw instead).
|
||||
/// <paramref name="rvzPackedSize"/> is the number of bytes actually needed
|
||||
/// by the decompressor (may be < packed.Length due to alignment).
|
||||
/// </summary>
|
||||
public static byte[]? Pack(byte[] data, int dataOffset, int size,
|
||||
long discDataOffset, out uint rvzPackedSize, GcFst? fst = null)
|
||||
/// <param name="data">Source buffer</param>
|
||||
/// <param name="dataOffset">Start of data within <paramref name="data"/></param>
|
||||
public static byte[]? Pack(byte[] data,
|
||||
int dataOffset,
|
||||
int size,
|
||||
long discDataOffset,
|
||||
out uint rvzPackedSize,
|
||||
FileSystemTableReader? fst = null)
|
||||
{
|
||||
rvzPackedSize = 0;
|
||||
if (size <= 0)
|
||||
@@ -59,9 +77,9 @@ namespace SabreTools.Wrappers
|
||||
if (junkInfo.Count == 0)
|
||||
return null;
|
||||
|
||||
ChunkResult r = EmitChunk(data, dataOffset, 0L, size, size, junkInfo);
|
||||
rvzPackedSize = r.RvzPackedSize;
|
||||
return r.Packed;
|
||||
ChunkResult result = EmitChunk(data, dataOffset, 0L, size, junkInfo);
|
||||
rvzPackedSize = result.RvzPackedSize;
|
||||
return result.Packed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -69,101 +87,114 @@ namespace SabreTools.Wrappers
|
||||
/// Performs one Phase-1 scan over the entire buffer, then calls
|
||||
/// <see cref="EmitChunk"/> per chunk.
|
||||
/// </summary>
|
||||
/// <param name="data">Source buffer.</param>
|
||||
/// <param name="dataOffset">Start of data within <paramref name="data"/>.</param>
|
||||
/// <param name="totalSize">Total number of bytes to process.</param>
|
||||
/// <param name="bytesPerChunk">Size of each individual chunk.</param>
|
||||
/// <param name="numChunks">Number of chunks.</param>
|
||||
/// <param name="discDataOffset">Disc-partition byte offset of the first byte.</param>
|
||||
/// <param name="fst">Optional FST for file-boundary optimisation.</param>
|
||||
/// <param name="data">Source buffer</param>
|
||||
/// <param name="dataOffset">Start of data within <paramref name="data"/></param>
|
||||
/// <param name="totalSize">Total number of bytes to process</param>
|
||||
/// <param name="bytesPerChunk">Size of each individual chunk</param>
|
||||
/// <param name="numChunks">Number of chunks</param>
|
||||
/// <param name="discDataOffset">Disc-partition byte offset of the first byte</param>
|
||||
/// <param name="fst">Optional FST for file-boundary optimisation</param>
|
||||
/// <returns>
|
||||
/// One <see cref="ChunkResult"/> per chunk;
|
||||
/// <c>Packed == null</c> means the chunk has no junk and should be written raw.
|
||||
/// Packed == null means the chunk has no junk and should be written raw.
|
||||
/// </returns>
|
||||
public static ChunkResult[] PackGroup(
|
||||
byte[] data, int dataOffset, int totalSize,
|
||||
int bytesPerChunk, int numChunks,
|
||||
long discDataOffset, GcFst? fst = null)
|
||||
public static ChunkResult[] PackGroup(byte[] data,
|
||||
int dataOffset,
|
||||
int totalSize,
|
||||
int bytesPerChunk,
|
||||
int numChunks,
|
||||
long discDataOffset,
|
||||
FileSystemTableReader? fst = null)
|
||||
{
|
||||
var junkInfo = ScanForJunk(data, dataOffset, totalSize, discDataOffset, fst);
|
||||
|
||||
var result = new ChunkResult[numChunks];
|
||||
for (int c = 0; c < numChunks; c++)
|
||||
for (int i = 0; i < numChunks; i++)
|
||||
{
|
||||
long chunkStart = (long)c * bytesPerChunk;
|
||||
long chunkEnd = Math.Min(chunkStart + bytesPerChunk, totalSize);
|
||||
result[c] = EmitChunk(data, dataOffset, chunkStart, chunkEnd, totalSize, junkInfo);
|
||||
long chunkStart = (long)i * bytesPerChunk;
|
||||
long chunkEnd = Math.Min(chunkStart + bytesPerChunk, totalSize);
|
||||
|
||||
result[i] = EmitChunk(data, dataOffset, chunkStart, chunkEnd, junkInfo);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Phase 1 — scan buffer for junk regions
|
||||
|
||||
/// <summary>
|
||||
/// Scan buffer for junk regions
|
||||
/// </summary>
|
||||
/// <param name="data">Source buffer</param>
|
||||
/// <param name="dataOffset">Start of data within <paramref name="data"/></param>
|
||||
/// <param name="totalSize">Total number of bytes to process</param>
|
||||
/// <param name="discDataOffset">Disc-partition byte offset of the first byte</param>
|
||||
/// <param name="fst">Optional FST for file-boundary optimisation</param>
|
||||
/// <returns></returns>
|
||||
private static SortedDictionary<long, JunkRegion> ScanForJunk(
|
||||
byte[] data, int dataOffset, int totalSize, long discDataOffset, GcFst? fst)
|
||||
byte[] data,
|
||||
int dataOffset,
|
||||
int totalSize,
|
||||
long discDataOffset,
|
||||
FileSystemTableReader? fst)
|
||||
{
|
||||
var junkInfo = new SortedDictionary<long, JunkRegion>();
|
||||
|
||||
long position = 0;
|
||||
long dataOff = discDataOffset;
|
||||
long dataOff = discDataOffset;
|
||||
|
||||
while (position < totalSize)
|
||||
{
|
||||
// Step 1: count and advance past leading zeros
|
||||
long zeroes = 0;
|
||||
while ((position + zeroes) < totalSize &&
|
||||
data[dataOffset + position + zeroes] == 0)
|
||||
while ((position + zeroes) < totalSize && data[dataOffset + position + zeroes] == 0)
|
||||
{
|
||||
zeroes++;
|
||||
}
|
||||
|
||||
if (zeroes > SeedSizeBytes)
|
||||
{
|
||||
junkInfo[position + zeroes] = new JunkRegion
|
||||
{
|
||||
StartOffset = position,
|
||||
Seed = new uint[LaggedFibonacciGenerator.SEED_SIZE]
|
||||
Seed = new uint[LaggedFibonacciGenerator.SEED_SIZE]
|
||||
};
|
||||
}
|
||||
|
||||
position += zeroes;
|
||||
dataOff += zeroes;
|
||||
dataOff += zeroes;
|
||||
|
||||
if (position >= totalSize)
|
||||
break;
|
||||
|
||||
// Step 2: compute aligned read window (next 0x8000 boundary)
|
||||
long nextBoundary = AlignUp(dataOff + 1, 0x8000);
|
||||
long bytesToRead = Math.Min(nextBoundary - dataOff, totalSize - position);
|
||||
int dataOffMod = (int)(dataOff % 0x8000);
|
||||
long bytesToRead = Math.Min(nextBoundary - dataOff, totalSize - position);
|
||||
int dataOffMod = (int)(dataOff % 0x8000);
|
||||
|
||||
// Step 3: ALWAYS call GetSeed unconditionally — no FST pre-check
|
||||
var seed = new uint[LaggedFibonacciGenerator.SEED_SIZE];
|
||||
int reconstructed = LaggedFibonacciGenerator.GetSeed(
|
||||
data, (int)(dataOffset + position), (int)bytesToRead, dataOffMod, seed);
|
||||
var seed = new uint[LaggedFibonacciGenerator.SEED_SIZE];
|
||||
int reconstructed = LaggedFibonacciGenerator.GetSeed(data, (int)(dataOffset + position), (int)bytesToRead, dataOffMod, seed);
|
||||
|
||||
if (reconstructed > 0)
|
||||
{
|
||||
junkInfo[position + reconstructed] = new JunkRegion
|
||||
{
|
||||
StartOffset = position,
|
||||
Seed = seed
|
||||
Seed = seed
|
||||
};
|
||||
}
|
||||
|
||||
// Step 4: FST skip AFTER GetSeed
|
||||
if (fst != null)
|
||||
if (fst is not null)
|
||||
{
|
||||
long queryOff = dataOff + reconstructed;
|
||||
GcFst.FileEntry? fileInfo = fst.FindFileInfo(queryOff);
|
||||
if (fileInfo.HasValue)
|
||||
FileSystemTableReader.FileEntry? fileInfo = fst.FindFileInfo(queryOff);
|
||||
if (fileInfo is not null)
|
||||
{
|
||||
long fileEnd = fileInfo.Value.FileEnd;
|
||||
if (fileEnd < (dataOff + bytesToRead))
|
||||
{
|
||||
position += fileEnd - dataOff;
|
||||
dataOff = fileEnd;
|
||||
dataOff = fileEnd;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -171,33 +202,40 @@ namespace SabreTools.Wrappers
|
||||
|
||||
// Step 5: normal advance by block window
|
||||
position += bytesToRead;
|
||||
dataOff += bytesToRead;
|
||||
dataOff += bytesToRead;
|
||||
}
|
||||
|
||||
return junkInfo;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Phase 2 — emit packed segments for a single chunk
|
||||
|
||||
/// <summary>
|
||||
/// Emit packed segments for a single chunk
|
||||
/// </summary>
|
||||
/// <param name="data">Source buffer</param>
|
||||
/// <param name="dataOffset">Start of data within <paramref name="data"/></param>
|
||||
/// <param name="chunkStart"></param>
|
||||
/// <param name="chunkEnd"></param>
|
||||
/// <param name="junkInfo"></param>
|
||||
/// <returns></returns>
|
||||
private static ChunkResult EmitChunk(
|
||||
byte[] data, int dataOffset,
|
||||
long chunkStart, long chunkEnd, long totalSize,
|
||||
byte[] data,
|
||||
int dataOffset,
|
||||
long chunkStart,
|
||||
long chunkEnd,
|
||||
SortedDictionary<long, JunkRegion> junkInfo)
|
||||
{
|
||||
long currentOffset = chunkStart;
|
||||
long currentOffset = chunkStart;
|
||||
bool firstIteration = true;
|
||||
|
||||
var output = new MemoryStream((int)(chunkEnd - chunkStart));
|
||||
var output = new MemoryStream((int)(chunkEnd - chunkStart));
|
||||
uint packedSize = 0;
|
||||
|
||||
while (currentOffset < chunkEnd)
|
||||
{
|
||||
long remaining = chunkEnd - currentOffset;
|
||||
long nextJunkStart = chunkEnd;
|
||||
long nextJunkEnd = chunkEnd;
|
||||
uint[]? junkSeed = null;
|
||||
long remaining = chunkEnd - currentOffset;
|
||||
long nextJunkStart = chunkEnd;
|
||||
long nextJunkEnd = chunkEnd;
|
||||
uint[]? junkSeed = null;
|
||||
|
||||
if (remaining > SeedSizeBytes)
|
||||
{
|
||||
@@ -206,12 +244,11 @@ namespace SabreTools.Wrappers
|
||||
// Dolphin Phase-2 condition:
|
||||
// key > currentOffset + SEED_SIZE_BYTES AND
|
||||
// startOffset + SEED_SIZE_BYTES < chunkEnd
|
||||
if ((kvp.Key > (currentOffset + SeedSizeBytes)) &&
|
||||
((kvp.Value.StartOffset + SeedSizeBytes) < chunkEnd))
|
||||
if ((kvp.Key > (currentOffset + SeedSizeBytes)) && ((kvp.Value.StartOffset + SeedSizeBytes) < chunkEnd))
|
||||
{
|
||||
nextJunkStart = Math.Max(currentOffset, kvp.Value.StartOffset);
|
||||
nextJunkEnd = Math.Min(chunkEnd, kvp.Key);
|
||||
junkSeed = kvp.Value.Seed;
|
||||
nextJunkEnd = Math.Min(chunkEnd, kvp.Key);
|
||||
junkSeed = kvp.Value.Seed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -232,19 +269,21 @@ namespace SabreTools.Wrappers
|
||||
{
|
||||
output.WriteBigEndian((uint)nonJunkBytes);
|
||||
output.Write(data, (int)(dataOffset + currentOffset), (int)nonJunkBytes);
|
||||
packedSize += 4 + (uint)nonJunkBytes;
|
||||
packedSize += 4 + (uint)nonJunkBytes;
|
||||
currentOffset += nonJunkBytes;
|
||||
}
|
||||
|
||||
// Emit junk-seed segment
|
||||
long junkBytes = nextJunkEnd - currentOffset;
|
||||
if (junkBytes > 0 && junkSeed != null)
|
||||
if (junkBytes > 0 && junkSeed is not null)
|
||||
{
|
||||
output.WriteBigEndian(0x80000000u | (uint)junkBytes);
|
||||
|
||||
byte[] seedBytes = new byte[SeedSizeBytes];
|
||||
Buffer.BlockCopy(junkSeed, 0, seedBytes, 0, SeedSizeBytes);
|
||||
output.Write(seedBytes, 0, SeedSizeBytes);
|
||||
packedSize += 4 + (uint)SeedSizeBytes;
|
||||
|
||||
packedSize += 4 + (uint)SeedSizeBytes;
|
||||
currentOffset += junkBytes;
|
||||
}
|
||||
|
||||
@@ -255,10 +294,12 @@ namespace SabreTools.Wrappers
|
||||
return new ChunkResult { Packed = output.ToArray(), RvzPackedSize = packedSize };
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Align a value to a boundary
|
||||
/// </summary>
|
||||
/// TODO: Figure out how to use buffer alignment helpers here
|
||||
private static long AlignUp(long value, long alignment) => (value + alignment - 1) & ~(alignment - 1);
|
||||
|
||||
#endregion
|
||||
@@ -9,6 +9,7 @@ using SharpCompress.Compressors.ZStandard;
|
||||
#endif
|
||||
using SabreTools.Data.Models.WIA;
|
||||
|
||||
// TODO: Move to IO
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
/// <summary>
|
||||
@@ -17,10 +18,12 @@ namespace SabreTools.Wrappers
|
||||
/// </summary>
|
||||
internal static class WiaRvzCompressionHelper
|
||||
{
|
||||
// Dictionary sizes per compression level 1–9 (index 0 unused).
|
||||
// Mirrors Dolphin WIACompression.cpp dict_size choices.
|
||||
/// <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
|
||||
@@ -31,29 +34,43 @@ namespace SabreTools.Wrappers
|
||||
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))];
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="level"></param>
|
||||
/// <returns></returns>
|
||||
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.
|
||||
/// <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 <c>WiaHeader2.CompressorData</c> /
|
||||
/// <c>WiaHeader2.CompressorDataSize</c>.
|
||||
/// 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)
|
||||
internal static void GetCompressorData(WiaRvzCompressionType type, int level, out byte[] propData, out byte propSize)
|
||||
{
|
||||
propData = new byte[7];
|
||||
int dictSize = GetDictSize(level);
|
||||
@@ -74,15 +91,27 @@ namespace SabreTools.Wrappers
|
||||
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)
|
||||
/// <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)
|
||||
@@ -95,6 +124,10 @@ namespace SabreTools.Wrappers
|
||||
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));
|
||||
}
|
||||
@@ -103,29 +136,38 @@ namespace SabreTools.Wrappers
|
||||
#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)
|
||||
/// <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);
|
||||
}
|
||||
byte[] lzmaProps = new byte[compressorDataSize];
|
||||
Array.Copy(compressorData, lzmaProps, compressorDataSize);
|
||||
return DecompressLzma(data, offset, length, lzmaProps, isLzma2: false);
|
||||
|
||||
case WiaRvzCompressionType.LZMA2:
|
||||
{
|
||||
byte[] props = new byte[compressorDataSize];
|
||||
Array.Copy(compressorData, props, compressorDataSize);
|
||||
return DecompressLzma(data, offset, length, props, isLzma2: true);
|
||||
}
|
||||
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));
|
||||
}
|
||||
@@ -135,7 +177,13 @@ namespace SabreTools.Wrappers
|
||||
}
|
||||
|
||||
#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();
|
||||
@@ -143,18 +191,37 @@ namespace SabreTools.Wrappers
|
||||
{
|
||||
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);
|
||||
@@ -163,37 +230,66 @@ namespace SabreTools.Wrappers
|
||||
{
|
||||
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))
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ namespace SabreTools.Wrappers
|
||||
internal sealed class WiaVirtualStream : Stream
|
||||
{
|
||||
private readonly WIA _wia;
|
||||
|
||||
private long _position;
|
||||
|
||||
public WiaVirtualStream(WIA wia)
|
||||
@@ -17,10 +18,19 @@ namespace SabreTools.Wrappers
|
||||
_wia = wia ?? throw new ArgumentNullException(nameof(wia));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanRead => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanSeek => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanWrite => false;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override long Length => (long)_wia.IsoFileSize;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override long Position
|
||||
{
|
||||
get => _position;
|
||||
@@ -28,10 +38,12 @@ namespace SabreTools.Wrappers
|
||||
{
|
||||
if (value < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
|
||||
_position = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (buffer is null)
|
||||
@@ -53,14 +65,15 @@ namespace SabreTools.Wrappers
|
||||
return read;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
long newPos;
|
||||
switch (origin)
|
||||
{
|
||||
case SeekOrigin.Begin: newPos = offset; break;
|
||||
case SeekOrigin.Begin: newPos = offset; break;
|
||||
case SeekOrigin.Current: newPos = _position + offset; break;
|
||||
case SeekOrigin.End: newPos = Length + offset; break;
|
||||
case SeekOrigin.End: newPos = Length + offset; break;
|
||||
default: throw new ArgumentOutOfRangeException(nameof(origin));
|
||||
}
|
||||
|
||||
@@ -71,10 +84,13 @@ namespace SabreTools.Wrappers
|
||||
return _position;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Flush() { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,62 @@
|
||||
using System;
|
||||
using SabreTools.Data.Models.NintendoDisc;
|
||||
using SabreTools.Hashing;
|
||||
|
||||
// TODO: Move to IO
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
public partial class NintendoDisc
|
||||
{
|
||||
#region Wii Encryption / Decryption
|
||||
/// <summary>
|
||||
/// Retail common key
|
||||
/// </summary>
|
||||
public static byte[] KoreanCommonKey = [];
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a Wii common key by its ticket index (0 = retail, 1 = Korean).
|
||||
/// Must be set by the caller before invoking <see cref="DecryptTitleKey"/>.
|
||||
/// If <see langword="null"/>, or the delegate returns <see langword="null"/> for a given
|
||||
/// index, decryption will return <see langword="null"/>.
|
||||
/// Korean common key SHA-256 hash
|
||||
/// </summary>
|
||||
public static System.Func<byte, byte[]?>? CommonKeyProvider { get; set; }
|
||||
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";
|
||||
|
||||
/// <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="commonKeyIndex">
|
||||
/// Common key index from ticket offset 0x1F1: 0 = retail, 1 = Korean
|
||||
/// </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, byte commonKeyIndex)
|
||||
public static byte[]? DecryptTitleKey(byte[]? encryptedTitleKey, byte[]? titleId, int commonKeyIndex)
|
||||
{
|
||||
if (encryptedTitleKey is null || encryptedTitleKey.Length != 16)
|
||||
return null;
|
||||
if (titleId is null || titleId.Length != 8)
|
||||
return null;
|
||||
|
||||
byte[]? commonKey = CommonKeyProvider?.Invoke(commonKeyIndex);
|
||||
if (commonKey is null || commonKey.Length != 16)
|
||||
byte[] commonKey;
|
||||
if (commonKeyIndex == 0)
|
||||
commonKey = RetailCommonKey;
|
||||
else if (commonKeyIndex == 1)
|
||||
commonKey = KoreanCommonKey;
|
||||
else
|
||||
return null;
|
||||
|
||||
if (commonKey.Length != 16)
|
||||
return null;
|
||||
|
||||
// IV is the 8-byte title ID padded with zeros to 16 bytes
|
||||
byte[] iv = new byte[16];
|
||||
System.Array.Copy(titleId, 0, iv, 0, 8);
|
||||
Array.Copy(titleId, 0, iv, 0, 8);
|
||||
|
||||
return DecryptAesCbc(encryptedTitleKey, commonKey, iv);
|
||||
return AesCbc.Decrypt(encryptedTitleKey, commonKey, iv);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -57,14 +75,39 @@ namespace SabreTools.Wrappers
|
||||
if (iv is null || iv.Length != 16)
|
||||
return null;
|
||||
|
||||
return DecryptAesCbc(encryptedData, titleKey, iv);
|
||||
return AesCbc.Decrypt(encryptedData, titleKey, iv);
|
||||
}
|
||||
|
||||
private static byte[]? DecryptAesCbc(byte[] data, byte[] key, byte[] iv)
|
||||
/// <summary>
|
||||
/// Validate the Korean common key based on hash and length
|
||||
/// </summary>
|
||||
/// <param name="commonKey">Korean common key to validate</param>
|
||||
/// <returns>True if the key was valid, false otherwise</returns>
|
||||
public static bool ValidateKoreanCommonKey(byte[]? commonKey)
|
||||
{
|
||||
return AesCbc.Decrypt(data, key, iv);
|
||||
// Ignore invalid values
|
||||
if (commonKey is null || commonKey.Length != 16)
|
||||
return false;
|
||||
|
||||
// Hash the key and compare
|
||||
string? actualHash = HashTool.GetByteArrayHash(commonKey, HashType.SHA256);
|
||||
return string.Equals(actualHash, KoreanCommonKeySHA256, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
#endregion
|
||||
/// <summary>
|
||||
/// Validate the retail common key based on hash and length
|
||||
/// </summary>
|
||||
/// <param name="commonKey">Retail common key to validate</param>
|
||||
/// <returns>True if the key was valid, false otherwise</returns>
|
||||
public static bool ValidateRetailCommonKey(byte[]? commonKey)
|
||||
{
|
||||
// Ignore invalid values
|
||||
if (commonKey is null || commonKey.Length != 16)
|
||||
return false;
|
||||
|
||||
// Hash the key and compare
|
||||
string? actualHash = HashTool.GetByteArrayHash(commonKey, HashType.SHA256);
|
||||
return string.Equals(actualHash, RetailCommonKeySHA256, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using SabreTools.Data.Models.NintendoDisc;
|
||||
using SabreTools.Numerics.Extensions;
|
||||
using SabreTools.Text.Extensions;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
@@ -15,11 +18,9 @@ namespace SabreTools.Wrappers
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(outputDirectory);
|
||||
|
||||
if (Model.Platform == Platform.GameCube)
|
||||
if (Platform == Platform.GameCube)
|
||||
return ExtractGameCube(outputDirectory);
|
||||
else if (Model.Platform == Platform.Wii)
|
||||
else if (Platform == Platform.Wii)
|
||||
return ExtractWii(outputDirectory);
|
||||
|
||||
return false;
|
||||
@@ -28,52 +29,72 @@ namespace SabreTools.Wrappers
|
||||
{
|
||||
if (includeDebug)
|
||||
Console.Error.WriteLine(ex);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#region Pre-decrypted reader override
|
||||
|
||||
/// <summary>
|
||||
/// When set, <see cref="ReadDecryptedPartitionRange"/> calls this delegate instead of
|
||||
/// performing AES-CBC decryption. Used by WIA/RVZ extraction, where partition data is
|
||||
/// already stored decrypted and the encrypt-then-decrypt round-trip is unnecessary.
|
||||
/// Signature: (absDataOffset, partitionDataOffset, length) -> decrypted bytes or null.
|
||||
/// </summary>
|
||||
internal Func<long, long, int, byte[]?>? _preDecryptedReader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region GameCube extraction
|
||||
|
||||
private bool ExtractGameCube(string dest)
|
||||
/// <summary>
|
||||
/// Extract GameCube data from a disc
|
||||
/// </summary>
|
||||
/// <param name="outputDirectory">Output directory to write to</param>
|
||||
/// <returns>True if the extraction was successful, false otherwise</returns>
|
||||
private bool ExtractGameCube(string outputDirectory)
|
||||
{
|
||||
string sysDir = Path.Combine(dest, "sys");
|
||||
string sysDir = Path.Combine(outputDirectory, "sys");
|
||||
Directory.CreateDirectory(sysDir);
|
||||
|
||||
// sys/boot.bin (disc header, 0x000 – 0x43F)
|
||||
// sys/boot.bin (disc header, 0x000 - 0x43F)
|
||||
WriteRange(0, Constants.DiscHeaderSize, Path.Combine(sysDir, "boot.bin"));
|
||||
|
||||
// sys/bi2.bin (0x440 – 0x243F)
|
||||
// sys/bi2.bin (0x440 - 0x243F)
|
||||
WriteRange(Constants.Bi2Address, Constants.Bi2Size, Path.Combine(sysDir, "bi2.bin"));
|
||||
|
||||
// sys/apploader.img
|
||||
WriteApploader(sysDir);
|
||||
WriteGCApploader(sysDir);
|
||||
|
||||
// DOL offset stored without shift on GameCube
|
||||
long dolOffset = Model.Header.DolOffset;
|
||||
long dolOffset = Header.DolOffset;
|
||||
if (dolOffset > 0)
|
||||
{
|
||||
byte[]? dolHeader = ReadDisc(dolOffset, 0xE0);
|
||||
if (dolHeader != null)
|
||||
byte[] dolHeaderBytes = ReadRangeFromSource(dolOffset, 0xE0);
|
||||
if (dolHeaderBytes.Length > 0)
|
||||
{
|
||||
int dolHeaderOffset = 0;
|
||||
var dolHeader = Serialization.Readers.NintendoDisc.ParseDOLHeader(dolHeaderBytes, ref dolHeaderOffset);
|
||||
|
||||
int dolSize = GetDolSize(dolHeader);
|
||||
WriteRange(dolOffset, dolSize, Path.Combine(sysDir, "main.dol"));
|
||||
}
|
||||
}
|
||||
|
||||
// FST offset stored without shift on GameCube
|
||||
long fstOffset = Model.Header.FstOffset;
|
||||
long fstSize = Model.Header.FstSize;
|
||||
long fstOffset = Header.FstOffset;
|
||||
long fstSize = Header.FstSize;
|
||||
if (fstOffset > 0 && fstSize > 0)
|
||||
{
|
||||
WriteRange(fstOffset, (int)Math.Min(fstSize, int.MaxValue),
|
||||
Path.Combine(sysDir, "fst.bin"));
|
||||
WriteRange(fstOffset, (int)Math.Min(fstSize, int.MaxValue), Path.Combine(sysDir, "fst.bin"));
|
||||
|
||||
byte[]? fstData = ReadDisc(fstOffset, (int)Math.Min(fstSize, int.MaxValue));
|
||||
if (fstData != null)
|
||||
byte[] fstData = ReadRangeFromSource(fstOffset, (int)Math.Min(fstSize, int.MaxValue));
|
||||
if (fstData.Length > 0)
|
||||
{
|
||||
string filesDir = Path.Combine(dest, "files");
|
||||
string filesDir = Path.Combine(outputDirectory, "files");
|
||||
Directory.CreateDirectory(filesDir);
|
||||
ExtractFstFiles(fstData, offsetShift: 0, filesDir, ReadDisc);
|
||||
ExtractFstFiles(fstData, offsetShift: 0, filesDir, ReadRangeFromSource);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,28 +105,33 @@ namespace SabreTools.Wrappers
|
||||
|
||||
#region Wii extraction
|
||||
|
||||
private bool ExtractWii(string dest)
|
||||
/// <summary>
|
||||
/// Extract Wii data from a disc
|
||||
/// </summary>
|
||||
/// <param name="outputDirectory">Output directory to write to</param>
|
||||
/// <returns>True if the extraction was successful, false otherwise</returns>
|
||||
private bool ExtractWii(string outputDirectory)
|
||||
{
|
||||
// Unencrypted disc header area
|
||||
string discDir = Path.Combine(dest, "disc");
|
||||
string discDir = Path.Combine(outputDirectory, "disc");
|
||||
Directory.CreateDirectory(discDir);
|
||||
|
||||
WriteRange(0, 0x100, Path.Combine(discDir, "header.bin"));
|
||||
WriteRange(Constants.WiiRegionDataAddress, Constants.WiiRegionDataSize,
|
||||
Path.Combine(discDir, "region.bin"));
|
||||
WriteRange(Constants.WiiRegionDataAddress, Constants.WiiRegionDataSize, Path.Combine(discDir, "region.bin"));
|
||||
|
||||
if (Model.PartitionTableEntries is null)
|
||||
return true;
|
||||
|
||||
var typeCounters = new System.Collections.Generic.Dictionary<uint, int>();
|
||||
var typeCounters = new Dictionary<uint, int>();
|
||||
|
||||
foreach (var pte in Model.PartitionTableEntries)
|
||||
{
|
||||
long partOffset = pte.Offset;
|
||||
long partOffset = pte.Offset << 2;
|
||||
if (partOffset <= 0 || partOffset >= _dataSource.Length)
|
||||
continue;
|
||||
|
||||
string partName = GetPartitionName(pte.Type, typeCounters);
|
||||
string partDir = Path.Combine(dest, partName);
|
||||
string partDir = Path.Combine(outputDirectory, partName);
|
||||
Directory.CreateDirectory(partDir);
|
||||
|
||||
ExtractWiiPartition(partOffset, partDir);
|
||||
@@ -114,15 +140,22 @@ namespace SabreTools.Wrappers
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ExtractWiiPartition(long partOffset, string partDir)
|
||||
/// <summary>
|
||||
/// Extract a Wii partition
|
||||
/// </summary>
|
||||
/// <param name="partitionOffset">Offset to the partition</param>
|
||||
/// <param name="outputDirectory">Output directory to write to</param>
|
||||
private void ExtractWiiPartition(long partitionOffset, string outputDirectory)
|
||||
{
|
||||
// ticket.bin (unencrypted, 0x2A4 bytes at partition start)
|
||||
WriteRange(partOffset, Constants.WiiTicketSize, Path.Combine(partDir, "ticket.bin"));
|
||||
WriteRange(partitionOffset, Constants.WiiTicketSize, Path.Combine(outputDirectory, "ticket.bin"));
|
||||
|
||||
byte[]? ticketData = ReadDisc(partOffset, Constants.WiiTicketSize);
|
||||
byte[]? ticketData = ReadRangeFromSource(partitionOffset, Constants.WiiTicketSize);
|
||||
if (ticketData is null || ticketData.Length < Constants.TicketCommonKeyIndexOffset + 1)
|
||||
return;
|
||||
|
||||
#region Title Key
|
||||
|
||||
// Decrypt title key
|
||||
byte[] encTitleKey = new byte[16];
|
||||
Array.Copy(ticketData, Constants.TicketEncryptedTitleKeyOffset, encTitleKey, 0, 16);
|
||||
@@ -134,59 +167,76 @@ namespace SabreTools.Wrappers
|
||||
if (titleKey is null)
|
||||
return;
|
||||
|
||||
// TMD
|
||||
byte[]? tmdSizeBytes = ReadDisc(partOffset + Constants.WiiTmdSizeAddress, 4);
|
||||
#endregion
|
||||
|
||||
#region TMD
|
||||
|
||||
byte[] tmdSizeBytes = ReadRangeFromSource(partitionOffset + Constants.WiiTmdSizeAddress, 4);
|
||||
int tmdSizePos = 0;
|
||||
uint tmdSize = tmdSizeBytes != null
|
||||
uint tmdSize = tmdSizeBytes.Length > 0
|
||||
? tmdSizeBytes.ReadUInt32BigEndian(ref tmdSizePos)
|
||||
: 0;
|
||||
byte[]? tmdOffBytes = ReadDisc(partOffset + Constants.WiiTmdOffsetAddress, 4);
|
||||
|
||||
byte[] tmdOffBytes = ReadRangeFromSource(partitionOffset + Constants.WiiTmdOffsetAddress, 4);
|
||||
int tmdOffPos = 0;
|
||||
uint tmdOffShifted = tmdOffBytes != null
|
||||
uint tmdOffShifted = tmdOffBytes.Length > 0
|
||||
? tmdOffBytes.ReadUInt32BigEndian(ref tmdOffPos)
|
||||
: 0;
|
||||
|
||||
long tmdOffset = (long)tmdOffShifted << 2;
|
||||
if (tmdSize > 0 && tmdOffset > 0)
|
||||
WriteRange(partOffset + tmdOffset, (int)tmdSize, Path.Combine(partDir, "tmd.bin"));
|
||||
WriteRange(partitionOffset + tmdOffset, (int)tmdSize, Path.Combine(outputDirectory, "tmd.bin"));
|
||||
|
||||
// cert.bin
|
||||
byte[]? certSizeBytes = ReadDisc(partOffset + Constants.WiiCertSizeAddress, 4);
|
||||
#endregion
|
||||
|
||||
#region cert.bin
|
||||
|
||||
byte[] certSizeBytes = ReadRangeFromSource(partitionOffset + Constants.WiiCertSizeAddress, 4);
|
||||
int certSizePos = 0;
|
||||
uint certSize = certSizeBytes != null
|
||||
uint certSize = certSizeBytes.Length > 0
|
||||
? certSizeBytes.ReadUInt32BigEndian(ref certSizePos)
|
||||
: 0;
|
||||
byte[]? certOffBytes = ReadDisc(partOffset + Constants.WiiCertOffsetAddress, 4);
|
||||
|
||||
byte[] certOffBytes = ReadRangeFromSource(partitionOffset + Constants.WiiCertOffsetAddress, 4);
|
||||
int certOffPos = 0;
|
||||
uint certOffShifted = certOffBytes != null
|
||||
uint certOffShifted = certOffBytes.Length > 0
|
||||
? certOffBytes.ReadUInt32BigEndian(ref certOffPos)
|
||||
: 0;
|
||||
|
||||
long certOffset = (long)certOffShifted << 2;
|
||||
if (certSize > 0 && certOffset > 0)
|
||||
WriteRange(partOffset + certOffset, (int)certSize, Path.Combine(partDir, "cert.bin"));
|
||||
WriteRange(partitionOffset + certOffset, (int)certSize, Path.Combine(outputDirectory, "cert.bin"));
|
||||
|
||||
// h3.bin
|
||||
byte[]? h3OffBytes = ReadDisc(partOffset + Constants.WiiH3OffsetAddress, 4);
|
||||
#endregion
|
||||
|
||||
#region h3.bin
|
||||
|
||||
byte[] h3OffBytes = ReadRangeFromSource(partitionOffset + Constants.WiiH3OffsetAddress, 4);
|
||||
int h3OffPos = 0;
|
||||
uint h3OffShifted = h3OffBytes != null
|
||||
uint h3OffShifted = h3OffBytes.Length > 0
|
||||
? h3OffBytes.ReadUInt32BigEndian(ref h3OffPos)
|
||||
: 0;
|
||||
|
||||
long h3Offset = (long)h3OffShifted << 2;
|
||||
if (h3Offset > 0)
|
||||
WriteRange(partOffset + h3Offset, Constants.WiiH3Size, Path.Combine(partDir, "h3.bin"));
|
||||
WriteRange(partitionOffset + h3Offset, Constants.WiiH3Size, Path.Combine(outputDirectory, "h3.bin"));
|
||||
|
||||
#endregion
|
||||
|
||||
// Encrypted partition data start
|
||||
byte[]? dataOffBytes = ReadDisc(partOffset + Constants.WiiDataOffsetAddress, 4);
|
||||
byte[] dataOffBytes = ReadRangeFromSource(partitionOffset + Constants.WiiDataOffsetAddress, 4);
|
||||
int dataOffPos = 0;
|
||||
uint dataOffShifted = dataOffBytes != null
|
||||
uint dataOffShifted = dataOffBytes.Length > 0
|
||||
? dataOffBytes.ReadUInt32BigEndian(ref dataOffPos)
|
||||
: 0;
|
||||
|
||||
long dataOffset = (long)dataOffShifted << 2;
|
||||
if (dataOffset <= 0)
|
||||
return;
|
||||
|
||||
long absDataOffset = partOffset + dataOffset;
|
||||
long absDataOffset = partitionOffset + dataOffset;
|
||||
|
||||
string sysDir = Path.Combine(partDir, "sys");
|
||||
string sysDir = Path.Combine(outputDirectory, "sys");
|
||||
Directory.CreateDirectory(sysDir);
|
||||
|
||||
// Read boot block from decrypted partition (block 0, offset 0 within data)
|
||||
@@ -197,57 +247,79 @@ namespace SabreTools.Wrappers
|
||||
File.WriteAllBytes(Path.Combine(sysDir, "boot.bin"), bootBlock);
|
||||
|
||||
// bi2.bin
|
||||
byte[]? bi2 = ReadDecryptedPartitionRange(absDataOffset, titleKey,
|
||||
Constants.Bi2Address, Constants.Bi2Size);
|
||||
if (bi2 != null)
|
||||
byte[]? bi2 = ReadDecryptedPartitionRange(absDataOffset, titleKey, Constants.Bi2Address, Constants.Bi2Size);
|
||||
if (bi2 is not null)
|
||||
File.WriteAllBytes(Path.Combine(sysDir, "bi2.bin"), bi2);
|
||||
|
||||
// apploader
|
||||
WriteWiiApploader(absDataOffset, titleKey, sysDir);
|
||||
|
||||
// DOL — stored offset is shifted <<2 in Wii partition
|
||||
#region DOL
|
||||
|
||||
// Stored offset is shifted <<2 in Wii partition
|
||||
int dolOffPos = 0x420;
|
||||
uint dolOffShifted = bootBlock.ReadUInt32BigEndian(ref dolOffPos);
|
||||
long dolOff = (long)dolOffShifted << 2;
|
||||
if (dolOff > 0)
|
||||
{
|
||||
byte[]? dolHdr = ReadDecryptedPartitionRange(absDataOffset, titleKey, dolOff, 0xE0);
|
||||
if (dolHdr != null)
|
||||
byte[]? dolHeaderBytes = ReadDecryptedPartitionRange(absDataOffset, titleKey, dolOff, 0xE0);
|
||||
if (dolHeaderBytes is not null)
|
||||
{
|
||||
int dolSize = GetDolSize(dolHdr);
|
||||
int dolHeaderOffset = 0;
|
||||
var dolHeader = Serialization.Readers.NintendoDisc.ParseDOLHeader(dolHeaderBytes, ref dolHeaderOffset);
|
||||
|
||||
int dolSize = GetDolSize(dolHeader);
|
||||
byte[]? dol = ReadDecryptedPartitionRange(absDataOffset, titleKey, dolOff, dolSize);
|
||||
if (dol != null)
|
||||
if (dol is not null)
|
||||
File.WriteAllBytes(Path.Combine(sysDir, "main.dol"), dol);
|
||||
}
|
||||
}
|
||||
|
||||
// FST — stored offset shifted <<2 in Wii partition
|
||||
#endregion
|
||||
|
||||
#region FST
|
||||
|
||||
// Stored offset shifted <<2 in Wii partition
|
||||
int fstOffPos = 0x424;
|
||||
int fstSzPos = 0x428;
|
||||
uint fstOffShifted = bootBlock.ReadUInt32BigEndian(ref fstOffPos);
|
||||
uint fstSzShifted = bootBlock.ReadUInt32BigEndian(ref fstSzPos);
|
||||
long fstOff = (long)fstOffShifted << 2;
|
||||
|
||||
int fstSzPos = 0x428;
|
||||
uint fstSzShifted = bootBlock.ReadUInt32BigEndian(ref fstSzPos);
|
||||
long fstSize = (long)fstSzShifted << 2; // also stored >>2 on Wii
|
||||
|
||||
if (fstOff > 0 && fstSize > 0)
|
||||
{
|
||||
byte[]? fstData = ReadDecryptedPartitionRange(absDataOffset, titleKey,
|
||||
fstOff, (int)Math.Min(fstSize, int.MaxValue));
|
||||
if (fstData != null)
|
||||
byte[]? fstData = ReadDecryptedPartitionRange(absDataOffset, titleKey, fstOff, (int)Math.Min(fstSize, int.MaxValue));
|
||||
if (fstData is not null)
|
||||
{
|
||||
File.WriteAllBytes(Path.Combine(sysDir, "fst.bin"), fstData);
|
||||
string filesDir = Path.Combine(partDir, "files");
|
||||
string filesDir = Path.Combine(outputDirectory, "files");
|
||||
Directory.CreateDirectory(filesDir);
|
||||
ExtractFstFiles(fstData, offsetShift: 2, filesDir,
|
||||
ExtractFstFiles(fstData,
|
||||
offsetShift: 2,
|
||||
filesDir,
|
||||
(offset, length) => ReadDecryptedPartitionRange(absDataOffset, titleKey, offset, length));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region FST extraction
|
||||
#region FST Extraction
|
||||
|
||||
private void ExtractFstFiles(byte[] fstData, int offsetShift, string filesDir,
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="fstData"></param>
|
||||
/// <param name="offsetShift"></param>
|
||||
/// <param name="filesDir"></param>
|
||||
/// <param name="readFunc"></param>
|
||||
private void ExtractFstFiles(byte[] fstData,
|
||||
int offsetShift,
|
||||
string filesDir,
|
||||
Func<long, int, byte[]?> readFunc)
|
||||
{
|
||||
if (fstData is null || fstData.Length < 12)
|
||||
@@ -260,18 +332,21 @@ namespace SabreTools.Wrappers
|
||||
return;
|
||||
|
||||
// String table immediately follows all entries
|
||||
long stringTableOffset = rootCount * 12;
|
||||
int stringTableOffset = (int)(rootCount * 12);
|
||||
|
||||
ExtractFstDirectory(fstData, 1, (int)rootCount, stringTableOffset,
|
||||
filesDir, offsetShift, readFunc);
|
||||
ExtractFstDirectory(fstData, 1, (int)rootCount, stringTableOffset, filesDir, offsetShift, readFunc);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively extracts FST entries [start, end) into <paramref name="currentDir"/>.
|
||||
/// Returns the index of the next entry after this directory.
|
||||
/// </summary>
|
||||
private int ExtractFstDirectory(byte[] fstData, int start, int end,
|
||||
long stringTableOffset, string currentDir, int offsetShift,
|
||||
private int ExtractFstDirectory(byte[] fstData,
|
||||
int start,
|
||||
int end,
|
||||
int stringTableOffset,
|
||||
string currentDir,
|
||||
int offsetShift,
|
||||
Func<long, int, byte[]?> readFunc)
|
||||
{
|
||||
int i = start;
|
||||
@@ -283,14 +358,10 @@ namespace SabreTools.Wrappers
|
||||
|
||||
// Each FST entry is 12 bytes: [flags(1) | nameOff(3)] [fileOffRaw(4)] [fileSize(4)]
|
||||
int fstEntryPos = fstBase;
|
||||
uint flagsAndNameOff = fstData.ReadUInt32BigEndian(ref fstEntryPos);
|
||||
byte flags = (byte)(flagsAndNameOff >> 24);
|
||||
bool isDir = (flags & 1) != 0;
|
||||
uint nameOff = flagsAndNameOff & 0x00FFFFFFu;
|
||||
uint fileOffRaw = fstData.ReadUInt32BigEndian(ref fstEntryPos);
|
||||
uint fileSize = fstData.ReadUInt32BigEndian(ref fstEntryPos);
|
||||
var entry = Serialization.Readers.NintendoDisc.ParseFileSystemTableEntry(fstData, ref fstEntryPos);
|
||||
|
||||
string name = ReadFstString(fstData, stringTableOffset + nameOff);
|
||||
int nameOffset = stringTableOffset + (int)(entry.NameOffset & 0x00FFFFFF);
|
||||
string? name = fstData.ReadNullTerminatedAnsiString(ref nameOffset);
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
i++;
|
||||
@@ -298,19 +369,21 @@ namespace SabreTools.Wrappers
|
||||
}
|
||||
|
||||
// Sanitize name: replace path separators and reject/flatten dot-segments
|
||||
name = name.Replace('/', '_').Replace('\\', '_');
|
||||
name = name!.Replace('/', '_').Replace('\\', '_');
|
||||
if (name == "." || name == "..")
|
||||
name = "_";
|
||||
|
||||
name = name.TrimStart('.');
|
||||
|
||||
byte flags = (byte)(entry.NameOffset >> 24);
|
||||
bool isDir = (flags & 1) != 0;
|
||||
if (isDir)
|
||||
{
|
||||
// fileOffRaw = parent entry index; fileSize = last entry index in this dir
|
||||
int nextEntry = (int)fileSize;
|
||||
int nextEntry = (int)entry.FileSize;
|
||||
string subDir = Path.Combine(currentDir, name);
|
||||
Directory.CreateDirectory(subDir);
|
||||
i = ExtractFstDirectory(fstData, i + 1, nextEntry, stringTableOffset,
|
||||
subDir, offsetShift, readFunc);
|
||||
i = ExtractFstDirectory(fstData, i + 1, nextEntry, stringTableOffset, subDir, offsetShift, readFunc);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -319,16 +392,16 @@ namespace SabreTools.Wrappers
|
||||
if (!string.IsNullOrEmpty(outDir))
|
||||
Directory.CreateDirectory(outDir);
|
||||
|
||||
if (fileSize == 0)
|
||||
if (entry.FileSize == 0)
|
||||
{
|
||||
// Zero-byte file — create empty
|
||||
File.WriteAllBytes(outPath, new byte[0]);
|
||||
File.WriteAllBytes(outPath, []);
|
||||
}
|
||||
else
|
||||
{
|
||||
long discOffset = (long)fileOffRaw << offsetShift;
|
||||
byte[]? fileData = readFunc(discOffset, (int)Math.Min(fileSize, int.MaxValue));
|
||||
if (fileData != null)
|
||||
long discOffset = (long)entry.FileOffset << offsetShift;
|
||||
byte[]? fileData = readFunc(discOffset, (int)Math.Min(entry.FileSize, int.MaxValue));
|
||||
if (fileData is not null)
|
||||
File.WriteAllBytes(outPath, fileData);
|
||||
}
|
||||
|
||||
@@ -339,103 +412,155 @@ namespace SabreTools.Wrappers
|
||||
return i;
|
||||
}
|
||||
|
||||
private static string ReadFstString(byte[] fstData, long offset)
|
||||
{
|
||||
if (offset < 0 || offset >= fstData.Length)
|
||||
return string.Empty;
|
||||
|
||||
int start = (int)offset;
|
||||
int end = start;
|
||||
while (end < fstData.Length && fstData[end] != 0)
|
||||
end++;
|
||||
|
||||
return System.Text.Encoding.ASCII.GetString(fstData, start, end - start);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Apploader helpers
|
||||
#region Apploader Writing
|
||||
|
||||
private void WriteApploader(string sysDir)
|
||||
/// <summary>
|
||||
/// Write GameCube apploader.img to the output directory, if possible
|
||||
/// </summary>
|
||||
/// <param name="outputDirectory">Output directory to write to</param>
|
||||
private bool WriteGCApploader(string outputDirectory)
|
||||
{
|
||||
byte[]? hdr = ReadDisc(Constants.ApploaderAddress, Constants.ApploaderHeaderSize);
|
||||
if (hdr is null) return;
|
||||
byte[] header = ReadRangeFromSource(Constants.ApploaderAddress, Constants.ApploaderHeaderSize);
|
||||
if (header.Length == 0)
|
||||
return false;
|
||||
|
||||
int codeSizePos = Constants.ApploaderCodeSizeOffset;
|
||||
int trailerSizePos = Constants.ApploaderTrailerSizeOffset;
|
||||
uint codeSize = hdr.ReadUInt32BigEndian(ref codeSizePos);
|
||||
uint trailerSize = hdr.ReadUInt32BigEndian(ref trailerSizePos);
|
||||
int index = Constants.ApploaderCodeSizeOffset;
|
||||
uint codeSize = header.ReadUInt32BigEndian(ref index);
|
||||
|
||||
index = Constants.ApploaderTrailerSizeOffset;
|
||||
uint trailerSize = header.ReadUInt32BigEndian(ref index);
|
||||
|
||||
int totalSize = Constants.ApploaderHeaderSize + (int)codeSize + (int)trailerSize;
|
||||
WriteRange(Constants.ApploaderAddress, totalSize, Path.Combine(sysDir, "apploader.img"));
|
||||
WriteRange(Constants.ApploaderAddress, totalSize, Path.Combine(outputDirectory, "apploader.img"));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write Wii apploader.img to the output directory, if possible
|
||||
/// </summary>
|
||||
/// <param name="absDataOffset"></param>
|
||||
/// <param name="titleKey"></param>
|
||||
/// <param name="sysDir"></param>
|
||||
private void WriteWiiApploader(long absDataOffset, byte[] titleKey, string sysDir)
|
||||
{
|
||||
byte[]? hdr = ReadDecryptedPartitionRange(absDataOffset, titleKey,
|
||||
Constants.ApploaderAddress, Constants.ApploaderHeaderSize);
|
||||
if (hdr is null) return;
|
||||
byte[]? header = ReadDecryptedPartitionRange(absDataOffset, titleKey, Constants.ApploaderAddress, Constants.ApploaderHeaderSize);
|
||||
if (header is null)
|
||||
return;
|
||||
|
||||
int wiiCodeSizePos = Constants.ApploaderCodeSizeOffset;
|
||||
int wiiTrailerSizePos = Constants.ApploaderTrailerSizeOffset;
|
||||
uint codeSize = hdr.ReadUInt32BigEndian(ref wiiCodeSizePos);
|
||||
uint trailerSize = hdr.ReadUInt32BigEndian(ref wiiTrailerSizePos);
|
||||
int index = Constants.ApploaderCodeSizeOffset;
|
||||
uint codeSize = header.ReadUInt32BigEndian(ref index);
|
||||
|
||||
index = Constants.ApploaderTrailerSizeOffset;
|
||||
uint trailerSize = header.ReadUInt32BigEndian(ref index);
|
||||
|
||||
int totalSize = Constants.ApploaderHeaderSize + (int)codeSize + (int)trailerSize;
|
||||
byte[]? apploader = ReadDecryptedPartitionRange(absDataOffset, titleKey,
|
||||
Constants.ApploaderAddress, totalSize);
|
||||
if (apploader != null)
|
||||
byte[]? apploader = ReadDecryptedPartitionRange(absDataOffset, titleKey, Constants.ApploaderAddress, totalSize);
|
||||
if (apploader is not null)
|
||||
File.WriteAllBytes(Path.Combine(sysDir, "apploader.img"), apploader);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DOL size calculation
|
||||
#region Helpers
|
||||
|
||||
private static int GetDolSize(byte[] dolHeader)
|
||||
/// <summary>
|
||||
/// Total byte length of the raw disc image data
|
||||
/// </summary>
|
||||
internal long DataLength => _dataSource.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Read <paramref name="length"/> bytes from the disc image at <paramref name="offset"/>.
|
||||
/// </summary>
|
||||
internal byte[] ReadData(long offset, int length) => ReadRangeFromSource(offset, length);
|
||||
|
||||
/// <summary>
|
||||
/// Get the size of the DOL based on the header
|
||||
/// </summary>
|
||||
/// <param name="dolHeader">DOL header to retrieve the size for</param>
|
||||
/// <returns>The size of the DOL on success, 0 otherwise</returns>
|
||||
private static int GetDolSize(DOLHeader dolHeader)
|
||||
{
|
||||
// DOL header: 7 text section offsets (0x00), 11 data section offsets (0x1C),
|
||||
// 7 text sizes (0x90), 11 data sizes (0xAC), BSS offset (0xD8), BSS size (0xDC),
|
||||
// entry point (0xE0). Max (offset + size) over all sections gives the DOL size.
|
||||
if (dolHeader is null || dolHeader.Length < 0xE0)
|
||||
if (dolHeader.SectionOffsetTable.Length != 18)
|
||||
return 0;
|
||||
if (dolHeader.SectionLengthsTable.Length != 18)
|
||||
return 0;
|
||||
|
||||
int maxEnd = 0;
|
||||
// Text sections (7): offset table at 0x00, size table at 0x90
|
||||
for (int s = 0; s < 7; s++)
|
||||
|
||||
// Loop through the 18 offset and size entries
|
||||
for (int i = 0; i < 18; i++)
|
||||
{
|
||||
int offPos = s * 4;
|
||||
int szPos = 0x90 + (s * 4);
|
||||
int off = (int)dolHeader.ReadUInt32BigEndian(ref offPos);
|
||||
int sz = (int)dolHeader.ReadUInt32BigEndian(ref szPos);
|
||||
if (off > 0 && sz > 0) maxEnd = Math.Max(maxEnd, off + sz);
|
||||
}
|
||||
// Data sections (11): offset table at 0x1C, size table at 0xAC
|
||||
for (int s = 0; s < 11; s++)
|
||||
{
|
||||
int offPos = 0x1C + (s * 4);
|
||||
int szPos = 0xAC + (s * 4);
|
||||
int off = (int)dolHeader.ReadUInt32BigEndian(ref offPos);
|
||||
int sz = (int)dolHeader.ReadUInt32BigEndian(ref szPos);
|
||||
if (off > 0 && sz > 0) maxEnd = Math.Max(maxEnd, off + sz);
|
||||
uint offset = dolHeader.SectionOffsetTable[i];
|
||||
uint size = dolHeader.SectionLengthsTable[i];
|
||||
if (offset > 0 && size > 0)
|
||||
maxEnd = (int)Math.Max(maxEnd, offset + size);
|
||||
}
|
||||
|
||||
return maxEnd;
|
||||
}
|
||||
|
||||
#endregion
|
||||
/// <summary>
|
||||
/// Get the name of a given partition type
|
||||
/// </summary>
|
||||
/// <param name="type">Parition type</param>
|
||||
/// <param name="counters">Dictionary of counters used for global indexing</param>
|
||||
/// <returns>String representing the partition name</returns>
|
||||
private static string GetPartitionName(uint type, Dictionary<uint, int> counters)
|
||||
{
|
||||
string code;
|
||||
switch (type)
|
||||
{
|
||||
// GM + counter
|
||||
case 0: code = "GM"; break;
|
||||
|
||||
#region Wii partition block decryption helpers
|
||||
// UP + counter
|
||||
case 1: code = "UP"; break;
|
||||
|
||||
// CH + counter
|
||||
case 2: code = "CH"; break;
|
||||
|
||||
// Unknown: if all 4 bytes are printable ASCII, use the raw 4-char string (no prefix, no counter).
|
||||
// Otherwise fall back to P{globalIndex} — we use the cumulative counter sum as the index.
|
||||
default:
|
||||
byte b0 = (byte)(type >> 24),
|
||||
b1 = (byte)(type >> 16),
|
||||
b2 = (byte)(type >> 8),
|
||||
b3 = (byte)type;
|
||||
|
||||
if (b0 >= 0x20 && b0 <= 0x7E
|
||||
&& b1 >= 0x20 && b1 <= 0x7E
|
||||
&& b2 >= 0x20 && b2 <= 0x7E
|
||||
&& b3 >= 0x20 && b3 <= 0x7E)
|
||||
{
|
||||
return Encoding.ASCII.GetString([b0, b1, b2, b3]);
|
||||
}
|
||||
|
||||
// Non-printable: use global partition index (sum of all counter values so far)
|
||||
int globalIndex = 0;
|
||||
foreach (var v in counters.Values)
|
||||
{
|
||||
globalIndex += v;
|
||||
}
|
||||
|
||||
return $"P{globalIndex}";
|
||||
}
|
||||
|
||||
int index = counters.TryGetValue(type, out int cv) ? cv : 0;
|
||||
counters[type] = index + 1;
|
||||
return $"{code}{index}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads <paramref name="length"/> bytes at <paramref name="partitionDataOffset"/> within
|
||||
/// the decrypted partition data, decrypting 0x8000-byte blocks as needed.
|
||||
/// <paramref name="absDataOffset"/> is the absolute ISO offset where the encrypted data begins.
|
||||
/// </summary>
|
||||
private byte[]? ReadDecryptedPartitionRange(long absDataOffset, byte[] titleKey,
|
||||
long partitionDataOffset, int length)
|
||||
private byte[]? ReadDecryptedPartitionRange(long absDataOffset, byte[] titleKey, long partitionDataOffset, int length)
|
||||
{
|
||||
if (length <= 0) return null;
|
||||
if (length <= 0)
|
||||
return null;
|
||||
|
||||
// WIA/RVZ fast path: data is already decrypted; skip the AES round-trip.
|
||||
if (_preDecryptedReader is not null)
|
||||
@@ -448,11 +573,11 @@ namespace SabreTools.Wrappers
|
||||
{
|
||||
long dataOff = partitionDataOffset + produced;
|
||||
long blockNum = dataOff / Constants.WiiBlockDataSize;
|
||||
int offsetInBlock = (int)(dataOff % Constants.WiiBlockDataSize);
|
||||
int offsetInBlock = (int)(dataOff % Constants.WiiBlockDataSize);
|
||||
|
||||
long encBlockOffset = absDataOffset + (blockNum * Constants.WiiBlockSize);
|
||||
byte[]? encBlock = ReadDisc(encBlockOffset, Constants.WiiBlockSize);
|
||||
if (encBlock is null || encBlock.Length < Constants.WiiBlockSize)
|
||||
byte[] encBlock = ReadRangeFromSource(encBlockOffset, Constants.WiiBlockSize);
|
||||
if (encBlock.Length < Constants.WiiBlockSize)
|
||||
break;
|
||||
|
||||
// IV is at offset 0x3D0 of the raw (still-encrypted) block.
|
||||
@@ -460,7 +585,7 @@ namespace SabreTools.Wrappers
|
||||
byte[] iv = new byte[16];
|
||||
Array.Copy(encBlock, 0x3D0, iv, 0, 16);
|
||||
|
||||
// Decrypt the 0x7C00 data portion (bytes 0x400–0x7FFF of the raw block)
|
||||
// Decrypt the 0x7C00 data portion (bytes 0x400-0x7FFF of the raw block)
|
||||
byte[] encData = new byte[Constants.WiiBlockDataSize];
|
||||
Array.Copy(encBlock, Constants.WiiBlockHeaderSize, encData, 0, Constants.WiiBlockDataSize);
|
||||
|
||||
@@ -476,64 +601,26 @@ namespace SabreTools.Wrappers
|
||||
return produced == length ? result : null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Misc helpers
|
||||
|
||||
/// <summary>
|
||||
/// Write a range from the underlying data source to a file
|
||||
/// </summary>
|
||||
/// <param name="offset">Offset into the data source</param>
|
||||
/// <param name="length">Length of the data to read and write</param>
|
||||
/// <param name="filePath">Full path to the expected output file</param>
|
||||
private void WriteRange(long offset, int length, string filePath)
|
||||
{
|
||||
if (length <= 0) return;
|
||||
byte[]? data = ReadDisc(offset, length);
|
||||
if (data is null) return;
|
||||
string? dir = Path.GetDirectoryName(filePath);
|
||||
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
|
||||
File.WriteAllBytes(filePath, data);
|
||||
}
|
||||
if (length <= 0)
|
||||
return;
|
||||
|
||||
private byte[]? ReadDisc(long offset, int length)
|
||||
{
|
||||
if (length <= 0 || offset < 0) return null;
|
||||
byte[] data = ReadRangeFromSource(offset, length);
|
||||
return data.Length == length ? data : null;
|
||||
}
|
||||
if (data.Length == 0)
|
||||
return;
|
||||
|
||||
/// <summary>Total byte length of the raw disc image data.</summary>
|
||||
internal long DataLength => _dataSource.Length;
|
||||
string? dir = Path.GetDirectoryName(filePath);
|
||||
if (!string.IsNullOrEmpty(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
/// <summary>
|
||||
/// Read <paramref name="length"/> bytes from the disc image at <paramref name="offset"/>.
|
||||
/// Returns null if the range is out of bounds or a short read occurs.
|
||||
/// </summary>
|
||||
internal byte[]? ReadData(long offset, int length) => ReadDisc(offset, length);
|
||||
|
||||
private static string GetPartitionName(uint type,
|
||||
System.Collections.Generic.Dictionary<uint, int> counters)
|
||||
{
|
||||
// Matches DolphinIsoLib WiiDiscExtractor.PartitionFolderName exactly.
|
||||
// Known types: 0→GM+counter, 1→UP+counter, 2→CH+counter.
|
||||
// Unknown: if all 4 bytes are printable ASCII, use the raw 4-char string (no prefix, no counter).
|
||||
// Otherwise fall back to P{globalIndex} — we use the cumulative counter sum as the index.
|
||||
string code;
|
||||
switch (type)
|
||||
{
|
||||
case 0: code = "GM"; break;
|
||||
case 1: code = "UP"; break;
|
||||
case 2: code = "CH"; break;
|
||||
default:
|
||||
byte b0 = (byte)(type >> 24), b1 = (byte)(type >> 16),
|
||||
b2 = (byte)(type >> 8), b3 = (byte)type;
|
||||
if (b0 >= 0x20 && b0 <= 0x7E && b1 >= 0x20 && b1 <= 0x7E &&
|
||||
b2 >= 0x20 && b2 <= 0x7E && b3 >= 0x20 && b3 <= 0x7E)
|
||||
return System.Text.Encoding.ASCII.GetString(new byte[] { b0, b1, b2, b3 });
|
||||
// Non-printable: use global partition index (sum of all counter values so far)
|
||||
int globalIdx = 0;
|
||||
foreach (var v in counters.Values) globalIdx += v;
|
||||
return $"P{globalIdx}";
|
||||
}
|
||||
|
||||
int idx = counters.TryGetValue(type, out int cv) ? cv : 0;
|
||||
counters[type] = idx + 1;
|
||||
return $"{code}{idx}";
|
||||
File.WriteAllBytes(filePath, data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text;
|
||||
using SabreTools.Data.Models.NintendoDisc;
|
||||
using SabreTools.Text.Extensions;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
@@ -19,44 +20,69 @@ namespace SabreTools.Wrappers
|
||||
builder.AppendLine($"{Platform} Disc Image Information:");
|
||||
builder.AppendLine("-------------------------");
|
||||
|
||||
builder.AppendLine("Disc Header:");
|
||||
builder.AppendLine(Header.GameId, " Game ID");
|
||||
builder.AppendLine(Header.MakerCode, " Maker Code");
|
||||
builder.AppendLine(Header.DiscNumber, " Disc Number");
|
||||
builder.AppendLine(Header.DiscVersion, " Disc Version");
|
||||
builder.AppendLine(Header.AudioStreaming, " Audio Streaming");
|
||||
builder.AppendLine(Header.StreamingBufferSize, " Streaming Buffer Size");
|
||||
builder.AppendLine(Header.WiiMagic, " Wii Magic");
|
||||
builder.AppendLine(Header.GCMagic, " GC Magic");
|
||||
builder.AppendLine(Header.GameTitle, " Game Title");
|
||||
builder.AppendLine(Header.DisableHashVerification, " Disable Hash Verification");
|
||||
builder.AppendLine(Header.DisableDiscEncryption, " Disable Disc Encryption");
|
||||
builder.AppendLine(Header.DolOffset, " DOL Offset");
|
||||
builder.AppendLine(Header.FstOffset, " FST Offset");
|
||||
builder.AppendLine(Header.FstSize, " FST Size");
|
||||
Print(builder, Header);
|
||||
Print(builder, PartitionTableEntries);
|
||||
Print(builder, RegionData);
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, DiscHeader header)
|
||||
{
|
||||
builder.AppendLine(" Disc Header:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
|
||||
builder.AppendLine(header.GameId, " Game ID");
|
||||
builder.AppendLine(header.DiscNumber, " Disc Number");
|
||||
builder.AppendLine(header.DiscVersion, " Disc Version");
|
||||
builder.AppendLine(header.AudioStreaming, " Audio Streaming");
|
||||
builder.AppendLine(header.StreamingBufferSize, " Streaming Buffer Size");
|
||||
builder.AppendLine(header.WiiMagic, " Wii Magic");
|
||||
builder.AppendLine(header.GCMagic, " GC Magic");
|
||||
builder.AppendLine(header.GameTitle, " Game Title");
|
||||
builder.AppendLine(header.DisableHashVerification, " Disable Hash Verification");
|
||||
builder.AppendLine(header.DisableDiscEncryption, " Disable Disc Encryption");
|
||||
builder.AppendLine(header.DolOffset, " DOL Offset");
|
||||
builder.AppendLine(header.FstOffset, " FST Offset");
|
||||
builder.AppendLine(header.FstSize, " FST Size");
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
if (PartitionTableEntries is { Length: > 0 })
|
||||
private static void Print(StringBuilder builder, WiiPartitionTableEntry[]? entries)
|
||||
{
|
||||
builder.AppendLine(" Partition Table:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
if (entries is null || entries.Length == 0)
|
||||
{
|
||||
builder.AppendLine($"Partition Table ({PartitionTableEntries.Length} entries):");
|
||||
for (int i = 0; i < PartitionTableEntries.Length; i++)
|
||||
{
|
||||
var pt = PartitionTableEntries[i];
|
||||
builder.AppendLine($" Partition {i}:");
|
||||
builder.AppendLine(pt.Offset, " Offset");
|
||||
builder.AppendLine(pt.Type, " Type");
|
||||
}
|
||||
|
||||
builder.AppendLine(" No partition table entries");
|
||||
builder.AppendLine();
|
||||
return;
|
||||
}
|
||||
|
||||
if (RegionData is not null)
|
||||
for (int i = 0; i < entries.Length; i++)
|
||||
{
|
||||
builder.AppendLine("Region Data:");
|
||||
builder.AppendLine(RegionData.RegionSetting, " Region Setting");
|
||||
builder.AppendLine(RegionData.AgeRatings, " Age Ratings");
|
||||
builder.AppendLine();
|
||||
var entry = entries[i];
|
||||
|
||||
builder.AppendLine($" Partition Table Entry {i}");
|
||||
builder.AppendLine(entry.Offset, " Offset");
|
||||
builder.AppendLine(entry.Type, " Type");
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, WiiRegionData? regionData)
|
||||
{
|
||||
builder.AppendLine(" Region Data:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
if (regionData is null)
|
||||
{
|
||||
builder.AppendLine(" No region data");
|
||||
builder.AppendLine();
|
||||
return;
|
||||
}
|
||||
|
||||
builder.AppendLine(regionData.RegionSetting, " Region Setting");
|
||||
builder.AppendLine(regionData.AgeRatings, " Age Ratings");
|
||||
builder.AppendLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SabreTools.Data.Extensions;
|
||||
using SabreTools.Data.Models.NintendoDisc;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
@@ -18,14 +18,19 @@ namespace SabreTools.Wrappers
|
||||
/// <inheritdoc cref="Disc.Header"/>
|
||||
public DiscHeader Header => Model.Header;
|
||||
|
||||
/// <inheritdoc cref="Disc.Platform"/>
|
||||
public Platform Platform => Model.Platform;
|
||||
/// <summary>
|
||||
/// Detected platform (GameCube or Wii)
|
||||
/// </summary>
|
||||
public Platform Platform => Header.GetPlatform();
|
||||
|
||||
/// <inheritdoc cref="DiscHeader.GameId"/>
|
||||
public string GameId => Model.Header.GameId;
|
||||
|
||||
/// <inheritdoc cref="DiscHeader.MakerCode"/>
|
||||
public string MakerCode => Model.Header.MakerCode;
|
||||
/// <summary>
|
||||
/// 2-character ASCII maker / publisher code (e.g. "01")
|
||||
/// </summary>
|
||||
public string MakerCode
|
||||
=> GameId.Length >= 6 ? GameId.Substring(4, 2) : string.Empty;
|
||||
|
||||
/// <inheritdoc cref="DiscHeader.GameTitle"/>
|
||||
public string GameTitle => Model.Header.GameTitle;
|
||||
@@ -44,18 +49,6 @@ namespace SabreTools.Wrappers
|
||||
|
||||
#endregion
|
||||
|
||||
#region Pre-decrypted reader override
|
||||
|
||||
/// <summary>
|
||||
/// When set, <see cref="ReadDecryptedPartitionRange"/> calls this delegate instead of
|
||||
/// performing AES-CBC decryption. Used by WIA/RVZ extraction, where partition data is
|
||||
/// already stored decrypted and the encrypt-then-decrypt round-trip is unnecessary.
|
||||
/// Signature: (absDataOffset, partitionDataOffset, length) → decrypted bytes or null.
|
||||
/// </summary>
|
||||
internal Func<long, long, int, byte[]?>? _preDecryptedReader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
using System;
|
||||
using SabreTools.Numerics.Extensions;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
/// <summary>
|
||||
/// Decompressor for RVZ packed format.
|
||||
/// RVZ uses run-length encoding to store real data and junk data efficiently:
|
||||
/// - Real data: size (4 bytes) + data bytes
|
||||
/// - Junk data: size with high bit set (4 bytes) + 68-byte seed → regenerate using LFG
|
||||
/// </summary>
|
||||
internal class RvzPackDecompressor
|
||||
{
|
||||
private readonly byte[] m_packed_data;
|
||||
private readonly uint m_rvz_packed_size;
|
||||
private long m_data_offset;
|
||||
private readonly LaggedFibonacciGenerator m_lfg;
|
||||
|
||||
private int m_in_position = 0;
|
||||
private uint m_current_size = 0;
|
||||
private bool m_current_is_junk = false;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new RVZ pack decompressor.
|
||||
/// </summary>
|
||||
/// <param name="packedData">The packed RVZ data</param>
|
||||
/// <param name="rvzPackedSize">Expected size of packed data (for validation)</param>
|
||||
/// <param name="dataOffset">Offset in the virtual disc (for LFG alignment)</param>
|
||||
public RvzPackDecompressor(byte[] packedData, uint rvzPackedSize, long dataOffset)
|
||||
{
|
||||
m_packed_data = packedData ?? throw new ArgumentNullException(nameof(packedData));
|
||||
m_rvz_packed_size = rvzPackedSize;
|
||||
m_data_offset = dataOffset;
|
||||
m_lfg = new LaggedFibonacciGenerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses the packed data into the output buffer.
|
||||
/// </summary>
|
||||
/// <param name="output">Destination buffer</param>
|
||||
/// <param name="outputOffset">Offset in destination buffer</param>
|
||||
/// <param name="count">Number of bytes to decompress</param>
|
||||
/// <returns>Number of bytes actually decompressed</returns>
|
||||
public int Decompress(byte[] output, int outputOffset, int count)
|
||||
{
|
||||
int totalWritten = 0;
|
||||
|
||||
while (totalWritten < count && !IsDone())
|
||||
{
|
||||
if (m_current_size == 0)
|
||||
{
|
||||
if (!ReadNextSegment())
|
||||
break;
|
||||
}
|
||||
|
||||
int bytesToWrite = Math.Min((int)m_current_size, count - totalWritten);
|
||||
|
||||
if (m_current_is_junk)
|
||||
{
|
||||
m_lfg.GetBytes(bytesToWrite, output, outputOffset + totalWritten);
|
||||
}
|
||||
else
|
||||
{
|
||||
Array.Copy(m_packed_data, m_in_position, output, outputOffset + totalWritten, bytesToWrite);
|
||||
m_in_position += bytesToWrite;
|
||||
}
|
||||
|
||||
m_current_size -= (uint)bytesToWrite;
|
||||
totalWritten += bytesToWrite;
|
||||
m_data_offset += bytesToWrite;
|
||||
}
|
||||
|
||||
return totalWritten;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if decompression is complete.
|
||||
/// </summary>
|
||||
public bool IsDone() => m_current_size == 0 && m_in_position >= m_rvz_packed_size;
|
||||
|
||||
private bool ReadNextSegment()
|
||||
{
|
||||
if (m_in_position + 4 > m_packed_data.Length)
|
||||
return false;
|
||||
|
||||
// Size field is big-endian u32; high bit signals junk data
|
||||
uint sizeField = m_packed_data.ReadUInt32BigEndian(ref m_in_position);
|
||||
|
||||
m_current_is_junk = (sizeField & 0x80000000) != 0;
|
||||
m_current_size = sizeField & 0x7FFFFFFF;
|
||||
|
||||
if (m_current_is_junk)
|
||||
{
|
||||
if (m_in_position + (LaggedFibonacciGenerator.SEED_SIZE * 4) > m_packed_data.Length)
|
||||
return false;
|
||||
|
||||
byte[] seed = new byte[LaggedFibonacciGenerator.SEED_SIZE * 4];
|
||||
Array.Copy(m_packed_data, m_in_position, seed, 0, seed.Length);
|
||||
m_in_position += seed.Length;
|
||||
|
||||
m_lfg.SetSeed(seed);
|
||||
|
||||
// Advance LFG to the correct position within the buffer.
|
||||
// Dolphin: lfg.m_position_bytes = data_offset % (LFG_K * sizeof(u32))
|
||||
int offsetInBuffer = (int)(m_data_offset % LaggedFibonacciGenerator.BUFFER_BYTES);
|
||||
if (offsetInBuffer > 0)
|
||||
m_lfg.Forward(offsetInBuffer);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
using System.IO;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
{
|
||||
public partial class WIA : IExtractable
|
||||
@@ -7,7 +5,6 @@ namespace SabreTools.Wrappers
|
||||
/// <inheritdoc/>
|
||||
public bool Extract(string outputDirectory, bool includeDebug)
|
||||
{
|
||||
// Decompress WIA/RVZ to obtain the inner disc image, then delegate extraction.
|
||||
var inner = GetInnerWrapper();
|
||||
return inner?.Extract(outputDirectory, includeDebug) ?? false;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Text;
|
||||
using SabreTools.Data.Models.NintendoDisc;
|
||||
using SabreTools.Data.Models.WIA;
|
||||
using SabreTools.Text.Extensions;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
@@ -19,86 +21,125 @@ namespace SabreTools.Wrappers
|
||||
string formatName = IsRvz ? "RVZ" : "WIA";
|
||||
builder.AppendLine($"{formatName} Information:");
|
||||
builder.AppendLine("-------------------------");
|
||||
|
||||
builder.AppendLine("Header 1:");
|
||||
builder.AppendLine(Header1.Magic, " Magic");
|
||||
builder.AppendLine(Header1.Version, " Version");
|
||||
builder.AppendLine(Header1.VersionCompatible, " Version Compatible");
|
||||
builder.AppendLine(Header1.Header2Size, " Header 2 Size");
|
||||
builder.AppendLine(Header1.Header2Hash, " Header 2 Hash");
|
||||
builder.AppendLine(Header1.IsoFileSize, " ISO File Size");
|
||||
builder.AppendLine(Header1.WiaFileSize, " WIA File Size");
|
||||
builder.AppendLine(Header1.Header1Hash, " Header 1 Hash");
|
||||
builder.AppendLine();
|
||||
|
||||
builder.AppendLine("Header 2:");
|
||||
builder.AppendLine(Header2.DiscType.ToString(), " Disc Type");
|
||||
builder.AppendLine(Header2.CompressionType.ToString(), " Compression Type");
|
||||
builder.AppendLine(Header2.CompressionLevel, " Compression Level");
|
||||
builder.AppendLine(Header2.ChunkSize, " Chunk Size");
|
||||
builder.AppendLine(Header2.DiscHeader, " Disc Header");
|
||||
builder.AppendLine(Header2.NumberOfPartitionEntries, " Partition Entry Count");
|
||||
builder.AppendLine(Header2.PartitionEntrySize, " Partition Entry Size");
|
||||
builder.AppendLine(Header2.PartitionEntriesOffset, " Partition Entries Offset");
|
||||
builder.AppendLine(Header2.PartitionEntriesHash, " Partition Entries Hash");
|
||||
builder.AppendLine(Header2.NumberOfRawDataEntries, " Raw Data Entry Count");
|
||||
builder.AppendLine(Header2.RawDataEntriesOffset, " Raw Data Entries Offset");
|
||||
builder.AppendLine(Header2.RawDataEntriesSize, " Raw Data Entries Size");
|
||||
builder.AppendLine(Header2.NumberOfGroupEntries, " Group Entry Count");
|
||||
builder.AppendLine(Header2.GroupEntriesOffset, " Group Entries Offset");
|
||||
builder.AppendLine(Header2.GroupEntriesSize, " Group Entries Size");
|
||||
builder.AppendLine(Header2.CompressorDataSize, " Compressor Data Size");
|
||||
builder.AppendLine(Header2.CompressorData, " Compressor Data");
|
||||
Print(builder, Header1);
|
||||
Print(builder, Header2);
|
||||
Print(builder, DiscHeader);
|
||||
Print(builder, PartitionEntries);
|
||||
Print(builder, RawDataEntries);
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, WiaHeader1 header)
|
||||
{
|
||||
builder.AppendLine(" Header 1:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
builder.AppendLine(header.Magic, " Magic");
|
||||
builder.AppendLine(header.Version, " Version");
|
||||
builder.AppendLine(header.VersionCompatible, " Version Compatible");
|
||||
builder.AppendLine(header.Header2Size, " Header 2 Size");
|
||||
builder.AppendLine(header.Header2Hash, " Header 2 Hash");
|
||||
builder.AppendLine(header.IsoFileSize, " ISO File Size");
|
||||
builder.AppendLine(header.WiaFileSize, " WIA File Size");
|
||||
builder.AppendLine(header.Header1Hash, " Header 1 Hash");
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
var discHeader = DiscHeader;
|
||||
if (discHeader is not null)
|
||||
private static void Print(StringBuilder builder, WiaHeader2 header)
|
||||
{
|
||||
builder.AppendLine(" Header 2:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
builder.AppendLine(header.DiscType.ToString(), " Disc Type");
|
||||
builder.AppendLine(header.CompressionType.ToString(), " Compression Type");
|
||||
builder.AppendLine(header.CompressionLevel, " Compression Level");
|
||||
builder.AppendLine(header.ChunkSize, " Chunk Size");
|
||||
builder.AppendLine(header.DiscHeader, " Disc Header");
|
||||
builder.AppendLine(header.NumberOfPartitionEntries, " Partition Entry Count");
|
||||
builder.AppendLine(header.PartitionEntrySize, " Partition Entry Size");
|
||||
builder.AppendLine(header.PartitionEntriesOffset, " Partition Entries Offset");
|
||||
builder.AppendLine(header.PartitionEntriesHash, " Partition Entries Hash");
|
||||
builder.AppendLine(header.NumberOfRawDataEntries, " Raw Data Entry Count");
|
||||
builder.AppendLine(header.RawDataEntriesOffset, " Raw Data Entries Offset");
|
||||
builder.AppendLine(header.RawDataEntriesSize, " Raw Data Entries Size");
|
||||
builder.AppendLine(header.NumberOfGroupEntries, " Group Entry Count");
|
||||
builder.AppendLine(header.GroupEntriesOffset, " Group Entries Offset");
|
||||
builder.AppendLine(header.GroupEntriesSize, " Group Entries Size");
|
||||
builder.AppendLine(header.CompressorDataSize, " Compressor Data Size");
|
||||
builder.AppendLine(header.CompressorData, " Compressor Data");
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, DiscHeader? header)
|
||||
{
|
||||
builder.AppendLine(" Embedded Disc Header:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
if (header is null)
|
||||
{
|
||||
builder.AppendLine("Embedded Disc Header:");
|
||||
builder.AppendLine(discHeader.GameId, " Game ID");
|
||||
builder.AppendLine(discHeader.MakerCode, " Maker Code");
|
||||
builder.AppendLine(discHeader.DiscNumber, " Disc Number");
|
||||
builder.AppendLine(discHeader.DiscVersion, " Disc Version");
|
||||
builder.AppendLine(discHeader.GameTitle, " Game Title");
|
||||
builder.AppendLine(" No embedded disc header");
|
||||
builder.AppendLine();
|
||||
return;
|
||||
}
|
||||
|
||||
if (PartitionEntries is { Length: > 0 })
|
||||
{
|
||||
builder.AppendLine($"Partition Entries ({PartitionEntries.Length}):");
|
||||
for (int i = 0; i < PartitionEntries.Length; i++)
|
||||
{
|
||||
var pe = PartitionEntries[i];
|
||||
builder.AppendLine($" Partition {i}:");
|
||||
builder.AppendLine(pe.PartitionKey, " Partition Key");
|
||||
builder.AppendLine(pe.DataEntry0.FirstSector, " Data Entry 0 First Sector");
|
||||
builder.AppendLine(pe.DataEntry0.NumberOfSectors, " Data Entry 0 Sector Count");
|
||||
builder.AppendLine(pe.DataEntry0.GroupIndex, " Data Entry 0 Group Index");
|
||||
builder.AppendLine(pe.DataEntry0.NumberOfGroups, " Data Entry 0 Group Count");
|
||||
builder.AppendLine(pe.DataEntry1.FirstSector, " Data Entry 1 First Sector");
|
||||
builder.AppendLine(pe.DataEntry1.NumberOfSectors, " Data Entry 1 Sector Count");
|
||||
builder.AppendLine(pe.DataEntry1.GroupIndex, " Data Entry 1 Group Index");
|
||||
builder.AppendLine(pe.DataEntry1.NumberOfGroups, " Data Entry 1 Group Count");
|
||||
}
|
||||
builder.AppendLine(header.GameId, " Game ID");
|
||||
builder.AppendLine(header.DiscNumber, " Disc Number");
|
||||
builder.AppendLine(header.DiscVersion, " Disc Version");
|
||||
builder.AppendLine(header.GameTitle, " Game Title");
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, PartitionEntry[]? entries)
|
||||
{
|
||||
builder.AppendLine(" Partition Entry Table:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
if (entries is null || entries.Length == 0)
|
||||
{
|
||||
builder.AppendLine(" No partition table entries");
|
||||
builder.AppendLine();
|
||||
return;
|
||||
}
|
||||
|
||||
if (RawDataEntries is { Length: > 0 })
|
||||
for (int i = 0; i < entries.Length; i++)
|
||||
{
|
||||
builder.AppendLine($"Raw Data Entries ({RawDataEntries.Length}):");
|
||||
for (int i = 0; i < RawDataEntries.Length; i++)
|
||||
{
|
||||
var rde = RawDataEntries[i];
|
||||
builder.AppendLine($" Raw Data Entry {i}:");
|
||||
builder.AppendLine(rde.DataOffset, " Data Offset");
|
||||
builder.AppendLine(rde.DataSize, " Data Size");
|
||||
builder.AppendLine(rde.GroupIndex, " Group Index");
|
||||
builder.AppendLine(rde.NumberOfGroups, " Group Count");
|
||||
}
|
||||
var entry = entries[i];
|
||||
|
||||
builder.AppendLine();
|
||||
builder.AppendLine($" Partition Table Entry {i}:");
|
||||
builder.AppendLine(entry.PartitionKey, " Partition Key");
|
||||
builder.AppendLine(entry.DataEntry0.FirstSector, " Data Entry 0 First Sector");
|
||||
builder.AppendLine(entry.DataEntry0.NumberOfSectors, " Data Entry 0 Sector Count");
|
||||
builder.AppendLine(entry.DataEntry0.GroupIndex, " Data Entry 0 Group Index");
|
||||
builder.AppendLine(entry.DataEntry0.NumberOfGroups, " Data Entry 0 Group Count");
|
||||
builder.AppendLine(entry.DataEntry1.FirstSector, " Data Entry 1 First Sector");
|
||||
builder.AppendLine(entry.DataEntry1.NumberOfSectors, " Data Entry 1 Sector Count");
|
||||
builder.AppendLine(entry.DataEntry1.GroupIndex, " Data Entry 1 Group Index");
|
||||
builder.AppendLine(entry.DataEntry1.NumberOfGroups, " Data Entry 1 Group Count");
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
private static void Print(StringBuilder builder, RawDataEntry[]? entries)
|
||||
{
|
||||
builder.AppendLine(" Raw Data Entry Table:");
|
||||
builder.AppendLine(" -------------------------");
|
||||
if (entries is null || entries.Length == 0)
|
||||
{
|
||||
builder.AppendLine(" No raw data table entries");
|
||||
builder.AppendLine();
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < entries.Length; i++)
|
||||
{
|
||||
var entry = entries[i];
|
||||
|
||||
builder.AppendLine($" Raw Data Table Entry {i}:");
|
||||
builder.AppendLine(entry.DataOffset, " Data Offset");
|
||||
builder.AppendLine(entry.DataSize, " Data Size");
|
||||
builder.AppendLine(entry.GroupIndex, " Group Index");
|
||||
builder.AppendLine(entry.NumberOfGroups, " Group Count");
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SabreTools.Hashing;
|
||||
using SabreTools.Data.Models.NintendoDisc;
|
||||
using SabreTools.Data.Models.WIA;
|
||||
using WiaConstants = SabreTools.Data.Models.WIA.Constants;
|
||||
using SabreTools.Hashing;
|
||||
using static SabreTools.Data.Models.NintendoDisc.Constants;
|
||||
using static SabreTools.Data.Models.WIA.Constants;
|
||||
using WiaReader = SabreTools.Serialization.Readers.WIA;
|
||||
|
||||
namespace SabreTools.Wrappers
|
||||
@@ -19,6 +20,9 @@ namespace SabreTools.Wrappers
|
||||
|
||||
#region Extension Properties
|
||||
|
||||
/// <inheritdoc cref="DiscImage.GroupEntries"/>
|
||||
public WiaGroupEntry[]? GroupEntries => Model.GroupEntries;
|
||||
|
||||
/// <inheritdoc cref="DiscImage.Header1"/>
|
||||
public WiaHeader1 Header1 => Model.Header1;
|
||||
|
||||
@@ -26,7 +30,7 @@ namespace SabreTools.Wrappers
|
||||
public WiaHeader2 Header2 => Model.Header2;
|
||||
|
||||
/// <summary>True if this is an RVZ file; false if this is a WIA file.</summary>
|
||||
public bool IsRvz => Model.Header1.Magic == WiaConstants.RvzMagic;
|
||||
public bool IsRvz => Header1.Magic == RvzMagic;
|
||||
|
||||
/// <inheritdoc cref="DiscImage.PartitionEntries"/>
|
||||
public PartitionEntry[]? PartitionEntries => Model.PartitionEntries;
|
||||
@@ -34,10 +38,13 @@ namespace SabreTools.Wrappers
|
||||
/// <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 => Model.Header1.IsoFileSize;
|
||||
public ulong IsoFileSize => Header1.IsoFileSize;
|
||||
|
||||
/// <summary>
|
||||
/// Disc header parsed from the 128-byte raw disc header stored in Header2.
|
||||
@@ -46,19 +53,19 @@ namespace SabreTools.Wrappers
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_discHeader is not null)
|
||||
return _discHeader;
|
||||
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);
|
||||
_discHeader = Serialization.Readers.NintendoDisc.ParseDiscHeaderOnly(ms);
|
||||
return _discHeader;
|
||||
field = Serialization.Readers.NintendoDisc.ParseDiscHeader(ms);
|
||||
return field;
|
||||
}
|
||||
}
|
||||
|
||||
private DiscHeader? _discHeader;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
@@ -125,9 +132,11 @@ namespace SabreTools.Wrappers
|
||||
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);
|
||||
}
|
||||
@@ -137,6 +146,7 @@ namespace SabreTools.Wrappers
|
||||
}
|
||||
}
|
||||
|
||||
#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
|
||||
@@ -144,7 +154,6 @@ namespace SabreTools.Wrappers
|
||||
/// </summary>
|
||||
private static void DecompressTables(DiscImage model, Stream data, long baseOffset)
|
||||
{
|
||||
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
|
||||
var comp = model.Header2.CompressionType;
|
||||
|
||||
// None / Purge tables are stored as plain big-endian structs — already parsed correctly.
|
||||
@@ -155,13 +164,13 @@ namespace SabreTools.Wrappers
|
||||
byte compDataSize = model.Header2.CompressorDataSize;
|
||||
|
||||
// --- Raw data entries (stored compressed) ---
|
||||
if (model.Header2.NumberOfRawDataEntries > 0 &&
|
||||
model.Header2.RawDataEntriesOffset > 0 &&
|
||||
model.Header2.RawDataEntriesSize > 0)
|
||||
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 * WiaConstants.RawDataEntrySize;
|
||||
int expectedSize = count * RawDataEntrySize;
|
||||
|
||||
data.Seek(baseOffset + (long)model.Header2.RawDataEntriesOffset, SeekOrigin.Begin);
|
||||
byte[] buf = new byte[compressedSize];
|
||||
@@ -169,8 +178,7 @@ namespace SabreTools.Wrappers
|
||||
if (read < compressedSize)
|
||||
return;
|
||||
|
||||
byte[] plain = WiaRvzCompressionHelper.Decompress(
|
||||
comp, buf, 0, compressedSize, compData, compDataSize);
|
||||
byte[] plain = WiaRvzCompressionHelper.Decompress(comp, buf, 0, compressedSize, compData, compDataSize);
|
||||
if (plain is null || plain.Length < expectedSize)
|
||||
return;
|
||||
|
||||
@@ -178,13 +186,13 @@ namespace SabreTools.Wrappers
|
||||
}
|
||||
|
||||
// --- Group entries (stored compressed) ---
|
||||
if (model.Header2.NumberOfGroupEntries > 0 &&
|
||||
model.Header2.GroupEntriesOffset > 0 &&
|
||||
model.Header2.GroupEntriesSize > 0)
|
||||
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 == WiaConstants.RvzMagic ? WiaConstants.RvzGroupEntrySize : WiaConstants.WiaGroupEntrySize;
|
||||
int entrySize = model.Header1.Magic == RvzMagic ? RvzGroupEntrySize : WiaGroupEntrySize;
|
||||
int expectedSize = count * entrySize;
|
||||
|
||||
data.Seek(baseOffset + (long)model.Header2.GroupEntriesOffset, SeekOrigin.Begin);
|
||||
@@ -193,85 +201,66 @@ namespace SabreTools.Wrappers
|
||||
if (read < compressedSize)
|
||||
return;
|
||||
|
||||
byte[] plain = WiaRvzCompressionHelper.Decompress(
|
||||
comp, buf, 0, compressedSize, compData, compDataSize);
|
||||
byte[] plain = WiaRvzCompressionHelper.Decompress(comp, buf, 0, compressedSize, compData, compDataSize);
|
||||
if (plain is null || plain.Length < expectedSize)
|
||||
return;
|
||||
|
||||
if (model.Header1.Magic == WiaConstants.RvzMagic)
|
||||
if (model.Header1.Magic == RvzMagic)
|
||||
model.RvzGroupEntries = ParseRvzGroupEntries(plain, count);
|
||||
else
|
||||
model.GroupEntries = ParseWiaGroupEntries(plain, count);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
|
||||
/// <summary>Parses raw data entries from a plain (already decompressed) byte array.</summary>
|
||||
/// <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++)
|
||||
{
|
||||
int o = i * WiaConstants.RawDataEntrySize;
|
||||
var e = new RawDataEntry();
|
||||
e.DataOffset = ReadUInt64BE(plain, o);
|
||||
e.DataSize = ReadUInt64BE(plain, o + 8);
|
||||
e.GroupIndex = ReadUInt32BE(plain, o + 16);
|
||||
e.NumberOfGroups = ReadUInt32BE(plain, o + 20);
|
||||
entries[i] = e;
|
||||
entries[i] = WiaReader.ParseRawDataEntry(plain, ref offset);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// <summary>Parses WIA group entries from a plain (already decompressed) byte array.</summary>
|
||||
/// <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++)
|
||||
{
|
||||
int o = i * WiaConstants.WiaGroupEntrySize;
|
||||
var e = new WiaGroupEntry();
|
||||
e.DataOffset = (ulong)ReadUInt32BE(plain, o) << 2;
|
||||
e.DataSize = ReadUInt32BE(plain, o + 4);
|
||||
entries[i] = e;
|
||||
entries[i] = WiaReader.ParseWiaGroupEntry(plain, ref offset);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// <summary>Parses RVZ group entries from a plain (already decompressed) byte array.</summary>
|
||||
/// <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++)
|
||||
{
|
||||
int o = i * WiaConstants.RvzGroupEntrySize;
|
||||
var e = new RvzGroupEntry();
|
||||
e.DataOffset = (ulong)ReadUInt32BE(plain, o) << 2;
|
||||
e.DataSize = ReadUInt32BE(plain, o + 4);
|
||||
e.RvzPackedSize = ReadUInt32BE(plain, o + 8);
|
||||
entries[i] = e;
|
||||
entries[i] = WiaReader.ParseRvzGroupEntry(plain, ref offset);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
|
||||
private static ulong ReadUInt64BE(byte[] b, int o)
|
||||
{
|
||||
return ((ulong)b[o] << 56) | ((ulong)b[o + 1] << 48) | ((ulong)b[o + 2] << 40) | ((ulong)b[o + 3] << 32)
|
||||
| ((ulong)b[o + 4] << 24) | ((ulong)b[o + 5] << 16) | ((ulong)b[o + 6] << 8) | b[o + 7];
|
||||
}
|
||||
|
||||
private static uint ReadUInt32BE(byte[] b, int o)
|
||||
{
|
||||
return ((uint)b[o] << 24) | ((uint)b[o + 1] << 16) | ((uint)b[o + 2] << 8) | b[o + 3];
|
||||
}
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
|
||||
#region Inner Wrapper
|
||||
@@ -288,7 +277,7 @@ namespace SabreTools.Wrappers
|
||||
/// </summary>
|
||||
public NintendoDisc? GetInnerWrapper()
|
||||
{
|
||||
if (Model.Header1.IsoFileSize == 0)
|
||||
if (Header1.IsoFileSize == 0)
|
||||
return null;
|
||||
|
||||
var vStream = new WiaVirtualStream(this);
|
||||
@@ -299,47 +288,43 @@ namespace SabreTools.Wrappers
|
||||
// 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 (Model.PartitionEntries is { Length: > 0 })
|
||||
disc._preDecryptedReader = BuildPreDecryptedReader();
|
||||
if (PartitionEntries is not null && PartitionEntries.Length > 0)
|
||||
disc._preDecryptedReader = PreDecryptedReader;
|
||||
|
||||
return disc;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the delegate used by <see cref="NintendoDisc._preDecryptedReader"/>.
|
||||
/// 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
|
||||
/// <c>de.FirstSector * 0x8000</c>, then delegates to
|
||||
/// de.FirstSector * 0x8000, then delegates to
|
||||
/// <see cref="ReadDecryptedPartitionBytes"/>.
|
||||
/// </summary>
|
||||
private Func<long, long, int, byte[]?> BuildPreDecryptedReader()
|
||||
private byte[]? PreDecryptedReader(long absDataOffset, long partitionDataOffset, int length)
|
||||
{
|
||||
const int WiiBlockSize = 0x8000;
|
||||
return (absDataOffset, partitionDataOffset, length) =>
|
||||
{
|
||||
if (Model.PartitionEntries is null)
|
||||
return null;
|
||||
|
||||
foreach (var pe in Model.PartitionEntries)
|
||||
{
|
||||
// The data area of this partition starts at de.FirstSector * 0x8000
|
||||
long deIsoStart = (long)pe.DataEntry0.FirstSector * WiiBlockSize;
|
||||
long deIsoEnd = deIsoStart + ((long)pe.DataEntry0.NumberOfSectors * WiiBlockSize);
|
||||
|
||||
if (absDataOffset >= deIsoStart && absDataOffset < deIsoEnd)
|
||||
return ReadDecryptedPartitionBytes(pe, partitionDataOffset, length);
|
||||
|
||||
if (pe.DataEntry1 is { NumberOfSectors: > 0 })
|
||||
{
|
||||
long de1Start = (long)pe.DataEntry1.FirstSector * WiiBlockSize;
|
||||
long de1End = de1Start + ((long)pe.DataEntry1.NumberOfSectors * WiiBlockSize);
|
||||
if (absDataOffset >= de1Start && absDataOffset < de1End)
|
||||
return ReadDecryptedPartitionBytes(pe, partitionDataOffset, 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>
|
||||
@@ -349,7 +334,7 @@ namespace SabreTools.Wrappers
|
||||
/// </summary>
|
||||
internal int ReadVirtual(long offset, byte[] buffer, int bufferOffset, int count)
|
||||
{
|
||||
long isoSize = (long)Model.Header1.IsoFileSize;
|
||||
long isoSize = (long)Header1.IsoFileSize;
|
||||
if (offset >= isoSize || count <= 0)
|
||||
return 0;
|
||||
|
||||
@@ -382,32 +367,34 @@ namespace SabreTools.Wrappers
|
||||
private int ReadVirtualChunk(long pos, byte[] buffer, int bufferOffset, int count)
|
||||
{
|
||||
// 1. Disc header (first 0x80 bytes stored verbatim in Header2.DiscHeader)
|
||||
if (pos < WiaConstants.DiscHeaderStoredSize && Model.Header2.DiscHeader is { Length: > 0 })
|
||||
if (pos < DiscHeaderStoredSize && Header2.DiscHeader.Length > 0)
|
||||
{
|
||||
int available = (int)Math.Min(WiaConstants.DiscHeaderStoredSize - pos, count);
|
||||
int srcAvail = Math.Min(available, Model.Header2.DiscHeader.Length - (int)pos);
|
||||
int available = (int)Math.Min(DiscHeaderStoredSize - pos, count);
|
||||
int srcAvail = Math.Min(available, Header2.DiscHeader.Length - (int)pos);
|
||||
if (srcAvail > 0)
|
||||
Array.Copy(Model.Header2.DiscHeader, (int)pos, buffer, bufferOffset, srcAvail);
|
||||
Array.Copy(Header2.DiscHeader, (int)pos, buffer, bufferOffset, srcAvail);
|
||||
|
||||
if (available > srcAvail)
|
||||
Array.Clear(buffer, bufferOffset + srcAvail, available - srcAvail);
|
||||
|
||||
return available;
|
||||
}
|
||||
|
||||
uint chunkSize = Model.Header2.ChunkSize;
|
||||
var comp = Model.Header2.CompressionType;
|
||||
byte[] compData = Model.Header2.CompressorData ?? new byte[7];
|
||||
byte compDataSize = Model.Header2.CompressorDataSize;
|
||||
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 (Model.RawDataEntries is { Length: > 0 })
|
||||
if (RawDataEntries.Length > 0)
|
||||
{
|
||||
foreach (var rde in Model.RawDataEntries)
|
||||
foreach (var entry in RawDataEntries)
|
||||
{
|
||||
if (rde.DataSize == 0 || rde.NumberOfGroups == 0)
|
||||
if (entry.DataSize == 0 || entry.NumberOfGroups == 0)
|
||||
continue;
|
||||
|
||||
long rdeStart = (long)rde.DataOffset;
|
||||
long rdeEnd = rdeStart + (long)rde.DataSize;
|
||||
long rdeStart = (long)entry.DataOffset;
|
||||
long rdeEnd = rdeStart + (long)entry.DataSize;
|
||||
if (pos < rdeStart || pos >= rdeEnd)
|
||||
continue;
|
||||
|
||||
@@ -417,10 +404,10 @@ namespace SabreTools.Wrappers
|
||||
uint g = (uint)(adjustedPos / chunkSize);
|
||||
int offsetInGroup = (int)(adjustedPos % chunkSize);
|
||||
|
||||
if (g >= rde.NumberOfGroups)
|
||||
if (g >= entry.NumberOfGroups)
|
||||
continue;
|
||||
|
||||
uint groupFileIdx = rde.GroupIndex + g;
|
||||
uint groupFileIdx = entry.GroupIndex + g;
|
||||
byte[]? groupBytes = GetCachedRawGroup(groupFileIdx, comp, compData, compDataSize, chunkSize);
|
||||
if (groupBytes is null)
|
||||
return 0;
|
||||
@@ -430,6 +417,7 @@ namespace SabreTools.Wrappers
|
||||
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);
|
||||
@@ -443,16 +431,35 @@ namespace SabreTools.Wrappers
|
||||
}
|
||||
|
||||
// 3. Partition data entries (Wii encrypted partition data)
|
||||
if (Model.PartitionEntries is { Length: > 0 })
|
||||
if (PartitionEntries is not null && PartitionEntries.Length > 0)
|
||||
{
|
||||
foreach (var pe in Model.PartitionEntries)
|
||||
foreach (var pe in PartitionEntries)
|
||||
{
|
||||
int r = ReadPartitionChunk(pe.DataEntry0, pe.PartitionKey, pos,
|
||||
buffer, bufferOffset, count, comp, compData, compDataSize, chunkSize);
|
||||
if (r > 0) return r;
|
||||
r = ReadPartitionChunk(pe.DataEntry1, pe.PartitionKey, pos,
|
||||
buffer, bufferOffset, count, comp, compData, compDataSize, chunkSize);
|
||||
if (r > 0) return r;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,23 +477,21 @@ namespace SabreTools.Wrappers
|
||||
if (length <= 0 || pe is null)
|
||||
return null;
|
||||
|
||||
const int WiiBlockSize = 0x8000;
|
||||
const int WiiBlockDataSize = 0x7C00;
|
||||
uint chunkSize = Header2.ChunkSize;
|
||||
var comp = Header2.CompressionType;
|
||||
byte[] compData = Header2.CompressorData ?? new byte[7];
|
||||
byte compDataSize = Header2.CompressorDataSize;
|
||||
int blocksPerGroup = (int)(chunkSize / WiiBlockSize);
|
||||
|
||||
uint chunkSize = Model.Header2.ChunkSize;
|
||||
var comp = Model.Header2.CompressionType;
|
||||
byte[] compData = Model.Header2.CompressorData ?? new byte[7];
|
||||
byte compDataSize = Model.Header2.CompressorDataSize;
|
||||
int blocksPerGroup = (int)(chunkSize / WiiBlockSize);
|
||||
byte[] result = new byte[length];
|
||||
int produced = 0;
|
||||
|
||||
byte[] result = new byte[length];
|
||||
int produced = 0;
|
||||
|
||||
// DataEntry0 covers [0 .. de0.NumberOfSectors * 0x7C00) in partition-data space.
|
||||
// DataEntry1 (if present) immediately follows.
|
||||
// DataEntry0 covers [0 .. de0.NumberOfSectors * 0x7C00) in partition-data space
|
||||
var de0 = pe.DataEntry0;
|
||||
var de1 = pe.DataEntry1;
|
||||
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)
|
||||
@@ -511,24 +516,29 @@ namespace SabreTools.Wrappers
|
||||
break; // beyond available data
|
||||
}
|
||||
|
||||
long blockNum = deRelOff / WiiBlockDataSize;
|
||||
int offsetInBlock = (int)(deRelOff % WiiBlockDataSize);
|
||||
long blockNum = deRelOff / WiiBlockDataSize;
|
||||
int offsetInBlock = (int)(deRelOff % WiiBlockDataSize);
|
||||
long groupRelative = blockNum / blocksPerGroup;
|
||||
int blockInGroup = (int)(blockNum % blocksPerGroup);
|
||||
int blockInGroup = (int)(blockNum % blocksPerGroup);
|
||||
|
||||
if (groupRelative >= de.NumberOfGroups)
|
||||
break;
|
||||
|
||||
uint groupFileIdx = de.GroupIndex + (uint)groupRelative;
|
||||
uint groupFileIdx = de.GroupIndex + (uint)groupRelative;
|
||||
long dataOffsetForLfg = groupRelative * blocksPerGroup * WiiBlockDataSize;
|
||||
|
||||
byte[]? decrypted = ReadDecryptedGroupData(groupFileIdx, comp, compData, compDataSize,
|
||||
blocksPerGroup, WiiBlockDataSize, dataOffsetForLfg);
|
||||
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;
|
||||
int offsetInGroup = (blockInGroup * WiiBlockDataSize) + offsetInBlock;
|
||||
int available = decrypted.Length - offsetInGroup;
|
||||
if (available <= 0)
|
||||
break;
|
||||
|
||||
@@ -545,17 +555,38 @@ namespace SabreTools.Wrappers
|
||||
return null;
|
||||
if (produced < length)
|
||||
Array.Resize(ref result, produced);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private int ReadPartitionChunk(PartitionDataEntry de, byte[] partitionKey, long pos,
|
||||
byte[] buffer, int bufferOffset, int count,
|
||||
WiaRvzCompressionType comp, byte[] compData, byte compDataSize, uint chunkSize)
|
||||
/// <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;
|
||||
|
||||
const int WiiBlockSize = 0x8000;
|
||||
if (chunkSize == 0)
|
||||
return 0;
|
||||
|
||||
@@ -577,8 +608,7 @@ namespace SabreTools.Wrappers
|
||||
return 0;
|
||||
|
||||
uint groupFileIdx = de.GroupIndex + (uint)groupNum;
|
||||
byte[]? encryptedGroup = GetCachedEncGroup(groupFileIdx, de, partitionKey,
|
||||
comp, compData, compDataSize, blocksPerGroup, chunkSize);
|
||||
byte[]? encryptedGroup = GetCachedEncGroup(groupFileIdx, de, partitionKey, comp, compData, compDataSize, blocksPerGroup);
|
||||
if (encryptedGroup is null)
|
||||
return 0;
|
||||
|
||||
@@ -588,6 +618,7 @@ namespace SabreTools.Wrappers
|
||||
return 0;
|
||||
|
||||
long remainingInEntry = isoDataEnd - pos;
|
||||
|
||||
// Stay within this group
|
||||
long groupIsoEnd = isoDataStart + ((groupNum + 1) * blocksPerGroup * WiiBlockSize);
|
||||
long remainingInGroup = groupIsoEnd - pos;
|
||||
@@ -599,8 +630,20 @@ namespace SabreTools.Wrappers
|
||||
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)
|
||||
WiaRvzCompressionType comp,
|
||||
byte[] compData,
|
||||
byte compDataSize,
|
||||
uint chunkSize)
|
||||
{
|
||||
if (_cachedRawGroupIndex == groupFileIdx)
|
||||
return _cachedRawGroup;
|
||||
@@ -611,15 +654,30 @@ namespace SabreTools.Wrappers
|
||||
return group;
|
||||
}
|
||||
|
||||
private byte[]? GetCachedEncGroup(uint groupFileIdx, PartitionDataEntry de, byte[] partitionKey,
|
||||
WiaRvzCompressionType comp, byte[] compData, byte compDataSize, int blocksPerGroup, uint chunkSize)
|
||||
/// <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);
|
||||
byte[]? decrypted = ReadDecryptedGroupData(groupFileIdx, comp, compData, compDataSize, blocksPerGroup, 0x7C00, dataOffsetForLfg);
|
||||
if (decrypted is null)
|
||||
return null;
|
||||
|
||||
@@ -631,75 +689,128 @@ namespace SabreTools.Wrappers
|
||||
|
||||
/// <summary>
|
||||
/// Reads and decompresses one raw (non-partition) group.
|
||||
/// Returns <c>chunkSize</c> bytes of raw ISO data, or null on failure.
|
||||
/// 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)
|
||||
private byte[]? ReadGroupRaw(uint groupIdx,
|
||||
WiaRvzCompressionType comp,
|
||||
byte[] compressorData,
|
||||
byte compressorDataSize,
|
||||
uint chunkSize)
|
||||
{
|
||||
if (IsRvz)
|
||||
{
|
||||
if (Model.RvzGroupEntries is null || groupIdx >= Model.RvzGroupEntries.Length)
|
||||
if (RvzGroupEntries is null || groupIdx >= RvzGroupEntries.Length)
|
||||
return null;
|
||||
var ge = Model.RvzGroupEntries[groupIdx];
|
||||
|
||||
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, (int)dataSize);
|
||||
return DecompressGroupBytes(fileData, 0, (int)dataSize, comp,
|
||||
compressorData, compressorDataSize, (int)chunkSize, IsRvz, isRvzCompressed,
|
||||
ge.RvzPackedSize, groupIdx * chunkSize, false, 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 (Model.GroupEntries is null || groupIdx >= Model.GroupEntries.Length)
|
||||
if (GroupEntries is null || groupIdx >= GroupEntries.Length)
|
||||
return null;
|
||||
var ge = Model.GroupEntries[groupIdx];
|
||||
|
||||
var ge = GroupEntries[groupIdx];
|
||||
if (ge.DataSize == 0)
|
||||
return new byte[chunkSize];
|
||||
byte[] fileData = ReadRangeFromSource((long)ge.DataOffset, (int)ge.DataSize);
|
||||
return DecompressGroupBytes(fileData, 0, (int)ge.DataSize, comp,
|
||||
compressorData, compressorDataSize, (int)chunkSize, false, false,
|
||||
0, 0L, false, 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,
|
||||
private byte[]? ReadDecryptedGroupData(uint groupIdx,
|
||||
WiaRvzCompressionType comp,
|
||||
byte[] compressorData,
|
||||
byte compressorDataSize,
|
||||
int blocksPerGroup,
|
||||
int blockDataSize,
|
||||
long dataOffsetForLfg)
|
||||
{
|
||||
int decryptedGroupSize = blocksPerGroup * blockDataSize;
|
||||
|
||||
if (IsRvz)
|
||||
{
|
||||
if (Model.RvzGroupEntries is null || groupIdx >= Model.RvzGroupEntries.Length)
|
||||
if (RvzGroupEntries is null || groupIdx >= RvzGroupEntries.Length)
|
||||
return null;
|
||||
var ge = Model.RvzGroupEntries[groupIdx];
|
||||
|
||||
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, (int)dataSize);
|
||||
return DecompressGroupBytes(fileData, 0, (int)dataSize, comp,
|
||||
compressorData, compressorDataSize, decryptedGroupSize, IsRvz, isRvzCompressed,
|
||||
ge.RvzPackedSize, dataOffsetForLfg, true,
|
||||
Model.Header2.ChunkSize);
|
||||
|
||||
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 (Model.GroupEntries is null || groupIdx >= Model.GroupEntries.Length)
|
||||
if (GroupEntries is null || groupIdx >= GroupEntries.Length)
|
||||
return null;
|
||||
var ge = Model.GroupEntries[groupIdx];
|
||||
|
||||
var ge = GroupEntries[groupIdx];
|
||||
if (ge.DataSize == 0)
|
||||
return new byte[decryptedGroupSize];
|
||||
byte[] fileData2 = ReadRangeFromSource((long)ge.DataOffset, (int)ge.DataSize);
|
||||
return DecompressGroupBytes(fileData2, 0, (int)ge.DataSize, comp,
|
||||
compressorData, compressorDataSize, decryptedGroupSize, false, false,
|
||||
0, 0L, true,
|
||||
Model.Header2.ChunkSize);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -707,10 +818,18 @@ namespace SabreTools.Wrappers
|
||||
/// 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,
|
||||
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)
|
||||
@@ -734,10 +853,10 @@ namespace SabreTools.Wrappers
|
||||
// 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 != null)
|
||||
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 PurgeDecompressor.Decompress(fileData, purgeStart, purgeLen, expectedSize, exceptionBytes);
|
||||
}
|
||||
@@ -785,6 +904,7 @@ namespace SabreTools.Wrappers
|
||||
int bytesRead = rvzDecomp.Decompress(unpacked, 0, expectedSize);
|
||||
if (bytesRead < expectedSize)
|
||||
Array.Resize(ref unpacked, bytesRead);
|
||||
|
||||
return unpacked;
|
||||
}
|
||||
|
||||
@@ -820,8 +940,10 @@ namespace SabreTools.Wrappers
|
||||
{
|
||||
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;
|
||||
@@ -856,13 +978,10 @@ namespace SabreTools.Wrappers
|
||||
/// </summary>
|
||||
internal static byte[] EncryptWiiGroup(byte[] decryptedData, byte[] key, int blocksPerGroup)
|
||||
{
|
||||
const int WiiBlockSize = 0x8000;
|
||||
const int WiiBlockDataSize = 0x7C00;
|
||||
const int WiiBlockHashSize = 0x0400;
|
||||
const int H0Count = 31;
|
||||
const int H1Count = 8;
|
||||
const int H2Count = 8;
|
||||
const int HashLen = 20;
|
||||
const int HashLen = 20;
|
||||
|
||||
// --- Build H0 / H1 / H2 hash arrays ---
|
||||
byte[][][] h0 = new byte[blocksPerGroup][][];
|
||||
@@ -894,7 +1013,10 @@ namespace SabreTools.Wrappers
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -906,7 +1028,10 @@ namespace SabreTools.Wrappers
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -919,7 +1044,11 @@ namespace SabreTools.Wrappers
|
||||
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; }
|
||||
for (int i = 0; i < H0Count; i++)
|
||||
{
|
||||
Array.Copy(h0[b][i], 0, hashBlock, off, HashLen);
|
||||
off += HashLen;
|
||||
}
|
||||
|
||||
off += 0x14; // padding0
|
||||
|
||||
@@ -927,7 +1056,11 @@ namespace SabreTools.Wrappers
|
||||
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; }
|
||||
for (int i = 0; i < H1Count; i++)
|
||||
{
|
||||
Array.Copy(h1[h1Grp][i], 0, hashBlock, off, HashLen);
|
||||
off += HashLen;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -964,16 +1097,19 @@ namespace SabreTools.Wrappers
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] ComputeSha1(byte[] data, int offset, int count)
|
||||
{
|
||||
if (count == 0)
|
||||
return new byte[20];
|
||||
/// <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];
|
||||
}
|
||||
using var sha1 = new HashWrapper(HashType.SHA1);
|
||||
sha1.Process(data, offset, count);
|
||||
sha1.Terminate();
|
||||
return sha1.CurrentHashBytes ?? new byte[20];
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user