mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-04-05 21:44:05 +00:00
250 lines
8.9 KiB
C#
250 lines
8.9 KiB
C#
// /***************************************************************************
|
|
// Aaru Data Preservation Suite
|
|
// ----------------------------------------------------------------------------
|
|
//
|
|
// Filename : Lfg.cs
|
|
// Author(s) : Natalia Portillo <claunia@claunia.com>
|
|
//
|
|
// Component : Aaru.Decryption.Ngcw (GPL-3.0-or-later).
|
|
//
|
|
// --[ Description ] ----------------------------------------------------------
|
|
//
|
|
// Lagged Fibonacci Generator for Nintendo GameCube/Wii junk fill.
|
|
// Based on Dolphin emulator's LaggedFibonacciGenerator (CC0 licensed).
|
|
//
|
|
// --[ License ] --------------------------------------------------------------
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as
|
|
// published by the Free Software Foundation, either version 3 of the
|
|
// License, or (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
//
|
|
// ----------------------------------------------------------------------------
|
|
// Copyright © 2019-2026 Natalia Portillo
|
|
// ****************************************************************************/
|
|
|
|
using System;
|
|
using System.Buffers.Binary;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace Aaru.Decryption.Ngcw;
|
|
|
|
/// <summary>Lagged Fibonacci Generator for Nintendo GameCube/Wii junk fill.</summary>
|
|
public static class Lfg
|
|
{
|
|
/// <summary>LFG buffer size (number of uint32 words in state).</summary>
|
|
const int K = 521;
|
|
|
|
/// <summary>LFG second tap.</summary>
|
|
const int J = 32;
|
|
|
|
/// <summary>Number of uint32 words needed to seed the LFG.</summary>
|
|
public const int SEED_SIZE = 17;
|
|
|
|
/// <summary>Minimum number of bytes needed for seed extraction (K * 4 = 2084).</summary>
|
|
public const int MIN_SEED_DATA_BYTES = K * sizeof(uint);
|
|
|
|
/// <summary>Total bytes in the LFG buffer (K * 4).</summary>
|
|
const int BUFFER_BYTES = K * sizeof(uint);
|
|
|
|
static uint Swap32(uint x) => BinaryPrimitives.ReverseEndianness(x);
|
|
|
|
static void Forward(uint[] buffer)
|
|
{
|
|
for(var i = 0; i < J; i++) buffer[i] ^= buffer[i + K - J];
|
|
|
|
for(int i = J; i < K; i++) buffer[i] ^= buffer[i - J];
|
|
}
|
|
|
|
static void Backward(uint[] buffer, int startWord, int endWord)
|
|
{
|
|
int loopEnd = J > startWord ? J : startWord;
|
|
int upper = endWord < K ? endWord : K;
|
|
|
|
for(int i = upper; i > loopEnd; --i) buffer[i - 1] ^= buffer[i - 1 - J];
|
|
|
|
int upper2 = endWord < J ? endWord : J;
|
|
|
|
for(int i = upper2; i > startWord; --i) buffer[i - 1] ^= buffer[i - 1 + K - J];
|
|
}
|
|
|
|
static bool Initialize(uint[] buffer, bool checkExisting)
|
|
{
|
|
for(int i = SEED_SIZE; i < 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;
|
|
}
|
|
|
|
// Apply the shift-by-18-instead-of-16 quirk + byteswap
|
|
for(var i = 0; i < K; i++) buffer[i] = Swap32(buffer[i] & 0xFF00FFFF | buffer[i] >> 2 & 0x00FF0000);
|
|
|
|
for(var i = 0; i < 4; i++) Forward(buffer);
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool Reinitialize(uint[] buffer, uint[] seedOut)
|
|
{
|
|
for(var i = 0; i < 4; i++) Backward(buffer, 0, K);
|
|
|
|
for(var i = 0; i < K; i++) buffer[i] = Swap32(buffer[i]);
|
|
|
|
// Reconstruct bits lost by the shift-by-18-instead-of-16 quirk
|
|
for(var i = 0; i < SEED_SIZE; i++)
|
|
{
|
|
buffer[i] = buffer[i] & 0xFF00FFFF |
|
|
buffer[i] << 2 & 0x00FC0000 |
|
|
(buffer[i + 16] ^ buffer[i + 15]) << 9 & 0x00030000;
|
|
}
|
|
|
|
for(var i = 0; i < SEED_SIZE; i++) seedOut[i] = Swap32(buffer[i]);
|
|
|
|
return Initialize(buffer, true);
|
|
}
|
|
|
|
/// <summary>Initialize LFG state from a 17-word big-endian seed.</summary>
|
|
/// <param name="buffer">521-word LFG buffer to initialize.</param>
|
|
/// <param name="seed">17-word seed (big-endian uint32 values).</param>
|
|
public static void SetSeed(uint[] buffer, uint[] seed)
|
|
{
|
|
for(var i = 0; i < SEED_SIZE; i++) buffer[i] = Swap32(seed[i]);
|
|
|
|
Initialize(buffer, false);
|
|
}
|
|
|
|
/// <summary>Generate junk bytes from the LFG state.</summary>
|
|
/// <param name="buffer">521-word LFG buffer (must be initialized).</param>
|
|
/// <param name="positionBytes">Current byte position within the buffer; updated on return.</param>
|
|
/// <param name="output">Output buffer to fill.</param>
|
|
/// <param name="offset">Start offset within <paramref name="output" />.</param>
|
|
/// <param name="count">Number of bytes to generate.</param>
|
|
public static void GetBytes(uint[] buffer, ref int positionBytes, byte[] output, int offset, int count)
|
|
{
|
|
Span<byte> bufferBytes = MemoryMarshal.AsBytes(buffer.AsSpan());
|
|
|
|
while(count > 0)
|
|
{
|
|
int avail = BUFFER_BYTES - positionBytes;
|
|
int chunk = count < avail ? count : avail;
|
|
|
|
bufferBytes.Slice(positionBytes, chunk).CopyTo(output.AsSpan(offset, chunk));
|
|
|
|
positionBytes += chunk;
|
|
count -= chunk;
|
|
offset += chunk;
|
|
|
|
if(positionBytes == BUFFER_BYTES)
|
|
{
|
|
Forward(buffer);
|
|
positionBytes = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to extract the LFG seed from a chunk of data suspected to be PRNG output.
|
|
/// Requires at least K * 4 = 2084 bytes of contiguous PRNG output.
|
|
/// </summary>
|
|
/// <param name="data">Data to test.</param>
|
|
/// <param name="dataOffset">
|
|
/// Byte offset of data[0] within its LFG block (0x8000-aligned for Wii,
|
|
/// arbitrary for GC).
|
|
/// </param>
|
|
/// <param name="seedOut">If successful, receives the 17-word seed (big-endian).</param>
|
|
/// <returns>
|
|
/// Number of consecutive bytes from data[0] that match the LFG.
|
|
/// Returns 0 if the data does not look like junk.
|
|
/// </returns>
|
|
public static int GetSeed(ReadOnlySpan<byte> data, int dataOffset, uint[] seedOut)
|
|
{
|
|
int size = data.Length;
|
|
|
|
// Work on whole u32 words — skip partial leading bytes to reach uint32 alignment
|
|
int bytesToSkip = (dataOffset + 3 & ~3) - dataOffset;
|
|
|
|
if(bytesToSkip > size) return 0;
|
|
|
|
// Reinterpret data as uint32 array (native endian)
|
|
ReadOnlySpan<byte> aligned = data.Slice(bytesToSkip);
|
|
int u32Size = aligned.Length / sizeof(uint);
|
|
int u32Offset = (dataOffset + bytesToSkip) / sizeof(uint);
|
|
|
|
if(u32Size < K) return 0;
|
|
|
|
ReadOnlySpan<uint> u32Data = MemoryMarshal.Cast<byte, uint>(aligned);
|
|
|
|
// Quick sanity check: the top bits have a specific pattern from the shift quirk
|
|
for(var i = 0; i < K; i++)
|
|
{
|
|
uint x = Swap32(u32Data[i]);
|
|
|
|
if((x & 0x00C00000) != (x >> 2 & 0x00C00000)) return 0;
|
|
}
|
|
|
|
var lfgBuffer = new uint[K];
|
|
int offsetModK = u32Offset % K;
|
|
int offsetDivK = u32Offset / K;
|
|
|
|
// Place the data into the buffer at the correct position.
|
|
// Copy raw native-endian u32 values — NO byte-swapping here.
|
|
int firstPart = K - offsetModK;
|
|
|
|
if(firstPart > K) firstPart = K;
|
|
|
|
for(var i = 0; i < firstPart && i < K; i++) lfgBuffer[offsetModK + i] = u32Data[i];
|
|
|
|
for(var i = 0; i < offsetModK; i++) lfgBuffer[i] = u32Data[firstPart + i];
|
|
|
|
Backward(lfgBuffer, 0, offsetModK);
|
|
|
|
for(var i = 0; i < offsetDivK; i++) Backward(lfgBuffer, 0, K);
|
|
|
|
if(!Reinitialize(lfgBuffer, seedOut)) return 0;
|
|
|
|
int positionBytes = dataOffset % BUFFER_BYTES;
|
|
|
|
// Advance the LFG forward to match the data_offset position
|
|
for(var i = 0; i < offsetDivK; i++) Forward(lfgBuffer);
|
|
|
|
// Count how many bytes from data match the LFG output
|
|
Span<byte> lfgBytes = MemoryMarshal.AsBytes(lfgBuffer.AsSpan());
|
|
var result = 0;
|
|
var p = 0;
|
|
|
|
while(p < size)
|
|
{
|
|
byte expected = lfgBytes[positionBytes];
|
|
|
|
if(data[p] != expected) break;
|
|
|
|
result++;
|
|
p++;
|
|
positionBytes++;
|
|
|
|
if(positionBytes == BUFFER_BYTES)
|
|
{
|
|
Forward(lfgBuffer);
|
|
positionBytes = 0;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
} |