Update packages

This commit is contained in:
Matt Nadareski
2026-05-14 09:25:26 -04:00
parent cf586f7af2
commit 249c51933b
36 changed files with 110 additions and 1683 deletions

View File

@@ -49,8 +49,8 @@
<ItemGroup>
<PackageReference Include="SabreTools.CommandLine" Version="[1.4.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.0.0]" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.3" Condition="!$(TargetFramework.StartsWith(`net2`)) AND !$(TargetFramework.StartsWith(`net3`)) AND !$(TargetFramework.StartsWith(`net40`)) AND !$(TargetFramework.StartsWith(`net452`))" />
<PackageReference Include="SabreTools.IO" Version="[2.1.0]" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="10.0.8" Condition="!$(TargetFramework.StartsWith(`net2`)) AND !$(TargetFramework.StartsWith(`net3`)) AND !$(TargetFramework.StartsWith(`net40`)) AND !$(TargetFramework.StartsWith(`net452`))" />
</ItemGroup>
</Project>

View File

@@ -34,7 +34,7 @@
<ItemGroup>
<PackageReference Include="SabreTools.CommandLine" Version="[1.4.0]" />
<PackageReference Include="SabreTools.Hashing" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.1.0]" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
@@ -8,8 +8,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="8.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="coverlet.collector" Version="10.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>

View File

@@ -32,7 +32,7 @@
<ItemGroup>
<PackageReference Include="NetLegacySupport.Numerics" Version="1.0.1" Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`))" />
<PackageReference Include="SabreTools.IO" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.1.0]" />
</ItemGroup>
<ItemGroup>

View File

@@ -32,7 +32,7 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="SabreTools.IO" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.1.0]" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
@@ -8,8 +8,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="8.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="coverlet.collector" Version="10.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="SabreTools.Hashing" Version="[2.0.0]" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Assembly Properties -->
@@ -47,9 +47,9 @@
<PackageReference Include="MinValueTupleBridge" Version="0.2.1" Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`))" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="SabreTools.Hashing" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.1.0]" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" Condition="$(TargetFramework.StartsWith(`net40`)) OR $(TargetFramework.StartsWith(`net45`))" />
<PackageReference Include="System.ValueTuple" Version="4.6.1" Condition="$(TargetFramework.StartsWith(`net46`)) OR $(TargetFramework.StartsWith(`net47`)) OR $(TargetFramework.StartsWith(`net48`))" />
<PackageReference Include="System.ValueTuple" Version="4.6.2" Condition="$(TargetFramework.StartsWith(`net46`)) OR $(TargetFramework.StartsWith(`net47`)) OR $(TargetFramework.StartsWith(`net48`))" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
@@ -8,8 +8,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="8.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="coverlet.collector" Version="10.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="SabreTools.Hashing" Version="[2.0.0]" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />

View File

@@ -670,19 +670,17 @@ namespace SabreTools.Metadata.DatItems.Formats
CopyMachineInformation(machine);
// Process hash values
// TODO: This should be normalized to CRC-16
string? crc16 = CRC16;
if (crc16 is not null)
CRC16 = NormalizeHashData(crc16, 4);
CRC16 = TextHelper.NormalizeCRC16(crc16);
string? crc32 = CRC32;
if (crc32 is not null)
CRC32 = TextHelper.NormalizeCRC32(crc32);
// TODO: This should be normalized to CRC-64
string? crc64 = CRC64;
if (crc64 is not null)
CRC64 = NormalizeHashData(crc64, 16);
CRC64 = TextHelper.NormalizeCRC64(crc64);
string? md2 = MD2;
if (md2 is not null)
@@ -726,19 +724,17 @@ namespace SabreTools.Metadata.DatItems.Formats
DupeType = 0x00;
// Process hash values
// TODO: This should be normalized to CRC-16
string? crc16 = CRC16;
if (crc16 is not null)
CRC16 = NormalizeHashData(crc16, 4);
CRC16 = TextHelper.NormalizeCRC16(crc16);
string? crc32 = CRC32;
if (crc32 is not null)
CRC32 = TextHelper.NormalizeCRC32(crc32);
// TODO: This should be normalized to CRC-64
string? crc64 = CRC64;
if (crc64 is not null)
CRC64 = NormalizeHashData(crc64, 16);
CRC64 = TextHelper.NormalizeCRC64(crc64);
string? md2 = MD2;
if (md2 is not null)
@@ -789,87 +785,6 @@ namespace SabreTools.Metadata.DatItems.Formats
MachineIndex = machineIndex;
}
/// <summary>
/// Normalize a hash string and pad to the correct size
/// </summary>
/// TODO: Remove when IO is updated
private static string? NormalizeHashData(string? hash, int expectedLength)
{
// If we have a known blank hash, return blank
if (hash is null)
return null;
else if (hash == string.Empty || hash == "-" || hash == "_")
return string.Empty;
// Check to see if it's a "hex" hash
hash = hash!.Trim().Replace("0x", string.Empty);
// If we have a blank hash now, return blank
if (string.IsNullOrEmpty(hash))
return string.Empty;
// If the hash shorter than the required length, pad it
if (hash.Length < expectedLength)
hash = hash.PadLeft(expectedLength, '0');
// If the hash is longer than the required length, it's invalid
else if (hash.Length > expectedLength)
return string.Empty;
// Now normalize the hash
hash = hash.ToLowerInvariant();
// Otherwise, make sure that every character is a proper match
for (int i = 0; i < hash.Length; i++)
{
char c = hash[i];
#if NET7_0_OR_GREATER
if (!char.IsAsciiHexDigit(c))
#else
if (!IsAsciiHexDigit(c))
#endif
{
hash = string.Empty;
break;
}
}
return hash;
}
#if NETFRAMEWORK || NETCOREAPP3_1 || NET5_0 || NET6_0 || NETSTANDARD2_0_OR_GREATER
/// <summary>
/// Indicates whether a character is categorized as an ASCII hexademical digit.
/// </summary>
/// <param name="c">The character to evaluate.</param>
/// <returns>true if c is a hexademical digit; otherwise, false.</returns>
/// <remarks>This method determines whether the character is in the range '0' through '9', inclusive, 'A' through 'F', inclusive, or 'a' through 'f', inclusive.</remarks>
/// TODO: Remove when IO is updated
internal static bool IsAsciiHexDigit(char c)
{
return char.ToLowerInvariant(c) switch
{
'0' => true,
'1' => true,
'2' => true,
'3' => true,
'4' => true,
'5' => true,
'6' => true,
'7' => true,
'8' => true,
'9' => true,
'a' => true,
'b' => true,
'c' => true,
'd' => true,
'e' => true,
'f' => true,
_ => false,
};
}
#endif
#endregion
#region Accessors

