mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-04-05 21:44:05 +00:00
313 lines
11 KiB
C#
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++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |