Files
Aaru/Aaru.Decryption/Ngcw/Lfg.cs
2026-04-03 08:38:41 +02:00

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;
}
}