View File

@@ -33,7 +33,7 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="SabreTools.Hashing" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.1.0]" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
@@ -18,8 +18,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="8.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="coverlet.collector" Version="10.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>

View File

@@ -36,7 +36,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="SabreTools.IO" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.1.0]" />
</ItemGroup>
</Project>

View File

@@ -38,7 +38,7 @@
<ItemGroup>
<PackageReference Include="SabreTools.Hashing" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.1.0]" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
@@ -8,8 +8,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="8.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="coverlet.collector" Version="10.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
@@ -8,8 +8,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="8.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="coverlet.collector" Version="10.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="SabreTools.Hashing" Version="[2.0.0]" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
@@ -8,8 +8,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="8.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="coverlet.collector" Version="10.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>

View File

@@ -37,7 +37,7 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="SabreTools.Hashing" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.1.0]" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
@@ -8,8 +8,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="8.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="coverlet.collector" Version="10.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>

View File

@@ -33,7 +33,7 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="SabreTools.Hashing" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.1.0]" />
</ItemGroup>
<ItemGroup>

View File

@@ -58,16 +58,16 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="GrindCore" Version="0.7.0" />
<PackageReference Include="GrindCore.SharpCompress" Version="0.46.2" Condition="!$(TargetFramework.StartsWith(`net2`)) AND !$(TargetFramework.StartsWith(`net3`)) AND !$(TargetFramework.StartsWith(`net40`)) AND !$(TargetFramework.StartsWith(`net452`))" />
<PackageReference Include="GrindCore" Version="0.7.1" />
<PackageReference Include="GrindCore.SharpCompress" Version="0.48.0" Condition="!$(TargetFramework.StartsWith(`net2`)) AND !$(TargetFramework.StartsWith(`net3`)) AND !$(TargetFramework.StartsWith(`net40`)) AND !$(TargetFramework.StartsWith(`net452`))" />
<PackageReference Include="MinValueTupleBridge" Version="0.2.1" Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`))" />
<PackageReference Include="Net30.LinqBridge" Version="1.6.0.2" Condition="$(TargetFramework.StartsWith(`net2`))" />
<PackageReference Include="NetLegacySupport.Numerics" Version="1.0.1" Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`))" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="SabreTools.Hashing" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.1.0]" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" Condition="$(TargetFramework.StartsWith(`net40`)) OR $(TargetFramework.StartsWith(`net45`))" />
<PackageReference Include="System.ValueTuple" Version="4.6.1" Condition="$(TargetFramework.StartsWith(`net46`)) OR $(TargetFramework.StartsWith(`net47`)) OR $(TargetFramework.StartsWith(`net48`))" />
<PackageReference Include="System.ValueTuple" Version="4.6.2" Condition="$(TargetFramework.StartsWith(`net46`)) OR $(TargetFramework.StartsWith(`net47`)) OR $(TargetFramework.StartsWith(`net48`))" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
@@ -18,8 +18,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="8.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="coverlet.collector" Version="10.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />

View File

@@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Linq;
using SabreTools.Numerics.Extensions;
using SabreTools.Security.Cryptography;
using Xunit;
using static SabreTools.Data.Models.NintendoDisc.Constants;
@@ -120,7 +121,7 @@ namespace SabreTools.Wrappers.Test
/// Anti-bias: the final decryption uses <see cref="WiiDecrypter.DecryptBlock"/> — a single-block
/// AES-CBC call that is completely independent of EncryptWiiGroup — so a symmetric bug
/// (broken encrypt paired with broken decrypt) would still fail the plaintext comparison.
/// The title key is encrypted via <see cref="AesCbc.Encrypt"/> (BouncyCastle), while the
/// The title key is encrypted via <see cref="AESCBC.Encrypt"/> (BouncyCastle), while the
/// verification uses <see cref="WiiDecrypter.DecryptBlock"/> — a different code path.
/// </summary>
[Fact(Skip = "Common keys are validated so this cannot pass")]
@@ -375,14 +376,14 @@ namespace SabreTools.Wrappers.Test
/// <summary>
/// Encrypts a Wii title key with the given <paramref name="commonKey"/> using
/// <see cref="AesCbc.Encrypt"/>.
/// <see cref="AESCBC.Encrypt"/>.
/// </summary>
private static byte[] EncryptTitleKeyIndependent(byte[] titleKey, byte[] titleId, byte[] commonKey)
{
byte[] iv = new byte[16];
Array.Copy(titleId, 0, iv, 0, 8);
return AesCbc.Encrypt(titleKey, commonKey, iv)
?? throw new InvalidOperationException("AesCbc.Encrypt returned null");
return AESCBC.Encrypt(titleKey, commonKey, iv)
?? throw new InvalidOperationException("AESCBC.Encrypt returned null");
}
#endregion

View File

@@ -1,77 +0,0 @@
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
// TODO: Remove when IO is updated
namespace SabreTools.Wrappers
{
/// <summary>
/// AES-128-CBC encrypt/decrypt helpers used by NintendoDisc and WIA/RVZ.
/// </summary>
/// <remarks>
/// 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 AESCBC.Decrypt / AESCBC.Encrypt calls and remove the
/// BouncyCastle using directives from this file.
/// </remarks>
public static class AesCbc
{
/// <summary>
/// Decrypts <paramref name="data"/> with AES-128-CBC (no padding).
/// Returns null if any argument is invalid or decryption fails.
/// </summary>
/// <param name="data">Ciphertext to decrypt.</param>
/// <param name="key">16-byte AES key.</param>
/// <param name="iv">16-byte initialisation vector.</param>
public static byte[]? Decrypt(byte[] data, byte[] key, byte[] iv)
{
// TODO: replace with AESCBC.Decrypt(data, key, iv) once
// SabreTools.Security.Cryptography adds an AES-CBC wrapper.
try
{
var cipher = CreateCipher(forEncryption: false, key, iv);
return cipher.DoFinal(data);
}
catch
{
return null;
}
}
/// <summary>
/// Encrypts <paramref name="data"/> with AES-128-CBC (no padding).
/// Returns null if any argument is invalid or encryption fails.
/// </summary>
/// <param name="data">Plaintext to encrypt.</param>
/// <param name="key">16-byte AES key.</param>
/// <param name="iv">16-byte initialisation vector.</param>
public static byte[]? Encrypt(byte[] data, byte[] key, byte[] iv)
{
// TODO: replace with AESCBC.Encrypt(data, key, iv) once
// SabreTools.Security.Cryptography adds an AES-CBC wrapper.
try
{
var cipher = CreateCipher(forEncryption: true, key, iv);
return cipher.DoFinal(data);
}
catch
{
return null;
}
}
/// <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;
}
}
}

