Files
SabreTools.Serialization/SabreTools.Wrappers/WIA.Writing.cs
Matt Nadareski 249c51933b Update packages
2026-05-14 09:25:26 -04:00

1855 lines
69 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
using System.Threading.Tasks;
#endif
using SabreTools.Data.Extensions;
using SabreTools.Data.Models.NintendoDisc;
using SabreTools.Data.Models.WIA;
using SabreTools.Hashing;
using SabreTools.Numerics.Extensions;
using SabreTools.Security.Cryptography;
using static SabreTools.Data.Models.NintendoDisc.Constants;
using static SabreTools.Data.Models.WIA.Constants;
namespace SabreTools.Wrappers
{
public partial class WIA : IWritable
{
/// <inheritdoc/>
public bool Write(string outputPath, bool includeDebug)
{
if (string.IsNullOrEmpty(outputPath))
{
string ext = IsRvz ? ".rvz" : ".wia";
string outputFilename = Filename is null
? (Guid.NewGuid().ToString() + ext)
: (Filename + ".new");
outputPath = Path.GetFullPath(outputFilename);
}
var writer = new Serialization.Writers.WIA { Debug = includeDebug };
return writer.SerializeFile(Model, outputPath);
}
/// <summary>
/// Compress a <see cref="NintendoDisc"/> wrapper to a WIA or RVZ file.
/// </summary>
public static bool ConvertFromDisc(NintendoDisc source,
string outputPath,
bool isRvz = false,
WiaRvzCompressionType compressionType = WiaRvzCompressionType.None,
int compressionLevel = 5,
uint chunkSize = DefaultChunkSize)
{
if (string.IsNullOrEmpty(outputPath))
return false;
if (!isRvz && chunkSize != DefaultChunkSize)
return false;
if (isRvz && compressionType == WiaRvzCompressionType.Purge)
return false;
try
{
using var fs = File.Open(outputPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
return WriteWiaRvz(source, fs, isRvz, compressionType, Math.Max(1, Math.Min(22, compressionLevel)), chunkSize);
}
catch
{
return false;
}
}
/// <summary>
/// Compress a <see cref="NintendoDisc"/> wrapper to a WIA or RVZ stream.
/// Unlike <see cref="ConvertFromDisc"/>, exceptions are surfaced via
/// <paramref name="exception"/> rather than silently swallowed.
/// </summary>
/// <param name="exception">Set to the exception thrown on failure, or null on success.</param>
/// TODO: Determine why the exception can't just be thrown
public static bool ConvertFromDiscToStream(NintendoDisc source,
Stream dest,
bool isRvz,
WiaRvzCompressionType compressionType,
int compressionLevel,
uint chunkSize,
out Exception? exception)
{
exception = null;
if (source is null)
{
exception = new ArgumentNullException(nameof(source));
return false;
}
if (dest is null)
{
exception = new ArgumentNullException(nameof(dest));
return false;
}
if (!isRvz && chunkSize != DefaultChunkSize)
{
exception = new ArgumentException("WIA chunkSize must equal DefaultChunkSize");
return false;
}
if (isRvz && compressionType == WiaRvzCompressionType.Purge)
{
exception = new ArgumentException("RVZ does not support Purge compression");
return false;
}
try
{
return WriteWiaRvz(source, dest, isRvz, compressionType, Math.Max(1, Math.Min(22, compressionLevel)), chunkSize);
}
catch (Exception ex)
{
exception = ex;
return false;
}
}
/// <summary>
/// Core pipeline for writing WIA and RVZ images
/// </summary>
/// <param name="source"></param>
/// <param name="dest"></param>
/// <param name="isRvz"></param>
/// <param name="compressionType"></param>
/// <param name="compressionLevel"></param>
/// <param name="chunkSize"></param>
/// <returns></returns>
private static bool WriteWiaRvz(NintendoDisc source,
Stream dest,
bool isRvz,
WiaRvzCompressionType compressionType,
int compressionLevel,
uint chunkSize)
{
long isoSize = source.DataLength;
if (isoSize <= 0)
return false;
byte[]? discHdr = source.ReadData(0, DiscHeaderStoredSize);
if (discHdr is null)
return false;
Platform platform = discHdr.GetPlatform();
return platform switch
{
Platform.GameCube => WriteGameCube(source, dest, isRvz, compressionType, compressionLevel, chunkSize, isoSize, discHdr),
Platform.Wii => WriteWii(source, dest, isRvz, compressionType, compressionLevel, chunkSize, isoSize, discHdr),
// These should never happen
Platform.Unknown => false,
_ => false,
};
}
/// <summary>
/// Write a GameCube WIA or RVZ image
/// </summary>
/// <param name="source"></param>
/// <param name="dest"></param>
/// <param name="isRvz"></param>
/// <param name="compressionType"></param>
/// <param name="compressionLevel"></param>
/// <param name="chunkSize"></param>
/// <param name="isoSize"></param>
/// <param name="discHdr"></param>
/// <returns></returns>
private static bool WriteGameCube(NintendoDisc source,
Stream dest,
bool isRvz,
WiaRvzCompressionType compressionType,
int compressionLevel,
uint chunkSize,
long isoSize,
byte[] discHdr)
{
const long rawDataStart = DiscHeaderStoredSize;
long rawDataSize = isoSize - rawDataStart;
if (rawDataSize <= 0)
return false;
uint numGroups = (uint)((rawDataSize + chunkSize - 1) / chunkSize);
int groupEntrySize = isRvz ? RvzGroupEntrySize : WiaGroupEntrySize;
long headersBound = Align(
Header1Size + Header2Size + RawDataEntrySize + 0x100 + (numGroups * groupEntrySize),
WiiBlockSize);
dest.Write(new byte[headersBound], 0, (int)headersBound);
long bytesWritten = headersBound;
var groupEntries = new RvzGroupEntry[numGroups];
var rawDedupMap = new Dictionary<WiaDedupKey2, RvzGroupEntry>();
IO.Compression.RVZPack.FileSystemTableReader? gcFst = isRvz ? BuildFileSystemTableReader(source) : null;
GetCompressorData(compressionType, compressionLevel, out byte[] propData, out byte propSize);
uint groupIdx = 0;
long srcOff = rawDataStart;
long remaining = rawDataSize;
int batchSize = Math.Max(Environment.ProcessorCount * 4, 64);
while (remaining > 0)
{
int thisBatch = (int)Math.Min(batchSize, (remaining + chunkSize - 1) / chunkSize);
var work = new GcGroupWorkEntry[thisBatch];
int actualBatch = 0;
for (int i = 0; i < thisBatch && remaining > 0; i++)
{
int toRead = (int)Math.Min(chunkSize, remaining);
byte[] raw = source.ReadData(srcOff, toRead);
if (raw.Length == 0)
break;
var gi = work[i] = new GcGroupWorkEntry
{
BytesRead = toRead,
SourceOffset = srcOff,
};
srcOff += toRead;
remaining -= toRead;
actualBatch++;
gi.IsAllSame = IsAllSame(raw, toRead);
gi.SameByte = raw[0];
if (gi.IsAllSame)
{
var dk = new WiaDedupKey2(gi.SameByte, toRead);
if (rawDedupMap.TryGetValue(dk, out var cached))
{
gi.IsDedupHit = true;
gi.DedupEntry = cached;
continue;
}
if (gi.SameByte == 0)
continue;
}
if (isRvz)
{
byte[]? packed = IO.Compression.RVZPack.Compressor.Pack(raw, 0, toRead, srcOff - toRead, out gi.RvzPackedSize, gcFst);
gi.MainData = packed ?? raw;
if (packed is null)
gi.RvzPackedSize = 0;
}
else
{
gi.MainData = raw;
}
}
if (actualBatch == 0)
break;
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
if (compressionType > WiaRvzCompressionType.Purge)
{
WiaRvzCompressionType ct = compressionType;
int cl = compressionLevel;
byte[] pd = propData;
byte ps = propSize;
Parallel.For(0, actualBatch, w =>
{
var gi = work[w];
if (gi.MainData is not null && !gi.IsDedupHit)
gi.CompressedData = Compress(ct, gi.MainData, 0, gi.MainData.Length, cl, pd, ps);
});
}
#endif
for (int i = 0; i < actualBatch; i++)
{
uint idx = groupIdx + (uint)i;
var gi = work[i];
if (gi.IsDedupHit)
{
groupEntries[idx] = gi.DedupEntry;
}
else if (gi.IsAllSame && gi.SameByte == 0)
{
var dk = new WiaDedupKey2(0, gi.BytesRead);
if (!rawDedupMap.TryGetValue(dk, out var ze))
{
ze = new RvzGroupEntry
{
DataOffset = (uint)(bytesWritten >> 2),
DataSize = 0,
RvzPackedSize = 0
};
rawDedupMap[dk] = ze;
}
groupEntries[idx] = ze;
}
else if (gi.MainData is not null)
{
uint groupOff = (uint)(bytesWritten >> 2);
uint storedSz = WriteRawGroupData(dest, ref bytesWritten, gi, isRvz, compressionType);
PadToFourBytes(dest, ref bytesWritten);
var entry = new RvzGroupEntry
{
DataOffset = groupOff,
DataSize = storedSz,
RvzPackedSize = gi.RvzPackedSize
};
groupEntries[idx] = entry;
if (gi.IsAllSame && gi.SameByte != 0)
rawDedupMap[new WiaDedupKey2(gi.SameByte, gi.BytesRead)] = entry;
}
}
groupIdx += (uint)actualBatch;
}
// Write tables
dest.Seek(Header1Size + Header2Size, SeekOrigin.Begin);
long tablePos = Header1Size + Header2Size;
ulong rawEntriesOffset = (ulong)tablePos;
var rawEntry = new RawDataEntry
{
DataOffset = DiscHeaderStoredSize,
DataSize = (ulong)rawDataSize,
GroupIndex = 0,
NumberOfGroups = numGroups,
};
byte[] rawEntryBytes = SerializeRawDataEntry(rawEntry);
byte[] rawEntryWritten = CompressTableDataWia(rawEntryBytes, compressionType, compressionLevel, propData, propSize);
dest.Write(rawEntryWritten, 0, rawEntryWritten.Length);
tablePos += rawEntryWritten.Length;
PadTableToFourBytes(dest, ref tablePos);
ulong groupEntriesOffset = (ulong)tablePos;
byte[] groupEntryBytes = SerializeGroupEntries(groupEntries, numGroups, isRvz);
byte[] groupEntryWritten = CompressTableDataWia(groupEntryBytes, compressionType, compressionLevel, propData, propSize);
dest.Write(groupEntryWritten, 0, groupEntryWritten.Length);
tablePos += groupEntryWritten.Length;
WriteWiaHeaders(dest,
discHdr,
isRvz,
WiaDiscType.GameCube,
compressionType,
compressionLevel,
chunkSize,
0,
(ulong)tablePos,
new byte[20], // no partition entries
1u,
rawEntriesOffset,
(uint)rawEntryWritten.Length,
numGroups,
groupEntriesOffset,
(uint)groupEntryWritten.Length,
propData,
propSize,
isoSize,
bytesWritten);
dest.Flush();
return true;
}
/// <summary>
/// Write a Wii WIA or RVZ image
/// </summary>
/// <param name="source"></param>
/// <param name="dest"></param>
/// <param name="isRvz"></param>
/// <param name="compressionType"></param>
/// <param name="compressionLevel"></param>
/// <param name="chunkSize"></param>
/// <param name="isoSize"></param>
/// <param name="discHdr"></param>
/// <returns></returns>
private static bool WriteWii(NintendoDisc source,
Stream dest,
bool isRvz,
WiaRvzCompressionType compressionType,
int compressionLevel,
uint chunkSize,
long isoSize,
byte[] discHdr)
{
var partitions = ReadWiiPartitions(source);
if (partitions is null)
return false;
var rawRegions = BuildRawRegions(partitions, isoSize);
GetCompressorData(compressionType, compressionLevel, out byte[] propData, out byte propSize);
int groupEntrySize = isRvz ? RvzGroupEntrySize : WiaGroupEntrySize;
uint totalGroups = CalcTotalGroups(partitions, rawRegions, chunkSize);
long headersBound = Align(
Header1Size + Header2Size + (partitions.Count * PartitionEntrySize) + (rawRegions.Count * RawDataEntrySize) + 0x100 + (totalGroups * groupEntrySize),
WiiBlockSize);
dest.Write(new byte[headersBound], 0, (int)headersBound);
long bytesWritten = headersBound;
var allGroups = new List<RvzGroupEntry>();
uint currentGrpIdx = 0;
uint lastValidOff = 0;
var dedupMap = new Dictionary<WiaDedupKey3, WiaDedup2>();
var decDedupMap = new Dictionary<WiaDedupKey3, WiaDedup2>();
var rawDedupMap = new Dictionary<WiaDedupKey2, RvzGroupEntry>();
var wiaZeroDedup = new Dictionary<ulong, uint>();
var regions = BuildDiscRegions(partitions, rawRegions);
foreach (var region in regions)
{
if (region.IsPartition)
{
ProcessWiiPartition(source,
dest,
region.PartitionInfo!,
ref bytesWritten,
allGroups,
ref currentGrpIdx,
ref lastValidOff,
dedupMap,
decDedupMap,
wiaZeroDedup,
isRvz,
compressionType,
compressionLevel,
chunkSize,
propData,
propSize);
}
else
{
ProcessRawRegion(source,
dest,
region.RawInfo!,
ref bytesWritten,
allGroups,
ref currentGrpIdx,
ref lastValidOff,
rawDedupMap,
isRvz,
compressionType,
compressionLevel,
chunkSize,
propData,
propSize);
}
}
// Write tables
dest.Seek(Header1Size + Header2Size, SeekOrigin.Begin);
long tablePos = Header1Size + Header2Size;
ulong partEntriesOffset = (ulong)tablePos;
byte[] partEntriesBytes = SerializePartitionEntries(dest, partitions);
tablePos += partEntriesBytes.Length;
PadTableToFourBytes(dest, ref tablePos);
ulong rawEntriesOffset = (ulong)tablePos;
byte[] rawEntryBytes = SerializeRawDataEntries(rawRegions);
byte[] rawEntryWritten = CompressTableDataWia(rawEntryBytes, compressionType, compressionLevel, propData, propSize);
dest.Write(rawEntryWritten, 0, rawEntryWritten.Length);
tablePos += rawEntryWritten.Length;
PadTableToFourBytes(dest, ref tablePos);
ulong groupEntriesOffset = (ulong)tablePos;
using (var gms = new MemoryStream())
{
foreach (var e in allGroups)
{
WriteGroupEntryWia(gms, e, isRvz);
}
byte[] gBytes = gms.ToArray();
byte[] gWritten = CompressTableDataWia(gBytes, compressionType, compressionLevel, propData, propSize);
dest.Write(gWritten, 0, gWritten.Length);
tablePos += gWritten.Length;
byte[] partHashData = ComputeSha1Wia(partEntriesBytes, 0, partEntriesBytes.Length);
WriteWiaHeaders(dest,
discHdr,
isRvz,
WiaDiscType.Wii,
compressionType,
compressionLevel,
chunkSize,
(uint)partitions.Count, partEntriesOffset,
partHashData,
(uint)rawRegions.Count,
rawEntriesOffset,
(uint)rawEntryWritten.Length,
(uint)allGroups.Count,
groupEntriesOffset,
(uint)gWritten.Length,
propData,
propSize,
isoSize,
bytesWritten);
}
dest.Flush();
return true;
}
/// <summary>
/// Wii partition processing
/// </summary>
/// <param name="source"></param>
/// <param name="dest"></param>
/// <param name="part"></param>
/// <param name="bytesWritten"></param>
/// <param name="groupEntries"></param>
/// <param name="currentGrpIdx"></param>
/// <param name="lastValidOff"></param>
/// <param name="dedupMap"></param>
/// <param name="decDedupMap"></param>
/// <param name="wiaZeroDedup"></param>
/// <param name="isRvz"></param>
/// <param name="compressionType"></param>
/// <param name="compressionLevel"></param>
/// <param name="chunkSize"></param>
/// <param name="propData"></param>
/// <param name="propSize"></param>
private static void ProcessWiiPartition(NintendoDisc source,
Stream dest,
WiiPartInfo part,
ref long bytesWritten,
List<RvzGroupEntry> groupEntries,
ref uint currentGrpIdx,
ref uint lastValidOff,
Dictionary<WiaDedupKey3, WiaDedup2> dedupMap,
Dictionary<WiaDedupKey3, WiaDedup2> decDedupMap,
Dictionary<ulong, uint> wiaZeroDedup,
bool isRvz,
WiaRvzCompressionType compressionType,
int compressionLevel,
uint chunkSize,
byte[] propData,
byte propSize)
{
long remaining = (long)part.DataSize;
long srcOff = (long)part.DataStart;
ulong partKeyHash = BitConverter.ToUInt64(part.TitleKey, 0)
^ BitConverter.ToUInt64(part.TitleKey, 8);
part.FirstGroupIndex = currentGrpIdx;
int blocksPerChunk = (int)chunkSize / WiiBlockSize;
int chunksPerGroup = WiiBlocksPerGroup / blocksPerChunk;
int wiiGroupSize = WiiGroupSize;
int outerBatch = (chunksPerGroup == 1)
? Math.Max(Environment.ProcessorCount * 2, 16)
: 1;
var batchItems = new WiiBatchItem[outerBatch];
var flatWork = new List<WiaFlatWorkItem>(outerBatch);
long regionDecOff = 0;
while (remaining > 0)
{
int actualBatch = 0;
flatWork.Clear();
for (int b = 0; b < outerBatch && remaining > 0; b++)
{
int toRead = (int)Math.Min(wiiGroupSize, remaining);
byte[]? encGroup = source.ReadData(srcOff, toRead);
if (encGroup is null) break;
var item = batchItems[b] = new WiiBatchItem
{
BytesRead = toRead,
SrcOffset = srcOff,
};
srcOff += toRead;
remaining -= toRead;
actualBatch++;
bool encAllSame = (chunksPerGroup == 1) && IsAllSame(encGroup, toRead);
item.EncAllSame = encAllSame;
item.DedupKey = new WiaDedupKey3(partKeyHash, encGroup[0], toRead);
if (encAllSame && dedupMap.TryGetValue(item.DedupKey, out var reused))
{
item.IsInterDedupHit = true;
item.DedupResult = reused;
regionDecOff += (long)(toRead / WiiBlockSize) * WiiBlockDataSize;
continue;
}
int numBlocks = toRead / WiiBlockSize;
item.NumChunks = (numBlocks + blocksPerChunk - 1) / blocksPerChunk;
item.DecryptedAll = DecryptWiiGroup(encGroup, toRead, part.TitleKey);
item.AllExceptions = GenerateHashExceptions(encGroup, item.DecryptedAll, part.TitleKey, numBlocks);
item.PartWork = new WiiChunkWork[item.NumChunks];
for (int c = 0; c < item.NumChunks; c++)
{
int cBlockStart = c * blocksPerChunk;
int cBlockEnd = Math.Min(cBlockStart + blocksPerChunk, numBlocks);
int actualBlocks = cBlockEnd - cBlockStart;
int decOff = cBlockStart * WiiBlockDataSize;
int decLen = actualBlocks * WiiBlockDataSize;
byte[] procData = new byte[decLen];
if (item.DecryptedAll is not null && decLen > 0)
Array.Copy(item.DecryptedAll, decOff, procData, 0, decLen);
var chunkEx = new List<HashExceptionEntry>();
if (item.AllExceptions is not null)
{
foreach (var ex in item.AllExceptions)
{
int exBlock = ex.Offset / WiiBlockHeaderSize;
if (exBlock >= cBlockStart && exBlock < cBlockEnd)
{
int localBlock = exBlock - cBlockStart;
ushort localOff = (ushort)((localBlock * WiiBlockHeaderSize) + (ex.Offset % WiiBlockHeaderSize));
chunkEx.Add(new HashExceptionEntry { Offset = localOff, Hash = ex.Hash });
}
}
}
bool isAllZeros = !isRvz
&& chunksPerGroup == 1
&& chunkEx.Count == 0
&& procData.Length > 0
&& IsAllSame(procData, procData.Length)
&& procData[0] == 0;
bool decAllSame = !isRvz
&& !isAllZeros
&& chunksPerGroup == 1
&& chunkEx.Count == 0
&& procData.Length > 0
&& IsAllSame(procData, procData.Length);
var decDedupKey = new WiaDedupKey3(partKeyHash, procData.Length > 0 ? procData[0] : (byte)0, procData.Length);
var pw = item.PartWork[c] = new WiiChunkWork
{
IsAllZeros = isAllZeros,
DecAllSame = decAllSame,
DecDedupKey = decDedupKey,
};
if (isAllZeros)
continue;
if (decAllSame && decDedupMap.TryGetValue(decDedupKey, out var decReused))
{
pw.IsDecDedupHit = true;
pw.DecDedupOffset = decReused.Offset;
pw.DecDedupDataSize = decReused.DataSize;
continue;
}
byte[] exListBytes = BuildExceptionList(chunkEx);
int unpaddedExLen = 2 + (chunkEx.Count * 22);
byte[] mainData;
uint rvzPackedSize = 0;
if (isRvz)
{
long baseDecOff = regionDecOff + ((long)c * (blocksPerChunk * WiiBlockDataSize));
byte[]? packed = IO.Compression.RVZPack.Compressor.Pack(procData, 0, procData.Length, baseDecOff, out rvzPackedSize);
mainData = packed ?? procData;
if (packed is null)
rvzPackedSize = 0;
}
else
{
mainData = procData;
}
pw.ExceptionListBytes = exListBytes;
pw.UnpaddedExLen = unpaddedExLen;
pw.MainDataBytes = mainData;
pw.RvzPackedSize = rvzPackedSize;
if (compressionType != WiaRvzCompressionType.None)
flatWork.Add(new WiaFlatWorkItem(b, c));
}
regionDecOff += (long)numBlocks * WiiBlockDataSize;
}
if (actualBatch == 0)
break;
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
// Phase 2: compress
if (flatWork.Count > 0)
{
int cl = compressionLevel;
byte[] pd = propData;
byte ps = propSize;
Parallel.For(0, flatWork.Count, idx =>
{
var fw = flatWork[idx];
var pw = batchItems[fw.BatchIndex].PartWork![fw.ChunkIndex];
if (compressionType > WiaRvzCompressionType.Purge)
{
byte[] toCompress = ConcatBytes(
pw.ExceptionListBytes,
0,
pw.UnpaddedExLen,
pw.MainDataBytes,
0,
pw.MainDataBytes.Length);
pw.CompressedData = Compress(compressionType, toCompress, 0, toCompress.Length, cl, pd, ps);
}
else if (compressionType == WiaRvzCompressionType.Purge)
{
pw.CompressedData = IO.Compression.PURGE.Compressor.Compress(pw.MainDataBytes, 0, pw.MainDataBytes.Length, pw.ExceptionListBytes);
}
});
}
#endif
// Phase 3: write
for (int b = 0; b < actualBatch; b++)
{
var item = batchItems[b];
if (item.IsInterDedupHit)
{
lastValidOff = item.DedupResult.Offset;
groupEntries.Add(new RvzGroupEntry
{
DataOffset = item.DedupResult.Offset,
DataSize = item.DedupResult.DataSize,
RvzPackedSize = 0
});
currentGrpIdx++;
continue;
}
for (int c = 0; c < item.NumChunks; c++)
{
var pw = item.PartWork![c];
if (pw.IsAllZeros)
{
uint wouldBeOff = (uint)(bytesWritten >> 2);
if (!wiaZeroDedup.TryGetValue(partKeyHash, out uint firstOff))
{
firstOff = wouldBeOff;
wiaZeroDedup[partKeyHash] = firstOff;
}
groupEntries.Add(new RvzGroupEntry
{
DataOffset = firstOff,
DataSize = 0,
RvzPackedSize = 0,
});
}
else if (pw.IsDecDedupHit)
{
groupEntries.Add(new RvzGroupEntry
{
DataOffset = pw.DecDedupOffset,
DataSize = pw.DecDedupDataSize,
RvzPackedSize = 0,
});
}
else
{
uint groupOff = (uint)(bytesWritten >> 2);
lastValidOff = groupOff;
uint storedSz = WriteWiiChunkData(dest, ref bytesWritten, pw, isRvz, compressionType);
groupEntries.Add(new RvzGroupEntry
{
DataOffset = groupOff,
DataSize = storedSz,
RvzPackedSize = pw.RvzPackedSize,
});
if (item.EncAllSame && c == 0)
dedupMap[item.DedupKey] = new WiaDedup2(groupOff, storedSz);
if (pw.DecAllSame && c == 0)
decDedupMap[pw.DecDedupKey] = new WiaDedup2(groupOff, storedSz);
PadToFourBytes(dest, ref bytesWritten);
}
}
currentGrpIdx++;
}
}
part.NumberOfGroups = currentGrpIdx - part.FirstGroupIndex;
}
/// <summary>
///
/// </summary>
/// <param name="dest"></param>
/// <param name="bytesWritten"></param>
/// <param name="pw"></param>
/// <param name="isRvz"></param>
/// <param name="compressionType"></param>
/// <returns></returns>
private static uint WriteWiiChunkData(Stream dest,
ref long bytesWritten,
WiiChunkWork pw,
bool isRvz,
WiaRvzCompressionType compressionType)
{
if (pw.CompressedData is not null)
{
bool useC = !isRvz || pw.CompressedData.Length < pw.MainDataBytes.Length;
if (useC && compressionType > WiaRvzCompressionType.Purge)
{
dest.Write(pw.CompressedData, 0, pw.CompressedData.Length);
bytesWritten += pw.CompressedData.Length;
return isRvz
? (uint)pw.CompressedData.Length | 0x80000000u
: (uint)pw.CompressedData.Length;
}
if (compressionType == WiaRvzCompressionType.Purge)
{
dest.Write(pw.ExceptionListBytes, 0, pw.ExceptionListBytes.Length);
bytesWritten += pw.ExceptionListBytes.Length;
dest.Write(pw.CompressedData, 0, pw.CompressedData.Length);
bytesWritten += pw.CompressedData.Length;
return (uint)(pw.ExceptionListBytes.Length + pw.CompressedData.Length);
}
}
dest.Write(pw.ExceptionListBytes, 0, pw.ExceptionListBytes.Length);
bytesWritten += pw.ExceptionListBytes.Length;
dest.Write(pw.MainDataBytes, 0, pw.MainDataBytes.Length);
bytesWritten += pw.MainDataBytes.Length;
return (uint)(pw.ExceptionListBytes.Length + pw.MainDataBytes.Length);
}
/// <summary>
/// Raw region processing
/// </summary>
/// <param name="source"></param>
/// <param name="dest"></param>
/// <param name="raw"></param>
/// <param name="bytesWritten"></param>
/// <param name="groupEntries"></param>
/// <param name="currentGrpIdx"></param>
/// <param name="lastValidOff"></param>
/// <param name="rawDedupMap"></param>
/// <param name="isRvz"></param>
/// <param name="compressionType"></param>
/// <param name="compressionLevel"></param>
/// <param name="chunkSize"></param>
/// <param name="propData"></param>
/// <param name="propSize"></param>
private static void ProcessRawRegion(NintendoDisc source,
Stream dest,
RawDataEntry raw,
ref long bytesWritten,
List<RvzGroupEntry> groupEntries,
ref uint currentGrpIdx,
ref uint lastValidOff,
Dictionary<WiaDedupKey2, RvzGroupEntry> rawDedupMap,
bool isRvz,
WiaRvzCompressionType compressionType,
int compressionLevel,
uint chunkSize,
byte[] propData,
byte propSize)
{
raw.GroupIndex = currentGrpIdx;
long skip = (long)raw.DataOffset % WiiBlockSize;
long adjOffset = (long)raw.DataOffset - skip;
long remaining = (long)raw.DataSize + skip;
long srcOff = adjOffset;
while (remaining > 0)
{
int toRead = (int)Math.Min(chunkSize, remaining);
byte[]? data = source.ReadData(srcOff, toRead);
if (data is null)
break;
bool isAllSame = IsAllSame(data, toRead);
byte sameByte = data[0];
if (isAllSame)
{
var dk = new WiaDedupKey2(sameByte, toRead);
if (rawDedupMap.TryGetValue(dk, out var cached))
{
groupEntries.Add(cached);
currentGrpIdx++;
srcOff += toRead;
remaining -= toRead;
continue;
}
if (sameByte == 0)
{
var ze = new RvzGroupEntry
{
DataOffset = (uint)(bytesWritten >> 2),
DataSize = 0,
RvzPackedSize = 0,
};
rawDedupMap[dk] = ze;
groupEntries.Add(ze);
currentGrpIdx++;
srcOff += toRead;
remaining -= toRead;
continue;
}
}
byte[] mainData;
uint rvzPackedSize = 0;
if (isRvz)
{
byte[]? packed = IO.Compression.RVZPack.Compressor.Pack(data, 0, toRead, srcOff, out rvzPackedSize);
mainData = packed ?? data;
if (packed is null)
rvzPackedSize = 0;
}
else
{
mainData = data;
}
byte[]? compressed = null;
if (compressionType > WiaRvzCompressionType.Purge)
{
byte[] c2 = Compress(compressionType, mainData, 0, mainData.Length, compressionLevel, propData, propSize);
if (!isRvz || c2.Length < mainData.Length)
compressed = c2;
}
else if (compressionType == WiaRvzCompressionType.Purge)
{
compressed = IO.Compression.PURGE.Compressor.Compress(mainData, 0, mainData.Length);
}
uint groupOff = (uint)(bytesWritten >> 2);
lastValidOff = groupOff;
uint storedSz;
if (compressed is not null)
{
bool useC = !isRvz || compressed.Length < mainData.Length;
if (useC)
{
dest.Write(compressed, 0, compressed.Length);
bytesWritten += compressed.Length;
storedSz = isRvz
? (uint)compressed.Length | 0x80000000u
: (uint)compressed.Length;
}
else
{
dest.Write(mainData, 0, mainData.Length);
bytesWritten += mainData.Length;
storedSz = (uint)mainData.Length;
}
}
else
{
dest.Write(mainData, 0, mainData.Length);
bytesWritten += mainData.Length;
storedSz = (uint)mainData.Length;
}
PadToFourBytes(dest, ref bytesWritten);
var entry = new RvzGroupEntry
{
DataOffset = groupOff,
DataSize = storedSz,
RvzPackedSize = rvzPackedSize,
};
groupEntries.Add(entry);
if (isAllSame && sameByte != 0)
rawDedupMap[new WiaDedupKey2(sameByte, toRead)] = entry;
currentGrpIdx++;
srcOff += toRead;
remaining -= toRead;
}
raw.NumberOfGroups = currentGrpIdx - raw.GroupIndex;
}
#region Wii Crypto Helpers
/// <summary>
///
/// </summary>
/// <param name="encGroup"></param>
/// <param name="bytesRead"></param>
/// <param name="titleKey"></param>
/// <returns></returns>
private static byte[]? DecryptWiiGroup(byte[] encGroup, int bytesRead, byte[] titleKey)
{
int numBlocks = bytesRead / WiiBlockSize;
var result = new byte[numBlocks * WiiBlockDataSize];
for (int i = 0; i < numBlocks; i++)
{
int off = i * WiiBlockSize;
byte[] iv = new byte[16];
Array.Copy(encGroup, off + 0x3D0, iv, 0, 16);
byte[] encData = new byte[WiiBlockDataSize];
Array.Copy(encGroup, off + WiiBlockHeaderSize, encData, 0, WiiBlockDataSize);
byte[]? dec = WiiDecrypter.DecryptBlock(encData, titleKey, iv);
if (dec is null)
return null;
Array.Copy(dec, 0, result, i * WiiBlockDataSize, WiiBlockDataSize);
}
return result;
}
/// <summary>
///
/// </summary>
/// <param name="encGroup"></param>
/// <param name="decryptedData"></param>
/// <param name="titleKey"></param>
/// <param name="numBlocks"></param>
/// <returns></returns>
private static List<HashExceptionEntry> GenerateHashExceptions(byte[] encGroup,
byte[]? decryptedData,
byte[] titleKey,
int numBlocks)
{
var exceptions = new List<HashExceptionEntry>();
if (decryptedData is null)
return exceptions;
// Re-encrypt the decrypted data to obtain recomputed hashes
byte[] reEncGroup = EncryptWiiGroup(decryptedData, titleKey, numBlocks);
for (int blockIdx = 0; blockIdx < numBlocks; blockIdx++)
{
int blockOff = blockIdx * WiiBlockSize;
byte[] encHashBlock = new byte[WiiBlockHeaderSize];
Array.Copy(encGroup, blockOff, encHashBlock, 0, WiiBlockHeaderSize);
byte[] origHash = AESCBC.Decrypt(encHashBlock, titleKey, new byte[16]) ?? new byte[WiiBlockHeaderSize];
byte[] reEncHashBlock = new byte[WiiBlockHeaderSize];
Array.Copy(reEncGroup, blockOff, reEncHashBlock, 0, WiiBlockHeaderSize);
byte[] recompHash = AESCBC.Decrypt(reEncHashBlock, titleKey, new byte[16]) ?? new byte[WiiBlockHeaderSize];
for (int off = 0; off < WiiBlockHeaderSize; off += 20)
{
bool match = true;
for (int j = 0; j < 20 && (off + j) < WiiBlockHeaderSize; j++)
{
if (origHash[off + j] != recompHash[off + j])
{
match = false;
break;
}
}
if (!match)
{
byte[] hash = new byte[20];
Array.Copy(origHash, off, hash, 0, Math.Min(20, WiiBlockHeaderSize - off));
exceptions.Add(new HashExceptionEntry
{
Offset = (ushort)((blockIdx * WiiBlockHeaderSize) + off),
Hash = hash,
});
}
}
}
return exceptions;
}
/// <summary>
///
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
private static List<WiiPartInfo>? ReadWiiPartitions(NintendoDisc source)
{
var result = new List<WiiPartInfo>();
for (int group = 0; group < WiiPartitionGroupCount; group++)
{
byte[]? gEntry = source.ReadData(WiiPartitionTableAddress + (group * 8), 8);
if (gEntry is null)
continue;
int countPos = 0, offsetPos = 4;
uint count = gEntry.ReadUInt32BigEndian(ref countPos);
uint offset = gEntry.ReadUInt32BigEndian(ref offsetPos) << 2;
if (count == 0 || offset == 0)
continue;
for (int i = 0; i < count; i++)
{
byte[]? pEntry = source.ReadData(offset + (i * 8), 8);
if (pEntry is null)
continue;
int partOffPos = 0;
long partOff = (long)pEntry.ReadUInt32BigEndian(ref partOffPos) << 2;
byte[]? sigType = source.ReadData(partOff, 4);
int sigTypePos = 0;
if (sigType is null || sigType.ReadUInt32BigEndian(ref sigTypePos) != 0x10001U)
continue;
byte[]? hdr = source.ReadData(partOff, 0x2C0);
if (hdr is null)
continue;
byte[] encKey = new byte[16];
Array.Copy(hdr, 0x1BF, encKey, 0, 16);
byte[] titleId = new byte[8];
Array.Copy(hdr, 0x1DC, titleId, 0, 8);
byte ckIdx = hdr[0x1F1];
byte[]? titleKey = source.WiiDecrypter.DecryptTitleKey(encKey, titleId, ckIdx);
if (titleKey is null)
continue;
int dataOffPos = 0x2B8, dataSzPos = 0x2BC;
ulong dataOff = (ulong)hdr.ReadUInt32BigEndian(ref dataOffPos) << 2;
ulong dataSize = (ulong)hdr.ReadUInt32BigEndian(ref dataSzPos) << 2;
result.Add(new WiiPartInfo
{
PartitionOffset = (ulong)partOff,
TitleKey = titleKey,
DataOffset = dataOff,
DataSize = dataSize,
DataStart = (ulong)partOff + dataOff,
DataEnd = (ulong)partOff + dataOff + dataSize,
});
}
}
return result.Count == 0 ? null : result;
}
/// <summary>
///
/// </summary>
/// <param name="partitions"></param>
/// <param name="isoSize"></param>
/// <returns></returns>
private static List<RawDataEntry> BuildRawRegions(List<WiiPartInfo> partitions, long isoSize)
{
var regions = new List<RawDataEntry>();
partitions.Sort((a, b) => a.PartitionOffset.CompareTo(b.PartitionOffset));
ulong cur = DiscHeaderStoredSize;
foreach (var p in partitions)
{
if (cur < p.PartitionOffset)
regions.Add(new RawDataEntry { DataOffset = cur, DataSize = p.PartitionOffset - cur });
regions.Add(new RawDataEntry { DataOffset = p.PartitionOffset, DataSize = p.DataOffset });
cur = p.DataEnd;
}
if (cur < (ulong)isoSize)
regions.Add(new RawDataEntry { DataOffset = cur, DataSize = (ulong)isoSize - cur });
return regions;
}
/// <summary>
///
/// </summary>
/// <param name="partitions"></param>
/// <param name="rawRegions"></param>
/// <param name="chunkSize"></param>
/// <returns></returns>
private static uint CalcTotalGroups(List<WiiPartInfo> partitions, List<RawDataEntry> rawRegions, uint chunkSize)
{
uint total = 0;
foreach (var p in partitions)
{
total += (uint)((p.DataSize + chunkSize - 1) / chunkSize);
}
foreach (var r in rawRegions)
{
total += (uint)((r.DataSize + chunkSize - 1) / chunkSize);
}
return total;
}
/// <summary>
///
/// </summary>
/// <param name="partitions"></param>
/// <param name="rawRegions"></param>
/// <returns></returns>
private static List<DiscRegionEntry> BuildDiscRegions(List<WiiPartInfo> partitions, List<RawDataEntry> rawRegions)
{
var result = new List<DiscRegionEntry>();
foreach (var p in partitions)
{
result.Add(new DiscRegionEntry { IsPartition = true, Offset = (long)p.DataStart, PartitionInfo = p });
}
foreach (var r in rawRegions)
{
result.Add(new DiscRegionEntry { IsPartition = false, Offset = (long)r.DataOffset, RawInfo = r });
}
result.Sort((a, b) => a.Offset.CompareTo(b.Offset));
return result;
}
#endregion
/// <summary>
/// GcFst helper
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
private static IO.Compression.RVZPack.FileSystemTableReader? BuildFileSystemTableReader(NintendoDisc source)
{
byte[]? hdr = source.ReadData(0x420, 12);
if (hdr is null)
return null;
int fstOffPos = 4, fstSzPos = 8;
uint fstOff = hdr.ReadUInt32BigEndian(ref fstOffPos);
uint fstSize = hdr.ReadUInt32BigEndian(ref fstSzPos);
if (fstOff == 0 || fstSize == 0)
return null;
byte[]? fstData = source.ReadData(fstOff, (int)fstSize);
if (fstData is null)
return null;
return IO.Compression.RVZPack.FileSystemTableReader.TryParse(fstData, offsetShift: 0);
}
#region Serialization - TODO: MOVE TO WRITER
/// <summary>
///
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
private static byte[] SerializeRawDataEntry(RawDataEntry obj)
{
using var ms = new MemoryStream();
Serialization.Writers.WIA.WriteRawDataEntry(ms, obj);
return ms.ToArray();
}
/// <summary>
///
/// </summary>
/// <param name="entries"></param>
/// <param name="count"></param>
/// <param name="isRvz"></param>
/// <returns></returns>
private static byte[] SerializeGroupEntries(RvzGroupEntry[] entries, uint count, bool isRvz)
{
using var ms = new MemoryStream();
for (int i = 0; i < count; i++)
{
var entry = entries[i];
WriteGroupEntryWia(ms, entry, isRvz);
}
return ms.ToArray();
}
/// <summary>
///
/// </summary>
/// <param name="stream"></param>
/// <param name="obj"></param>
/// <param name="isRvz"></param>
private static void WriteGroupEntryWia(Stream stream, RvzGroupEntry obj, bool isRvz)
{
if (isRvz)
{
Serialization.Writers.WIA.WriteRvzGroupEntry(stream, obj);
}
else
{
var wiaEntry = new WiaGroupEntry { DataOffset = obj.DataOffset, DataSize = obj.DataSize };
Serialization.Writers.WIA.WriteWiaGroupEntry(stream, wiaEntry);
}
}
/// <summary>
///
/// </summary>
/// <param name="stream"></param>
/// <param name="partitions"></param>
/// <returns></returns>
private static byte[] SerializePartitionEntries(Stream stream, List<WiiPartInfo> partitions)
{
using var ms = new MemoryStream();
foreach (var partition in partitions)
{
// Write 16-byte key
ms.Write(partition.TitleKey, 0, 16);
stream.Write(partition.TitleKey, 0, 16);
// DataEntry0: all of the partition
ms.WriteBigEndian((uint)(partition.DataStart / WiiBlockSize));
ms.WriteBigEndian((uint)(partition.DataSize / WiiBlockSize));
ms.WriteBigEndian(partition.FirstGroupIndex);
ms.WriteBigEndian(partition.NumberOfGroups);
stream.WriteBigEndian((uint)(partition.DataStart / WiiBlockSize));
stream.WriteBigEndian((uint)(partition.DataSize / WiiBlockSize));
stream.WriteBigEndian(partition.FirstGroupIndex);
stream.WriteBigEndian(partition.NumberOfGroups);
// DataEntry1: zeros
byte[] zeroPDE = new byte[PartitionDataEntrySize];
ms.Write(zeroPDE, 0, zeroPDE.Length);
stream.Write(zeroPDE, 0, zeroPDE.Length);
}
return ms.ToArray();
}
/// <summary>
///
/// </summary>
/// <param name="regions"></param>
/// <returns></returns>
private static byte[] SerializeRawDataEntries(List<RawDataEntry> regions)
{
using var ms = new MemoryStream();
foreach (var region in regions)
{
ms.Write(SerializeRawDataEntry(region), 0, RawDataEntrySize);
}
return ms.ToArray();
}
/// <summary>
///
/// </summary>
/// <param name="exceptions"></param>
/// <returns></returns>
private static byte[] BuildExceptionList(List<HashExceptionEntry> exceptions)
{
using var ms = new MemoryStream();
long pos = 0;
ushort count = (ushort)exceptions.Count;
ms.WriteByte((byte)(count >> 8));
ms.WriteByte((byte)count);
pos += 2;
foreach (var ex in exceptions)
{
ms.WriteByte((byte)(ex.Offset >> 8));
ms.WriteByte((byte)ex.Offset);
ms.Write(ex.Hash, 0, 20);
pos += 22;
}
while ((pos % 4) != 0)
{
ms.WriteByte(0);
pos++;
}
return ms.ToArray();
}
/// <summary>
///
/// </summary>
/// <param name="data"></param>
/// <param name="compressionType"></param>
/// <param name="compressionLevel"></param>
/// <param name="propData"></param>
/// <param name="propSize"></param>
/// <returns></returns>
private static byte[] CompressTableDataWia(byte[] data,
WiaRvzCompressionType compressionType,
int compressionLevel,
byte[] propData,
byte propSize)
{
if (compressionType == WiaRvzCompressionType.Purge)
return IO.Compression.PURGE.Compressor.Compress(data, 0, data.Length);
if (compressionType > WiaRvzCompressionType.Purge)
return Compress(compressionType, data, 0, data.Length, compressionLevel, propData, propSize);
return data;
}
#endregion
#region Header Finalization
/// <summary>
///
/// </summary>
/// <param name="dest"></param>
/// <param name="discHdr"></param>
/// <param name="isRvz"></param>
/// <param name="discType"></param>
/// <param name="compressionType"></param>
/// <param name="compressionLevel"></param>
/// <param name="chunkSize"></param>
/// <param name="numPartitions"></param>
/// <param name="partEntriesOffset"></param>
/// <param name="partHash"></param>
/// <param name="numRawData"></param>
/// <param name="rawEntriesOffset"></param>
/// <param name="rawEntriesSize"></param>
/// <param name="numGroups"></param>
/// <param name="groupEntriesOffset"></param>
/// <param name="groupEntriesSize"></param>
/// <param name="propData"></param>
/// <param name="propSize"></param>
/// <param name="isoSize"></param>
/// <param name="fileSize"></param>
private static void WriteWiaHeaders(Stream dest,
byte[] discHdr,
bool isRvz,
WiaDiscType discType,
WiaRvzCompressionType compressionType,
int compressionLevel,
uint chunkSize,
uint numPartitions,
ulong partEntriesOffset,
byte[] partHash,
uint numRawData,
ulong rawEntriesOffset,
uint rawEntriesSize,
uint numGroups,
ulong groupEntriesOffset,
uint groupEntriesSize,
byte[] propData,
byte propSize,
long isoSize,
long fileSize)
{
var header2 = new WiaHeader2
{
DiscType = discType,
CompressionType = compressionType,
CompressionLevel = compressionLevel,
ChunkSize = chunkSize,
DiscHeader = discHdr,
NumberOfPartitionEntries = numPartitions,
PartitionEntrySize = PartitionEntrySize,
PartitionEntriesOffset = partEntriesOffset,
PartitionEntriesHash = partHash,
NumberOfRawDataEntries = numRawData,
RawDataEntriesOffset = rawEntriesOffset,
RawDataEntriesSize = rawEntriesSize,
NumberOfGroupEntries = numGroups,
GroupEntriesOffset = groupEntriesOffset,
GroupEntriesSize = groupEntriesSize,
CompressorDataSize = propSize,
CompressorData = propData,
};
byte[] h2Bytes = SerializeWiaHeader2(header2);
byte[] h2Hash = ComputeSha1Wia(h2Bytes, 0, h2Bytes.Length);
uint magic = isRvz ? RvzMagic : WiaMagic;
uint ver = isRvz ? RvzVersion : WiaVersion;
uint verC = isRvz ? RvzVersionWriteCompatible : WiaVersionWriteCompatible;
var header1 = new WiaHeader1
{
Magic = magic,
Version = ver,
VersionCompatible = verC,
Header2Size = Header2Size,
Header2Hash = h2Hash,
IsoFileSize = (ulong)isoSize,
WiaFileSize = (ulong)fileSize,
Header1Hash = new byte[20],
};
byte[] h1Bytes = SerializeWiaHeader1(header1);
byte[] h1Hashable = new byte[h1Bytes.Length - 20];
Array.Copy(h1Bytes, h1Hashable, h1Hashable.Length);
header1.Header1Hash = ComputeSha1Wia(h1Hashable, 0, h1Hashable.Length);
dest.Seek(0, SeekOrigin.Begin);
dest.Write(SerializeWiaHeader1(header1), 0, Header1Size);
dest.Write(h2Bytes, 0, h2Bytes.Length);
}
/// <summary>
///
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
private static byte[] SerializeWiaHeader1(WiaHeader1 obj)
{
using var ms = new MemoryStream();
Serialization.Writers.WIA.WriteWiaHeader1(ms, obj);
return ms.ToArray();
}
/// <summary>
///
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
private static byte[] SerializeWiaHeader2(WiaHeader2 obj)
{
using var ms = new MemoryStream();
Serialization.Writers.WIA.WriteWiaHeader2(ms, obj);
return ms.ToArray();
}
/// <summary>
///
/// </summary>
/// <param name="data"></param>
/// <param name="offset"></param>
/// <param name="count"></param>
/// <returns></returns>
private static byte[] ComputeSha1Wia(byte[] data, int offset, int count)
{
if (count == 0)
return new byte[20];
using var sha1 = new HashWrapper(HashType.SHA1);
sha1.Process(data, offset, count);
sha1.Terminate();
return sha1.CurrentHashBytes ?? new byte[20];
}
#endregion
#region Helpers
/// <summary>
///
/// </summary>
/// <param name="dest"></param>
/// <param name="bytesWritten"></param>
/// <param name="gi"></param>
/// <param name="isRvz"></param>
/// <param name="compressionType"></param>
/// <returns></returns>
private static uint WriteRawGroupData(Stream dest,
ref long bytesWritten,
GcGroupWorkEntry gi,
bool isRvz,
WiaRvzCompressionType compressionType)
{
if (gi.CompressedData is not null)
{
bool useC = !isRvz || gi.CompressedData.Length < gi.MainData!.Length;
if (useC)
{
dest.Write(gi.CompressedData, 0, gi.CompressedData.Length);
bytesWritten += gi.CompressedData.Length;
return isRvz
? (uint)gi.CompressedData.Length | 0x80000000u
: (uint)gi.CompressedData.Length;
}
}
if (compressionType == WiaRvzCompressionType.Purge && gi.MainData is not null)
{
byte[] comp = IO.Compression.PURGE.Compressor.Compress(gi.MainData, 0, gi.MainData.Length);
dest.Write(comp, 0, comp.Length);
bytesWritten += comp.Length;
return (uint)comp.Length;
}
byte[] data = gi.MainData!;
dest.Write(data, 0, data.Length);
bytesWritten += data.Length;
return (uint)data.Length;
}
/// <summary>
///
/// </summary>
/// <param name="data"></param>
/// <param name="length"></param>
/// <returns></returns>
private static bool IsAllSame(byte[] data, int length)
{
if (length == 0)
return true;
byte first = data[0];
for (int i = 1; i < length; i++)
{
if (data[i] != first)
return false;
}
return true;
}
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
/// <summary>
///
/// </summary>
/// <param name="a"></param>
/// <param name="aOff"></param>
/// <param name="aLen"></param>
/// <param name="b"></param>
/// <param name="bOff"></param>
/// <param name="bLen"></param>
/// <returns></returns>
private static byte[] ConcatBytes(byte[] a, int aOff, int aLen, byte[] b, int bOff, int bLen)
{
var r = new byte[aLen + bLen];
if (aLen > 0)
Array.Copy(a, aOff, r, 0, aLen);
if (bLen > 0)
Array.Copy(b, bOff, r, aLen, bLen);
return r;
}
#endif
/// <summary>
///
/// </summary>
/// <param name="stream"></param>
/// <param name="bytesWritten"></param>
private static void PadToFourBytes(Stream stream, ref long bytesWritten)
{
int pad = (int)((-bytesWritten) & 3);
if (pad > 0)
{
stream.Write(new byte[pad], 0, pad);
bytesWritten += pad;
}
}
/// <summary>
///
/// </summary>
/// <param name="stream"></param>
/// <param name="tablePos"></param>
private static void PadTableToFourBytes(Stream stream, ref long tablePos)
{
long pad = (-tablePos) & 3;
if (pad > 0)
{
stream.Write(new byte[pad], 0, (int)pad);
tablePos += pad;
}
}
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <param name="align"></param>
/// <returns></returns>
private static long Align(long value, long align)
=> (value + align - 1) / align * align;
#endregion
#region Inner work types
// Key: (byte sameByte, int bytesRead) — replaces ValueTuple<byte,int>
private struct WiaDedupKey2 : IEquatable<WiaDedupKey2>
{
public byte SameByte;
public int BytesRead;
public WiaDedupKey2(byte sameByte, int bytesRead)
{
SameByte = sameByte;
BytesRead = bytesRead;
}
public bool Equals(WiaDedupKey2 other) => SameByte == other.SameByte && BytesRead == other.BytesRead;
public override bool Equals(object? obj) => obj is WiaDedupKey2 k && Equals(k);
public override int GetHashCode() => (SameByte * 397) ^ BytesRead;
}
// Key: (ulong partKeyHash, byte sampleByte, int bytesRead) — replaces ValueTuple<ulong,byte,int>
private struct WiaDedupKey3 : IEquatable<WiaDedupKey3>
{
public ulong PartKeyHash;
public byte SampleByte;
public int BytesRead;
public WiaDedupKey3(ulong pkh, byte sb, int br)
{
PartKeyHash = pkh;
SampleByte = sb;
BytesRead = br;
}
public bool Equals(WiaDedupKey3 other) => PartKeyHash == other.PartKeyHash && SampleByte == other.SampleByte && BytesRead == other.BytesRead;
public override bool Equals(object? obj) => obj is WiaDedupKey3 k && Equals(k);
public override int GetHashCode() => (int)(PartKeyHash ^ (PartKeyHash >> 32)) ^ (SampleByte * 397) ^ BytesRead;
}
// Value: (uint offset, uint dataSize) — replaces ValueTuple<uint,uint>
private struct WiaDedup2(uint offset, uint dataSize)
{
public uint Offset = offset;
public uint DataSize = dataSize;
}
// Flat work item for Parallel.For — replaces (int b, int c) ValueTuple
private struct WiaFlatWorkItem(int batchIndex, int chunkIndex)
{
public int BatchIndex = batchIndex;
public int ChunkIndex = chunkIndex;
}
private sealed class GcGroupWorkEntry
{
public int BytesRead;
public long SourceOffset;
public bool IsAllSame;
public byte SameByte;
public bool IsDedupHit;
public RvzGroupEntry DedupEntry = new();
public byte[]? MainData;
public uint RvzPackedSize;
public byte[]? CompressedData = null;
}
private sealed class WiiChunkWork
{
public bool IsAllZeros;
public bool IsDecDedupHit;
public uint DecDedupOffset;
public uint DecDedupDataSize;
public byte[] ExceptionListBytes = [];
public int UnpaddedExLen;
public byte[] MainDataBytes = [];
public uint RvzPackedSize;
public byte[]? CompressedData = null;
public bool DecAllSame;
public WiaDedupKey3 DecDedupKey;
}
private sealed class WiiBatchItem
{
public int BytesRead;
public long SrcOffset;
public bool IsInterDedupHit;
public WiaDedup2 DedupResult;
public bool EncAllSame;
public WiaDedupKey3 DedupKey;
public byte[]? DecryptedAll;
public List<HashExceptionEntry>? AllExceptions;
public int NumChunks;
public WiiChunkWork[]? PartWork;
}
private sealed class WiiPartInfo
{
public ulong PartitionOffset;
public byte[] TitleKey = [];
public ulong DataOffset;
public ulong DataSize;
public ulong DataStart;
public ulong DataEnd;
public uint FirstGroupIndex;
public uint NumberOfGroups;
}
private sealed class DiscRegionEntry
{
public bool IsPartition;
public long Offset;
public WiiPartInfo? PartitionInfo;
public RawDataEntry? RawInfo;
}
/// <summary>
/// Converts the WIA/RVZ image to a flat ISO file at <paramref name="outputPath"/>.
/// Re-encrypts Wii partition groups on the fly via the virtual stream.
/// </summary>
/// <param name="outputPath">Destination ISO file path.</param>
/// <returns>true on success; false on any failure.</returns>
public bool DumpIso(string? outputPath)
{
if (string.IsNullOrEmpty(outputPath))
return false;
long isoSize = (long)IsoFileSize;
if (isoSize <= 0)
return false;
try
{
using var vStream = new WiaVirtualStream(this);
using var fs = File.Open(outputPath, FileMode.Create, FileAccess.Write, FileShare.None);
const int BufSize = 2 * 1024 * 1024; // 2 MiB — aligns to WIA default / RVZ max chunk size
byte[] buf = new byte[BufSize];
long remaining = isoSize;
while (remaining > 0)
{
int toRead = (int)Math.Min(BufSize, remaining);
int read = vStream.Read(buf, 0, toRead);
if (read <= 0)
break;
fs.Write(buf, 0, read);
remaining -= read;
}
return remaining == 0;
}
catch
{
return false;
}
}
#endregion
}
}