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

313 lines
11 KiB
C#

// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Junk.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Aaru.Decryption.Ngcw (GPL-3.0-or-later).
//
// --[ Description ] ----------------------------------------------------------
//
// Nintendo GameCube/Wii junk detection, collection, and serialization.
//
// --[ 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.Collections.Generic;
using Aaru.CommonTypes.Enums;
namespace Aaru.Decryption.Ngcw;
/// <summary>In-memory junk map entry.</summary>
public struct JunkEntry
{
/// <summary>Disc byte offset where junk starts.</summary>
public ulong Offset;
/// <summary>Length of junk region in bytes.</summary>
public ulong Length;
/// <summary>Partition index (0xFFFF for GC / inter-partition).</summary>
public ushort PartitionIndex;
/// <summary>LFG seed (17 words, big-endian).</summary>
public uint[] Seed;
}
/// <summary>
/// Collects junk entries during conversion, merging contiguous entries with the same seed.
/// </summary>
public sealed class JunkCollector
{
public int Count => Entries.Count;
public List<JunkEntry> Entries { get; } = new();
/// <summary>
/// Add a junk region, merging with the last entry if contiguous with the same seed and partition.
/// </summary>
public void Add(ulong offset, ulong length, ushort partitionIndex, uint[] seed)
{
// Try merge with last entry
if(Entries.Count > 0)
{
JunkEntry last = Entries[^1];
if(last.PartitionIndex == partitionIndex &&
last.Offset + last.Length == offset &&
SeedEquals(last.Seed, seed))
{
last.Length += length;
Entries[^1] = last;
return;
}
}
var seedCopy = new uint[Lfg.SEED_SIZE];
Array.Copy(seed, seedCopy, Lfg.SEED_SIZE);
Entries.Add(new JunkEntry
{
Offset = offset,
Length = length,
PartitionIndex = partitionIndex,
Seed = seedCopy
});
}
static bool SeedEquals(uint[] a, uint[] b)
{
if(a == null || b == null) return false;
for(var i = 0; i < Lfg.SEED_SIZE; i++)
{
if(a[i] != b[i]) return false;
}
return true;
}
}
/// <summary>
/// Junk map serialization/deserialization and block-level junk detection.
/// </summary>
public static class Junk
{
const ushort JUNK_MAP_VERSION = 1;
const int JUNK_MAP_HEADER = 8; // version(2) + count(4) + seed_size(2)
const int SECTOR_SIZE = 2048;
const int SECTORS_PER_BLOCK = 16;
/// <summary>
/// Serialize a junk map for storage as a media tag.
/// </summary>
public static byte[] Serialize(List<JunkEntry> entries)
{
int entrySize = 18 + Lfg.SEED_SIZE * sizeof(uint); // 86 bytes
int totalSize = JUNK_MAP_HEADER + entries.Count * entrySize;
var buf = new byte[totalSize];
BitConverter.TryWriteBytes(buf.AsSpan(0, 2), JUNK_MAP_VERSION); // version
BitConverter.TryWriteBytes(buf.AsSpan(2, 4), (uint)entries.Count); // entry_count
BitConverter.TryWriteBytes(buf.AsSpan(6, 2), (ushort)Lfg.SEED_SIZE); // seed_size
for(var i = 0; i < entries.Count; i++)
{
int offset = JUNK_MAP_HEADER + i * entrySize;
BitConverter.TryWriteBytes(buf.AsSpan(offset, 8), entries[i].Offset);
BitConverter.TryWriteBytes(buf.AsSpan(offset + 8, 8), entries[i].Length);
BitConverter.TryWriteBytes(buf.AsSpan(offset + 16, 2), entries[i].PartitionIndex);
// Seed: raw bytes (big-endian uint32 words stored as-is)
Buffer.BlockCopy(entries[i].Seed, 0, buf, offset + 18, Lfg.SEED_SIZE * sizeof(uint));
}
return buf;
}
/// <summary>
/// Detect junk sectors in a 0x8000-byte block.
/// Exact port of detect_junk_in_block from tool/ngcw/convert.c.
/// </summary>
/// <param name="blockBuf">Block data (up to 0x8000 bytes).</param>
/// <param name="blockBytes">Actual number of valid bytes in <paramref name="blockBuf" />.</param>
/// <param name="blockOff">Disc byte offset of this block.</param>
/// <param name="dataMap">FST data map (null to treat all sectors as data).</param>
/// <param name="sysEnd">End of system area (below this = always data).</param>
/// <param name="partitionIndex">Partition index for the junk collector (0xFFFF for GC / inter-partition).</param>
/// <param name="jc">Junk collector to record detected junk entries.</param>
/// <param name="dataSectors">Incremented for each data sector.</param>
/// <param name="junkSectors">Incremented for each junk sector.</param>
/// <param name="sectorStatusOut">
/// Output array of per-sector status (SECTORS_PER_BLOCK elements).
/// </param>
public static void DetectJunkInBlock(byte[] blockBuf, int blockBytes, ulong blockOff, DataRegion[] dataMap,
ulong sysEnd, ushort partitionIndex, JunkCollector jc, ref ulong dataSectors,
ref ulong junkSectors, SectorStatus[] sectorStatusOut)
{
int numSectors = blockBytes / SECTOR_SIZE;
if(blockBytes % SECTOR_SIZE != 0) numSectors++;
// Classify sectors
var sectorIsData = new bool[SECTORS_PER_BLOCK];
for(var si = 0; si < numSectors; si++)
{
ulong off = blockOff + (ulong)si * SECTOR_SIZE;
int slen = SECTOR_SIZE;
if(si * SECTOR_SIZE + slen > blockBytes) slen = blockBytes - si * SECTOR_SIZE;
if(off < sysEnd)
sectorIsData[si] = true;
else if(dataMap != null)
sectorIsData[si] = DataMap.IsDataRegion(dataMap, off, (ulong)slen);
else
sectorIsData[si] = true;
}
// Try full-block LFG seed extraction
var blockIsLfg = false;
var blockSeed = new uint[Lfg.SEED_SIZE];
if(blockBytes >= Lfg.MIN_SEED_DATA_BYTES)
{
int matched = Lfg.GetSeed(blockBuf.AsSpan(0, blockBytes), 0, blockSeed);
if(matched >= blockBytes) blockIsLfg = true;
}
if(blockIsLfg)
{
for(var si = 0; si < numSectors; si++)
{
int slen = SECTOR_SIZE;
if(si * SECTOR_SIZE + slen > blockBytes) slen = blockBytes - si * SECTOR_SIZE;
if(sectorIsData[si])
{
sectorStatusOut[si] = SectorStatus.Dumped;
dataSectors++;
}
else
{
sectorStatusOut[si] = SectorStatus.Generable;
jc.Add(blockOff + (ulong)si * SECTOR_SIZE, (ulong)slen, partitionIndex, blockSeed);
junkSectors++;
}
}
}
else
{
// Mixed block: per-run seed extraction
var si = 0;
while(si < numSectors)
{
if(sectorIsData[si])
{
sectorStatusOut[si] = SectorStatus.Dumped;
dataSectors++;
si++;
continue;
}
int runStart = si;
while(si < numSectors && !sectorIsData[si]) si++;
int runEnd = si;
int runByteStart = runStart * SECTOR_SIZE;
int runByteEnd = runEnd * SECTOR_SIZE;
if(runByteEnd > blockBytes) runByteEnd = blockBytes;
int runBytes = runByteEnd - runByteStart;
var runHasSeed = false;
var runSeed = new uint[Lfg.SEED_SIZE];
if(runBytes >= Lfg.MIN_SEED_DATA_BYTES)
{
int matched = Lfg.GetSeed(blockBuf.AsSpan(runByteStart, runBytes), runByteStart, runSeed);
if(matched >= runBytes) runHasSeed = true;
}
for(int ri = runStart; ri < runEnd; ri++)
{
int s = ri * SECTOR_SIZE;
int slen = SECTOR_SIZE;
if(s + slen > blockBytes) slen = blockBytes - s;
if(runHasSeed)
{
// Verify this sector against LFG
var lfgBuffer = new uint[Lfg.MIN_SEED_DATA_BYTES / sizeof(uint)]; // K=521 words
var seedCopy = new uint[Lfg.SEED_SIZE];
Array.Copy(runSeed, seedCopy, Lfg.SEED_SIZE);
Lfg.SetSeed(lfgBuffer, seedCopy);
var positionBytes = 0;
if(s > 0)
{
var discard = new byte[4096];
int adv = s;
while(adv > 0)
{
int step = adv > discard.Length ? discard.Length : adv;
Lfg.GetBytes(lfgBuffer, ref positionBytes, discard, 0, step);
adv -= step;
}
}
var expected = new byte[SECTOR_SIZE];
Lfg.GetBytes(lfgBuffer, ref positionBytes, expected, 0, slen);
if(blockBuf.AsSpan(s, slen).SequenceEqual(expected.AsSpan(0, slen)))
{
sectorStatusOut[ri] = SectorStatus.Generable;
jc.Add(blockOff + (ulong)s, (ulong)slen, partitionIndex, runSeed);
junkSectors++;
}
else
{
sectorStatusOut[ri] = SectorStatus.Dumped;
dataSectors++;
}
}
else
{
// No seed — keep as data (zero-fill will dedup)
sectorStatusOut[ri] = SectorStatus.Dumped;
dataSectors++;
}
}
}
}
}
}