View File

@@ -1,172 +0,0 @@
using System.Collections.Generic;
// TODO: Remove when IO is updated
namespace SabreTools.Wrappers
{
/// <summary>
/// Lightweight GameCube / Wii File-System Table (FST) reader used by
/// <see cref="RvzPackEncoder"/> to distinguish real-file regions from junk.
///
/// Mirrors Dolphin's FileSystemGCWii offset-to-file-info cache
/// (m_offset_file_info_cache).
/// </summary>
public sealed class FileSystemTableReader
{
/// <summary>
/// File entry with start and end byte offsets on disc
/// </summary>
public struct FileEntry
{
public long FileStart;
public long FileEnd;
}
/// <remarks>
/// Sorted ascending by FileEnd for O(log n) upper_bound queries.
/// </remarks>
private readonly List<FileEntry> _files;
private FileSystemTableReader(List<FileEntry> files)
{
_files = files;
}
/// <summary>
/// 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
/// Wii partition data.
/// </param>
/// <param name="offsetShift">
/// Bit-shift to convert raw file-offset fields to byte addresses.
/// 0 for GameCube (direct bytes); 2 for Wii (offset × 4).
/// </param>
public static FileSystemTableReader? TryParse(byte[] fstData, int offsetShift)
{
// Read the file system table
var table = Serialization.Readers.NintendoDisc.ParseFileSystemTable(fstData);
if (table is null)
return null;
// Filter out the entries to non-empty files only
var filtered = new List<FileEntry>();
foreach (var entry in table.Entries)
{
// Directory entry
if ((entry.NameOffset & 0xFF000000) != 0)
continue;
// Empty file
if (entry.FileSize == 0)
continue;
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.
filtered.Sort(delegate (FileEntry a, FileEntry b)
{
return a.FileEnd.CompareTo(b.FileEnd);
});
return new FileSystemTableReader(filtered);
}
/// <summary>
/// Returns the file entry whose byte range contains <paramref name="discOffset"/>,
/// 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)
return null;
// Binary search: first index where _files[i].FileEnd > discOffset
int lo = 0, hi = _files.Count;
while (lo < hi)
{
int mid = (lo + hi) >> 1;
if (_files[mid].FileEnd <= discOffset)
lo = mid + 1;
else
hi = mid;
}
if (lo >= _files.Count)
return null;
var e = _files[lo];
if (e.FileStart <= discOffset)
return e;
return null;
}
/// <summary>
/// Returns the smallest FileEnd value strictly greater than
/// <paramref name="discOffset"/>, or null if there is none.
/// </summary>
public long? FindNextFileEnd(long discOffset)
{
if (_files.Count == 0)
return null;
int lo = 0, hi = _files.Count;
while (lo < hi)
{
int mid = (lo + hi) >> 1;
if (_files[mid].FileEnd <= discOffset)
lo = mid + 1;
else
hi = mid;
}
return lo < _files.Count ? _files[lo].FileEnd : null;
}
/// <summary>
/// Returns the smallest FileStart value strictly greater than
/// <paramref name="discOffset"/>, or null if there is none.
/// </summary>
public long? FindNextFileStart(long discOffset)
{
if (_files.Count == 0)
return null;
// Sort is by FileEnd; scan all entries whose FileEnd > discOffset
int lo = 0, hi = _files.Count;
while (lo < hi)
{
int mid = (lo + hi) >> 1;
if (_files[mid].FileEnd <= discOffset)
lo = mid + 1;
else
hi = mid;
}
long? best = null;
for (int i = lo; i < _files.Count; i++)
{
long start = _files[i].FileStart;
if (start <= discOffset)
continue;
if (best == null || start < best.Value)
best = start;
}
return best;
}
}
}

View File

@@ -1,377 +0,0 @@
using System;
using SabreTools.Numerics.Extensions;
// TODO: Remove when IO is updated
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 uint words)
/// instead of the full data, enabling significant compression of padding areas.
/// </summary>
public class LaggedFibonacciGenerator
{
/// <summary>
///
/// </summary>
private const int LFG_K = 521;
/// <summary>
///
/// </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} uint values.", nameof(seed));
// Reinterpret LE bytes as BE (Dolphin swap32)
_position = 0;
for (int i = 0; i < SEED_SIZE; i++)
{
_buffer[i] = SwapU32(seed[i]);
}
Initialize(false);
}
/// <summary>
/// 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));
_position = 0;
int offset = 0;
for (int i = 0; i < SEED_SIZE; i++)
{
_buffer[i] = seedBytes.ReadUInt32BigEndian(ref offset);
}
Initialize(false);
}
/// <summary>
/// Skips forward by <paramref name="count"/> bytes in the output stream.
/// </summary>
/// <remarks>Matches Dolphin: LaggedFibonacciGenerator::Forward(size_t count).</remarks>
public void Forward(int count)
{
_position += count;
while (_position >= BUFFER_BYTES)
{
ForwardStep();
_position -= BUFFER_BYTES;
}
}
/// <summary>
/// Generates <paramref name="count"/> junk bytes and returns them.
/// </summary>
public byte[] GetBytes(int count)
{
byte[] output = new byte[count];
GetBytes(count, output, 0);
return output;
}
/// <summary>
/// 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 - _position);
Buffer.BlockCopy(_buffer, _position, output, outputOffset, length);
_position += length;
count -= length;
outputOffset += length;
if (_position == BUFFER_BYTES)
{
ForwardStep();
_position = 0;
}
}
}
/// <summary>
/// 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 = _position / 4;
int byteInWord = _position % 4;
byte result = (byte)(_buffer[wordIdx] >> (byteInWord * 8)); // LE byte order
_position++;
if (_position == BUFFER_BYTES)
{
ForwardStep();
_position = 0;
}
return result;
}
#region Private forward/backward state steps
/// <summary>
/// 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])
/// </remarks>
private void ForwardStep()
{
for (int i = 0; i < LFG_J; i++)
{
_buffer[i] ^= _buffer[i + LFG_K - LFG_J];
}
for (int i = LFG_J; i < LFG_K; i++)
{
_buffer[i] ^= _buffer[i - LFG_J];
}
}
/// <summary>
/// Partial or full buffer state step backward — undoes ForwardStep.
/// </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--)
{
_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--)
{
_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 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++)
{
_buffer[i] = SwapU32(_buffer[i]);
}
// Reconstruct bits 16-17 for the first SEED_SIZE words
for (int i = 0; i < SEED_SIZE; i++)
{
_buffer[i] = (_buffer[i] & 0xFF00FFFF)
| ((_buffer[i] << 2) & 0x00FC0000)
| (((_buffer[i + 16] ^ _buffer[i + 15]) << 9) & 0x00030000);
}
// Output seed as LE uint values (swap32 converts BE->LE)
for (int i = 0; i < SEED_SIZE; i++)
{
seedOut[i] = SwapU32(_buffer[i]);
}
return Initialize(true);
}
/// <summary>
/// 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.
/// </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 = (_buffer[i - 17] << 23)
^ (_buffer[i - 16] >> 9)
^ _buffer[i - 1];
if (checkExisting)
{
uint actual = (_buffer[i] & 0xFF00FFFF) | ((_buffer[i] << 2) & 0x00FC0000);
if ((calculated & 0xFFFCFFFF) != actual)
return false;
}
_buffer[i] = calculated;
}
// Output transform: each word -> swap32((x & 0xFF00FFFF) | ((x >> 2) & 0x00FF0000))
for (int i = 0; i < LFG_K; i++)
{
_buffer[i] = SwapU32((_buffer[i] & 0xFF00FFFF) | ((_buffer[i] >> 2) & 0x00FF0000));
}
for (int i = 0; i < 4; i++)
{
ForwardStep();
}
return true;
}
#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 discOffset % 0x8000 — the offset within
/// the current LFG cycle.
/// </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 uint-aligned boundary
int bytesToSkip = (4 - (dataOffsetMod % 4)) % 4;
if (bytesToSkip >= size)
return 0;
int uintDataStart = dataStart + bytesToSkip;
int uintSize = (size - bytesToSkip) / 4;
int uintDataOffset = (dataOffsetMod + bytesToSkip) / 4;
if (uintSize < LFG_K)
return 0;
// 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);
}
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._position = dataOffsetMod % BUFFER_BYTES;
int reconstructed = 0;
for (int i = 0; i < size && lfg.GetByte() == data[dataStart + i]; i++)
{
reconstructed++;
}
return reconstructed;
}
/// <summary>
/// Inner UInt32-level seed recovery.
/// </summary>
/// <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;
// Quick sanity check: bits 22-23 of swap32(x) must equal bits 20-21
// (a property of the LFG output transform).
for (int i = 0; i < LFG_K; i++)
{
uint x = SwapU32(data[i]);
if ((x & 0x00C00000) != ((x >> 2) & 0x00C00000))
return false;
}
int dataOffsetModK = dataOffset % LFG_K;
int dataOffsetDivK = dataOffset / LFG_K;
// Rotate data into buffer so buffer[dataOffsetModK] = data[0]
Array.Copy(data, 0, lfg._buffer, dataOffsetModK, LFG_K - dataOffsetModK);
if (dataOffsetModK > 0)
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;
}
#endregion
#region Helpers
/// <summary>
/// Swap endinaness of a UInt32
/// </summary>
internal static uint SwapU32(uint value)
=> (value << 24) | ((value << 8) & 0x00FF0000) | ((value >> 8) & 0x0000FF00) | (value >> 24);
#endregion
}
}

View File

@@ -1,142 +0,0 @@
using System;
using System.IO;
using SabreTools.Hashing;
using SabreTools.Numerics.Extensions;
// TODO: Remove when IO is updated
namespace SabreTools.Wrappers
{
/// <summary>
/// Compresses data using the WIA PURGE format.
///
/// PURGE layout produced:
/// [ { u32 offset BE, u32 size BE, data[size] } ] ... (zero or more segments)
/// [ SHA-1 (20 bytes) ]
///
/// Only non-zero byte runs are emitted as segments; consecutive non-zero regions
/// separated by a gap of 8 or fewer zero bytes are merged into a single segment.
/// The SHA-1 covers: <paramref name="precedingBytes"/> (e.g. exception-list prefix) +
/// all segment headers and data bytes.
///
/// This is the exact inverse of <see cref="PurgeDecompressor"/>.
/// </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.
/// </summary>
/// <param name="data">Source buffer.</param>
/// <param name="offset">Start of data within <paramref name="data"/>.</param>
/// <param name="count">Number of bytes to compress.</param>
/// <param name="precedingBytes">
/// Optional bytes that precede this payload in the WIA group
/// (e.g. the serialised exception list). Included in the SHA-1 but not emitted.
/// Pass null or empty if there are none.
/// </param>
/// <returns>PURGE-compressed byte array (segments + 20-byte SHA-1).</returns>
public static byte[] Compress(byte[] data, int offset, int count, byte[]? precedingBytes = null)
{
var output = new MemoryStream((count / 2) + 32);
int end = offset + count;
int pos = offset;
while (pos < end)
{
// 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;
// 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)
{
pos = segEnd + 1;
continue;
}
uint segOffset = (uint)(segStart - offset);
uint segSize = (uint)(segEnd - segStart);
output.WriteBigEndian(segOffset);
output.WriteBigEndian(segSize);
output.Write(data, segStart, (int)segSize);
pos = segEnd;
}
byte[] segments = output.ToArray();
// SHA-1 over: precedingBytes + segments
byte[] hash = ComputeSha1(precedingBytes, segments);
// Final result: segments + hash
byte[] result = new byte[segments.Length + 20];
Array.Copy(segments, 0, result, 0, segments.Length);
Array.Copy(hash, 0, result, segments.Length, 20);
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 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];
}
}
}

View File

@@ -1,104 +0,0 @@
using System;
using SabreTools.Hashing;
using SabreTools.Numerics.Extensions;
// TODO: Remove when IO is updated
namespace SabreTools.Wrappers
{
/// <summary>
/// Decompresses WIA PURGE-compressed group data.
///
/// PURGE layout (uncompressed exception-list prefix already stripped by caller):
/// [ { u32 offset BE, u32 size BE } { data[size] } ] ... (zero or more segments)
/// [ SHA-1 (20 bytes) ]
///
/// The SHA-1 covers: <paramref name="precedingBytes"/> (exception-list bytes, if any) +
/// all segment headers and their data bytes.
/// Bytes in the output not covered by any segment are implicitly 0x00.
///
/// References: Dolphin WIACompression.cpp — PurgeDecompressor / PurgeCompressor
/// docs/WiaAndRvz.md — wia_segment_t section
/// </summary>
internal static class PurgeDecompressor
{
private const int SHA1_SIZE = 20;
private const int SEGMENT_HEADER_SIZE = 8; // u32 offset + u32 size
/// <summary>
/// Decompresses a PURGE-compressed block.
/// </summary>
/// <param name="input">Buffer containing the compressed data.</param>
/// <param name="inputOffset">Byte offset within <paramref name="input"/> where compressed data starts.</param>
/// <param name="inputLength">Number of bytes of compressed data (segments + trailing SHA-1).</param>
/// <param name="decompressedSize">Expected decompressed output size in bytes.</param>
/// <param name="precedingBytes">
/// Bytes that precede the compressed data in the SHA-1 computation — the uncompressed
/// exception-list section for Wii partition groups. Pass null for non-Wii groups.
/// </param>
/// <returns>
/// 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,
int decompressedSize,
byte[]? precedingBytes = null)
{
if (inputLength < SHA1_SIZE)
return null;
byte[] output = new byte[decompressedSize];
int pos = inputOffset;
int dataEnd = inputOffset + inputLength - SHA1_SIZE;
using (var sha1 = new HashWrapper(HashType.SHA1))
{
if (precedingBytes is not null && precedingBytes.Length > 0)
sha1.Process(precedingBytes, 0, precedingBytes.Length);
while (pos < dataEnd)
{
if (pos + SEGMENT_HEADER_SIZE > dataEnd)
return null;
int headerPos = pos;
uint segOffset = input.ReadUInt32BigEndian(ref headerPos);
uint segSize = input.ReadUInt32BigEndian(ref headerPos);
sha1.Process(input, pos, SEGMENT_HEADER_SIZE);
pos += SEGMENT_HEADER_SIZE;
if (segSize == 0)
continue;
if (pos + (int)segSize > dataEnd)
return null;
if (segOffset + segSize > (uint)decompressedSize)
return null;
Array.Copy(input, pos, output, (int)segOffset, (int)segSize);
sha1.Process(input, pos, (int)segSize);
pos += (int)segSize;
}
sha1.Terminate();
byte[]? computed = sha1.CurrentHashBytes;
if (computed is null)
return null;
for (int i = 0; i < SHA1_SIZE; i++)
{
if (computed[i] != input[dataEnd + i])
return null;
}
}
return output;
}
}
}

View File

@@ -1,143 +0,0 @@
using System;
using SabreTools.Numerics.Extensions;
// TODO: Remove when IO is updated
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;
}
}
}

View File

@@ -1,307 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using SabreTools.Numerics.Extensions;
// TODO: Remove when IO is updated
namespace SabreTools.Wrappers
{
/// <summary>
/// Encodes disc data into RVZ-Pack format by replacing predictable LFG
/// (Lagged Fibonacci Generator) junk regions with compact seed descriptors.
///
/// This is the exact inverse of <see cref="RvzPackDecompressor"/> and mirrors
/// Dolphin's RVZPack() in WIABlob.cpp.
///
/// Two-phase algorithm:
/// <list type="number">
/// <item>Phase 1 (<see cref="ScanForJunk"/>): walk the buffer, identify LFG
/// junk regions, build a map keyed by end-offset.</item>
/// <item>Phase 2 (<see cref="EmitChunk"/>): for each chunk, use the map to
/// emit alternating real-data and junk-seed segments.</item>
/// </list>
/// </summary>
internal static class RvzPackEncoder
{
/// <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 uint[]? Seed;
}
/// <summary>
/// Result of packing a single chunk: compressed payload and its logical size.
/// </summary>
internal struct ChunkResult
{
/// <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>
public uint RvzPackedSize;
}
/// <summary>
/// RVZ-pack a single chunk.
/// 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 &lt; packed.Length due to alignment).
/// </summary>
/// <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)
return null;
var junkInfo = ScanForJunk(data, dataOffset, size, discDataOffset, fst);
if (junkInfo.Count == 0)
return null;
ChunkResult result = EmitChunk(data, dataOffset, 0L, size, junkInfo);
rvzPackedSize = result.RvzPackedSize;
return result.Packed;
}
/// <summary>
/// RVZ-pack a multi-chunk buffer (e.g. a full 2 MiB Wii group).
/// 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>
/// <returns>
/// One <see cref="ChunkResult"/> per chunk;
/// 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,
FileSystemTableReader? fst = null)
{
var junkInfo = ScanForJunk(data, dataOffset, totalSize, discDataOffset, fst);
var result = new ChunkResult[numChunks];
for (int i = 0; i < numChunks; i++)
{
long chunkStart = (long)i * bytesPerChunk;
long chunkEnd = Math.Min(chunkStart + bytesPerChunk, totalSize);
result[i] = EmitChunk(data, dataOffset, chunkStart, chunkEnd, junkInfo);
}
return result;
}
/// <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,
FileSystemTableReader? fst)
{
var junkInfo = new SortedDictionary<long, JunkRegion>();
long position = 0;
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)
{
zeroes++;
}
if (zeroes > SeedSizeBytes)
{
junkInfo[position + zeroes] = new JunkRegion
{
StartOffset = position,
Seed = new uint[LaggedFibonacciGenerator.SEED_SIZE]
};
}
position += 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);
// 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);
if (reconstructed > 0)
{
junkInfo[position + reconstructed] = new JunkRegion
{
StartOffset = position,
Seed = seed
};
}
// Step 4: FST skip AFTER GetSeed
if (fst is not null)
{
long queryOff = dataOff + reconstructed;
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;
continue;
}
}
}
// Step 5: normal advance by block window
position += bytesToRead;
dataOff += bytesToRead;
}
return junkInfo;
}
/// <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,
SortedDictionary<long, JunkRegion> junkInfo)
{
long currentOffset = chunkStart;
bool firstIteration = true;
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;
if (remaining > SeedSizeBytes)
{
foreach (var kvp in junkInfo)
{
// 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))
{
nextJunkStart = Math.Max(currentOffset, kvp.Value.StartOffset);
nextJunkEnd = Math.Min(chunkEnd, kvp.Key);
junkSeed = kvp.Value.Seed;
break;
}
}
}
// On the first iteration, bail out if there is no junk in this chunk
if (firstIteration)
{
if (nextJunkStart == chunkEnd)
return new ChunkResult { Packed = null, RvzPackedSize = 0 };
firstIteration = false;
}
// Emit real-data segment before the junk region
long nonJunkBytes = nextJunkStart - currentOffset;
if (nonJunkBytes > 0)
{
output.WriteBigEndian((uint)nonJunkBytes);
output.Write(data, (int)(dataOffset + currentOffset), (int)nonJunkBytes);
packedSize += 4 + (uint)nonJunkBytes;
currentOffset += nonJunkBytes;
}
// Emit junk-seed segment
long junkBytes = nextJunkEnd - currentOffset;
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;
currentOffset += junkBytes;
}
if (junkSeed == null)
break;
}
return new ChunkResult { Packed = output.ToArray(), RvzPackedSize = packedSize };
}
#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
}
}

View File

@@ -1,157 +0,0 @@
using System;
using SabreTools.Hashing;
// TODO: Remove when IO is updated
namespace SabreTools.Wrappers
{
public class WiiDecrypter
{
/// <summary>
/// Retail common key (index 0)
/// </summary>
public byte[] RetailCommonKey
{
get;
set
{
if (ValidateRetailCommonKey(value))
field = value;
}
} = [];
/// <summary>
/// Korean common key (index 1)
/// </summary>
public byte[] KoreanCommonKey
{
get;
set
{
if (ValidateKoreanCommonKey(value))
field = value;
}
} = [];
#region Internal Test Values
/// <summary>
/// Korean common key SHA-256 hash
/// </summary>
private const string KoreanCommonKeySHA256 = "b9f42ca27a1e178f0f14ebf1a05d486fa8db8d08875336c4e6e8dfae29f2901c";
/// <summary>
/// Retail common key SHA-256 hash
/// </summary>
private const string RetailCommonKeySHA256 = "de38aeab4fe0c36d828a47e6fd315100e7ce234d3b00aa25e6ad6f5ff2824af8";
#endregion
#region Decryption
/// <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 to use for decryption</param>
/// <returns>Decrypted 16-byte title key, or null if no key is available for the given index</returns>
public 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;
if (commonKeyIndex == 0)
commonKey = RetailCommonKey;
else if (commonKeyIndex == 1)
commonKey = KoreanCommonKey;
else
return null;
return DecryptTitleKey(encryptedTitleKey, titleId, commonKey);
}
/// <summary>
/// Decrypt a Wii partition title key from the ticket data.
/// </summary>
/// <param name="encryptedTitleKey">16-byte encrypted title key from ticket offset 0x1BF</param>
/// <param name="titleId">8-byte title ID from ticket offset 0x1DC (big-endian)</param>
/// <param name="commonKey">Common key to use for decryption</param>
/// <returns>Decrypted 16-byte title key, or null if no key is available for the given index</returns>
public static byte[]? DecryptTitleKey(byte[]? encryptedTitleKey, byte[]? titleId, byte[] commonKey)
{
if (encryptedTitleKey is null || encryptedTitleKey.Length != 16)
return null;
if (titleId is null || titleId.Length != 8)
return null;
if (commonKey.Length != 16)
return null;
// IV is the 8-byte title ID padded with zeros to 16 bytes
byte[] iv = new byte[16];
Array.Copy(titleId, 0, iv, 0, 8);
return AesCbc.Decrypt(encryptedTitleKey, commonKey, iv);
}
/// <summary>
/// Decrypt one Wii block of data (0x7C00 bytes) using AES-128-CBC.
/// </summary>
/// <param name="encryptedData">0x7C00 bytes of encrypted block data</param>
/// <param name="titleKey">16-byte partition title key</param>
/// <param name="iv">16-byte initialization vector (last 16 bytes of the preceding hash block)</param>
/// <returns>Decrypted 0x7C00-byte block data, or null on error</returns>
public static byte[]? DecryptBlock(byte[] encryptedData, byte[] titleKey, byte[] iv)
{
if (encryptedData is null || encryptedData.Length != 0x7C00)
return null;
if (titleKey is null || titleKey.Length != 16)
return null;
if (iv is null || iv.Length != 16)
return null;
return AesCbc.Decrypt(encryptedData, titleKey, iv);
}
#endregion
#region Validation Methods
/// <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)
{
// 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);
}
/// <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);
}
#endregion
}
}

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Text;
using SabreTools.Data.Models.NintendoDisc;
using SabreTools.Numerics.Extensions;
using SabreTools.Security.Cryptography;
using SabreTools.Text.Extensions;
namespace SabreTools.Wrappers

View File

@@ -5,6 +5,7 @@ using System.Text.RegularExpressions;
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Common;
using SharpCompress.Readers;
#endif
@@ -31,12 +32,7 @@ namespace SabreTools.Wrappers
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
try
{
var readerOptions = new ReaderOptions()
{
LookForHeader = lookForHeader,
ExtractFullPath = true,
Overwrite = true
};
var readerOptions = new ReaderOptions() { LookForHeader = lookForHeader };
var rarFile = (RarArchive)RarArchive.OpenArchive(_dataSource, readerOptions);
// If the file exists
@@ -216,7 +212,7 @@ namespace SabreTools.Wrappers
/// Extraction method for non-solid archives. This iterates over each entry in the archive to extract every
/// file individually, in order to extract all valid files from the archive.
/// </summary>
private static bool ExtractNonSolid(RarArchive rarFile, string outDir, bool includeDebug)
private static bool ExtractNonSolid(RarArchive rarFile, string outputDirectory, bool includeDebug)
{
foreach (var entry in rarFile.Entries)
{
@@ -250,12 +246,17 @@ namespace SabreTools.Wrappers
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outDir, filename);
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName is not null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
entry.WriteToFile(filename);
var options = new ExtractionOptions
{
ExtractFullPath = true,
Overwrite = true,
};
entry.WriteToFile(filename, options);
}
catch (Exception ex)
{
@@ -270,14 +271,19 @@ namespace SabreTools.Wrappers
/// Extraction method for solid archives. Uses ExtractAllEntries because extraction for solid archives must be
/// done sequentially, and files beyond a corrupted point in a solid archive will be unreadable anyways.
/// </summary>
private static bool ExtractSolid(RarArchive rarFile, string outDir, bool includeDebug)
private static bool ExtractSolid(RarArchive rarFile, string outputDirectory, bool includeDebug)
{
try
{
if (!Directory.Exists(outDir))
Directory.CreateDirectory(outDir);
if (!Directory.Exists(outputDirectory))
Directory.CreateDirectory(outputDirectory);
rarFile.WriteToDirectory(outDir);
var options = new ExtractionOptions
{
ExtractFullPath = true,
Overwrite = true,
};
rarFile.WriteToDirectory(outputDirectory, options);
}
catch (Exception ex)

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Assembly Properties -->
@@ -45,11 +45,11 @@
<ItemGroup>
<PackageReference Include="BouncyCastle.NetCore" Version="1.9.0" Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`)) OR $(TargetFramework.StartsWith(`net40`))" />
<PackageReference Include="BouncyCastle.NetCore" Version="2.2.1" Condition="!$(TargetFramework.StartsWith(`net2`)) AND !$(TargetFramework.StartsWith(`net3`)) AND !$(TargetFramework.StartsWith(`net40`))" />
<PackageReference Include="GrindCore" Version="0.7.0" />
<PackageReference Include="GrindCore.SharpCompress" Version="0.46.2" Condition="!$(TargetFramework.StartsWith(`net2`)) AND !$(TargetFramework.StartsWith(`net3`)) AND !$(TargetFramework.StartsWith(`net40`)) AND !$(TargetFramework.StartsWith(`net452`))" />
<PackageReference Include="GrindCore" Version="0.7.1" />
<PackageReference Include="GrindCore.SharpCompress" Version="0.48.0" Condition="!$(TargetFramework.StartsWith(`net2`)) AND !$(TargetFramework.StartsWith(`net3`)) AND !$(TargetFramework.StartsWith(`net40`)) AND !$(TargetFramework.StartsWith(`net452`))" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="SabreTools.Hashing" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.0.0]" />
<PackageReference Include="SabreTools.IO" Version="[2.1.0]" />
</ItemGroup>
<ItemGroup>

View File

@@ -5,6 +5,7 @@ using System.Text.RegularExpressions;
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
using SharpCompress.Archives;
using SharpCompress.Archives.SevenZip;
using SharpCompress.Common;
using SharpCompress.Readers;
#endif
@@ -31,12 +32,7 @@ namespace SabreTools.Wrappers
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
try
{
var readerOptions = new ReaderOptions()
{
LookForHeader = lookForHeader,
ExtractFullPath = true,
Overwrite = true
};
var readerOptions = new ReaderOptions() { LookForHeader = lookForHeader };
var sevenZip = (SevenZipArchive)SevenZipArchive.OpenArchive(_dataSource, readerOptions);
if (sevenZip is null)
return false;
@@ -226,7 +222,12 @@ namespace SabreTools.Wrappers
if (directoryName is not null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
entry.WriteToFile(filename);
var options = new ExtractionOptions
{
ExtractFullPath = true,
Overwrite = true,
};
entry.WriteToFile(filename, options);
}
catch (Exception ex)
{
@@ -248,35 +249,13 @@ namespace SabreTools.Wrappers
if (!Directory.Exists(outputDirectory))
Directory.CreateDirectory(outputDirectory);
int index = 0;
var entries = sevenZip.ExtractAllEntries();
while (entries.MoveToNextEntry())
var options = new ExtractionOptions
{
var entry = entries.Entry;
if (entry.IsDirectory)
continue;
ExtractFullPath = true,
Overwrite = true,
};
sevenZip.WriteToDirectory(outputDirectory, options);
// Ensure directory separators are consistent
string filename = entry.Key ?? $"extracted_file_{index}";
if (Path.DirectorySeparatorChar == '\\')
filename = filename.Replace('/', '\\');
else if (Path.DirectorySeparatorChar == '/')
filename = filename.Replace('\\', '/');
// Ensure the full output directory exists
filename = Path.Combine(outputDirectory, filename);
var directoryName = Path.GetDirectoryName(filename);
if (directoryName is not null && !Directory.Exists(directoryName))
Directory.CreateDirectory(directoryName);
// Write to file
using var fs = File.Open(filename, FileMode.Create, FileAccess.Write, FileShare.None);
entries.WriteEntryTo(fs);
fs.Flush();
// Increment the index
index++;
}
}
catch (Exception ex)
{

View File

@@ -9,6 +9,7 @@ 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;
@@ -188,7 +189,7 @@ namespace SabreTools.Wrappers
var groupEntries = new RvzGroupEntry[numGroups];
var rawDedupMap = new Dictionary<WiaDedupKey2, RvzGroupEntry>();
FileSystemTableReader? gcFst = isRvz ? BuildFileSystemTableReader(source) : null;
IO.Compression.RVZPack.FileSystemTableReader? gcFst = isRvz ? BuildFileSystemTableReader(source) : null;
GetCompressorData(compressionType, compressionLevel, out byte[] propData, out byte propSize);
@@ -240,7 +241,7 @@ namespace SabreTools.Wrappers
if (isRvz)
{
byte[]? packed = RvzPackEncoder.Pack(raw, 0, toRead, srcOff - toRead, out gi.RvzPackedSize, gcFst);
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;
@@ -673,7 +674,7 @@ namespace SabreTools.Wrappers
if (isRvz)
{
long baseDecOff = regionDecOff + ((long)c * (blocksPerChunk * WiiBlockDataSize));
byte[]? packed = RvzPackEncoder.Pack(procData, 0, procData.Length, baseDecOff, out rvzPackedSize);
byte[]? packed = IO.Compression.RVZPack.Compressor.Pack(procData, 0, procData.Length, baseDecOff, out rvzPackedSize);
mainData = packed ?? procData;
if (packed is null)
rvzPackedSize = 0;
@@ -722,7 +723,7 @@ namespace SabreTools.Wrappers
}
else if (compressionType == WiaRvzCompressionType.Purge)
{
pw.CompressedData = PurgeCompressor.Compress(pw.MainDataBytes, 0, pw.MainDataBytes.Length, pw.ExceptionListBytes);
pw.CompressedData = IO.Compression.PURGE.Compressor.Compress(pw.MainDataBytes, 0, pw.MainDataBytes.Length, pw.ExceptionListBytes);
}
});
}
@@ -930,7 +931,7 @@ namespace SabreTools.Wrappers
uint rvzPackedSize = 0;
if (isRvz)
{
byte[]? packed = RvzPackEncoder.Pack(data, 0, toRead, srcOff, out rvzPackedSize);
byte[]? packed = IO.Compression.RVZPack.Compressor.Pack(data, 0, toRead, srcOff, out rvzPackedSize);
mainData = packed ?? data;
if (packed is null)
rvzPackedSize = 0;
@@ -949,7 +950,7 @@ namespace SabreTools.Wrappers
}
else if (compressionType == WiaRvzCompressionType.Purge)
{
compressed = PurgeCompressor.Compress(mainData, 0, mainData.Length);
compressed = IO.Compression.PURGE.Compressor.Compress(mainData, 0, mainData.Length);
}
uint groupOff = (uint)(bytesWritten >> 2);
@@ -1061,11 +1062,11 @@ namespace SabreTools.Wrappers
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[] 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];
byte[] recompHash = AESCBC.Decrypt(reEncHashBlock, titleKey, new byte[16]) ?? new byte[WiiBlockHeaderSize];
for (int off = 0; off < WiiBlockHeaderSize; off += 20)
{
@@ -1243,7 +1244,7 @@ namespace SabreTools.Wrappers
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
private static FileSystemTableReader? BuildFileSystemTableReader(NintendoDisc source)
private static IO.Compression.RVZPack.FileSystemTableReader? BuildFileSystemTableReader(NintendoDisc source)
{
byte[]? hdr = source.ReadData(0x420, 12);
if (hdr is null)
@@ -1259,7 +1260,7 @@ namespace SabreTools.Wrappers
if (fstData is null)
return null;
return FileSystemTableReader.TryParse(fstData, offsetShift: 0);
return IO.Compression.RVZPack.FileSystemTableReader.TryParse(fstData, offsetShift: 0);
}
#region Serialization - TODO: MOVE TO WRITER
@@ -1411,7 +1412,7 @@ namespace SabreTools.Wrappers
byte propSize)
{
if (compressionType == WiaRvzCompressionType.Purge)
return PurgeCompressor.Compress(data, 0, data.Length);
return IO.Compression.PURGE.Compressor.Compress(data, 0, data.Length);
if (compressionType > WiaRvzCompressionType.Purge)
return Compress(compressionType, data, 0, data.Length, compressionLevel, propData, propSize);
@@ -1593,7 +1594,7 @@ namespace SabreTools.Wrappers
if (compressionType == WiaRvzCompressionType.Purge && gi.MainData is not null)
{
byte[] comp = PurgeCompressor.Compress(gi.MainData, 0, gi.MainData.Length);
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;

View File

@@ -5,6 +5,9 @@ using SabreTools.Data.Models.WIA;
using SabreTools.Hashing;
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
using SabreTools.IO.Extensions;
#endif
using SabreTools.Security.Cryptography;
#if NET462_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
using SharpCompress.Compressors;
using SharpCompress.Compressors.BZip2;
using SharpCompress.Compressors.LZMA;
@@ -1143,7 +1146,7 @@ namespace SabreTools.Wrappers
Array.Copy(fileData, offset, exceptionBytes, 0, exceptionLen);
int purgeLen = length - exceptionLen;
return PurgeDecompressor.Decompress(fileData, purgeStart, purgeLen, expectedSize, exceptionBytes);
return IO.Compression.PURGE.Decompressor.Decompress(fileData, purgeStart, purgeLen, expectedSize, exceptionBytes);
}
else
{
@@ -1183,7 +1186,7 @@ namespace SabreTools.Wrappers
byte[] rvzPayload = new byte[rvzDataLen];
Array.Copy(workingData, rvzDataStart, rvzPayload, 0, rvzDataLen);
var rvzDecomp = new RvzPackDecompressor(rvzPayload, rvzPackedSize, dataOffsetForLfg);
var rvzDecomp = new IO.Compression.RVZPack.Decompressor(rvzPayload, rvzPackedSize, dataOffsetForLfg);
byte[] unpacked = new byte[expectedSize];
int bytesRead = rvzDecomp.Decompress(unpacked, 0, expectedSize);
if (bytesRead < expectedSize)
@@ -1358,7 +1361,7 @@ namespace SabreTools.Wrappers
// Note: off is now 0x3D4; IV will sit at 0x3D0 after encryption
// Encrypt hash block with IV = zero
byte[] encHashBlock = AesCbc.Encrypt(hashBlock, key, new byte[16]) ?? new byte[WiiBlockHashSize];
byte[] encHashBlock = AESCBC.Encrypt(hashBlock, key, new byte[16]) ?? new byte[WiiBlockHashSize];
// Extract IV for data block from offset 0x3D0 of the encrypted hash block
byte[] iv = new byte[16];
@@ -1371,7 +1374,7 @@ namespace SabreTools.Wrappers
if (dataLen > 0)
Array.Copy(decryptedData, dataSrc, dataBlock, 0, dataLen);
byte[] encDataBlock = AesCbc.Encrypt(dataBlock, key, iv) ?? new byte[WiiBlockDataSize];
byte[] encDataBlock = AESCBC.Encrypt(dataBlock, key, iv) ?? new byte[WiiBlockDataSize];
int dest = b * WiiBlockSize;
Array.Copy(encHashBlock, 0, result, dest, WiiBlockHashSize);