From f1434ef33f49ac0b243e70bead2ee79f63929495 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Fri, 27 Mar 2026 20:22:44 +0100 Subject: [PATCH] Add redumper DVD image --- Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs | 4 +- Aaru.Core/Image/Convert/Convert.cs | 4 +- Aaru.Core/Image/Convert/Edge.cs | 12 + Aaru.Core/Image/Convert/Ngcw/ConvertNgcw.cs | 154 +++++- Aaru.Decoders/DVD/Sector.cs | 94 ++++ Aaru.Decoders/Nintendo/Sector.cs | 20 +- Aaru.Decryption/Aaru.Decryption.csproj | 5 +- .../Ngcw/Crypto.cs | 6 +- .../Ngcw/DataMap.cs | 8 +- .../Convert => Aaru.Decryption}/Ngcw/Junk.cs | 10 +- .../Convert => Aaru.Decryption}/Ngcw/Lfg.cs | 6 +- .../Ngcw/Partitions.cs | 10 +- Aaru.Images/Aaru.Images.csproj | 3 +- .../Localization/Localization.Designer.cs | 14 +- Aaru.Images/Localization/Localization.es.resx | 6 + Aaru.Images/Localization/Localization.resx | 6 + Aaru.Images/Redumper/Identify.cs | 71 +++ Aaru.Images/Redumper/Ngcw.cs | 151 ++++++ Aaru.Images/Redumper/Properties.cs | 120 +++++ Aaru.Images/Redumper/Read.cs | 469 ++++++++++++++++++ Aaru.Images/Redumper/Redumper.cs | 118 +++++ Aaru.Images/Redumper/Verify.cs | 105 ++++ 22 files changed, 1355 insertions(+), 41 deletions(-) rename {Aaru.Core/Image/Convert => Aaru.Decryption}/Ngcw/Crypto.cs (97%) rename {Aaru.Core/Image/Convert => Aaru.Decryption}/Ngcw/DataMap.cs (96%) rename {Aaru.Core/Image/Convert => Aaru.Decryption}/Ngcw/Junk.cs (98%) rename {Aaru.Core/Image/Convert => Aaru.Decryption}/Ngcw/Lfg.cs (98%) rename {Aaru.Core/Image/Convert => Aaru.Decryption}/Ngcw/Partitions.cs (97%) create mode 100644 Aaru.Images/Redumper/Identify.cs create mode 100644 Aaru.Images/Redumper/Ngcw.cs create mode 100644 Aaru.Images/Redumper/Properties.cs create mode 100644 Aaru.Images/Redumper/Read.cs create mode 100644 Aaru.Images/Redumper/Redumper.cs create mode 100644 Aaru.Images/Redumper/Verify.cs diff --git a/Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs b/Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs index 3e119bb13..76f4f2e37 100644 --- a/Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs +++ b/Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs @@ -35,11 +35,11 @@ using System.Linq; using Aaru.CommonTypes; using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; -using Aaru.Core.Image.Ngcw; +using Aaru.Decryption.Ngcw; using Aaru.Decoders.Nintendo; using Aaru.Helpers; using Aaru.Localization; -using NgcwPartitions = Aaru.Core.Image.Ngcw.Partitions; +using NgcwPartitions = Aaru.Decryption.Ngcw.Partitions; namespace Aaru.Core.Devices.Dumping; diff --git a/Aaru.Core/Image/Convert/Convert.cs b/Aaru.Core/Image/Convert/Convert.cs index 43ff920b9..acefaf57a 100644 --- a/Aaru.Core/Image/Convert/Convert.cs +++ b/Aaru.Core/Image/Convert/Convert.cs @@ -287,7 +287,7 @@ public partial class Convert } } - errno = ConvertNgcwSectors(); + errno = ConvertNgcwSectors(useLong); if(errno != ErrorNumber.NoError) return errno; } @@ -367,6 +367,7 @@ public partial class Convert } } + // TODO: Enable for Ngcw if(!isPs3Conversion && !isWiiuConversion && !isNgcwConversion && _negativeSectors > 0) { errno = ConvertNegativeSectors(useLong); @@ -374,6 +375,7 @@ public partial class Convert if(errno != ErrorNumber.NoError) return errno; } + // TODO: Enable for Ngcw if(!isPs3Conversion && !isWiiuConversion && !isNgcwConversion && _overflowSectors > 0) { errno = ConvertOverflowSectors(useLong); diff --git a/Aaru.Core/Image/Convert/Edge.cs b/Aaru.Core/Image/Convert/Edge.cs index 5b9e7dfa4..08dbab8f5 100644 --- a/Aaru.Core/Image/Convert/Edge.cs +++ b/Aaru.Core/Image/Convert/Edge.cs @@ -135,6 +135,12 @@ public partial class Convert case SectorTagType.CdSectorEccP: case SectorTagType.CdSectorEccQ: case SectorTagType.CdSectorEcc: + case SectorTagType.DvdSectorCmi: + case SectorTagType.DvdSectorTitleKey: + case SectorTagType.DvdSectorEdc: + case SectorTagType.DvdSectorIed: + case SectorTagType.DvdSectorInformation: + case SectorTagType.DvdSectorNumber: // These tags are inline in long sector continue; case SectorTagType.CdTrackFlags: @@ -329,6 +335,12 @@ public partial class Convert case SectorTagType.CdSectorEccP: case SectorTagType.CdSectorEccQ: case SectorTagType.CdSectorEcc: + case SectorTagType.DvdSectorCmi: + case SectorTagType.DvdSectorTitleKey: + case SectorTagType.DvdSectorEdc: + case SectorTagType.DvdSectorIed: + case SectorTagType.DvdSectorInformation: + case SectorTagType.DvdSectorNumber: // These tags are inline in long sector continue; case SectorTagType.CdTrackFlags: diff --git a/Aaru.Core/Image/Convert/Ngcw/ConvertNgcw.cs b/Aaru.Core/Image/Convert/Ngcw/ConvertNgcw.cs index c095c36a0..264722eb6 100644 --- a/Aaru.Core/Image/Convert/Ngcw/ConvertNgcw.cs +++ b/Aaru.Core/Image/Convert/Ngcw/ConvertNgcw.cs @@ -37,7 +37,9 @@ using System.Collections.Generic; using System.Text; using Aaru.CommonTypes; using Aaru.CommonTypes.Enums; -using Aaru.Core.Image.Ngcw; +using Aaru.Decoders.Nintendo; +using Aaru.Decryption.Ngcw; +using NgcwPartitions = Aaru.Decryption.Ngcw.Partitions; using Aaru.Helpers; using Aaru.Localization; @@ -68,7 +70,7 @@ public partial class Convert // Parse Wii partition table PulseProgress?.Invoke(UI.Ngcw_parsing_partition_table); - _ngcwPartitions = Ngcw.Partitions.ParseWiiPartitions(_inputImage); + _ngcwPartitions = NgcwPartitions.ParseWiiPartitions(_inputImage); if(_ngcwPartitions == null) { @@ -83,10 +85,10 @@ public partial class Convert // Build partition region map PulseProgress?.Invoke(UI.Ngcw_building_partition_key_map); - _ngcwRegions = Ngcw.Partitions.BuildRegionMap(_ngcwPartitions); + _ngcwRegions = NgcwPartitions.BuildRegionMap(_ngcwPartitions); // Serialize and write partition key map - byte[] keyMapData = Ngcw.Partitions.SerializeKeyMap(_ngcwRegions); + byte[] keyMapData = NgcwPartitions.SerializeKeyMap(_ngcwRegions); _outputImage.WriteMediaTag(keyMapData, MediaTagType.WiiPartitionKeyMap); @@ -104,7 +106,7 @@ public partial class Convert /// writes with Unencrypted status; plaintext areas use Dumped/Generable. /// Does not copy sector tags, negative sectors, or overflow sectors. /// - ErrorNumber ConvertNgcwSectors() + ErrorNumber ConvertNgcwSectors(bool useLong) { if(_aborted) return ErrorNumber.NoError; @@ -119,7 +121,8 @@ public partial class Convert if(_mediaType == MediaType.GOD) { - ErrorNumber errno = + ErrorNumber errno = useLong ? + ConvertGameCubeSectorsLong(discSize, totalLogicalSectors, jc, ref dataSectors, ref junkSectors) : ConvertGameCubeSectors(discSize, totalLogicalSectors, jc, ref dataSectors, ref junkSectors); if(errno != ErrorNumber.NoError) @@ -131,7 +134,9 @@ public partial class Convert } else { - ErrorNumber errno = ConvertWiiSectors(discSize, totalLogicalSectors, jc, ref dataSectors, ref junkSectors); + ErrorNumber errno = useLong ? + ConvertWiiSectorsLong(discSize, totalLogicalSectors, jc, ref dataSectors, ref junkSectors) : + ConvertWiiSectors(discSize, totalLogicalSectors, jc, ref dataSectors, ref junkSectors); if(errno != ErrorNumber.NoError) { @@ -294,7 +299,7 @@ public partial class Convert (long)totalLogicalSectors); // Check if inside a partition's data area - int inPart = Ngcw.Partitions.FindPartitionAtOffset(_ngcwPartitions, offset); + int inPart = NgcwPartitions.FindPartitionAtOffset(_ngcwPartitions, offset); if(inPart >= 0) { @@ -747,4 +752,137 @@ public partial class Convert return result; } + + /// GameCube sector conversion pipeline for long sectors. + ErrorNumber ConvertGameCubeSectorsLong(ulong discSize, ulong totalLogicalSectors, JunkCollector jc, + ref ulong dataSectors, ref ulong junkSectors) + { + const int blockSize = Crypto.GROUP_SIZE; // 0x8000 logical (16 × 2048 user bytes) + const int sectorsPerBlock = Crypto.LOGICAL_PER_GROUP; + const int userSize = Crypto.SECTOR_SIZE; + + // Junk / FST offsets are in logical (user) byte space. User 2048-byte slices match ReadSector (Nintendo main_data at Sector.NintendoMainDataOffset). + ErrorNumber probeErr = _inputImage.ReadSectorLong(0, false, out byte[] longProbe, out _); + + if(probeErr != ErrorNumber.NoError || longProbe == null || + longProbe.Length < Sector.NintendoMainDataOffset + userSize) + return ErrorNumber.InOutError; + + int longSectorSize = longProbe.Length; + + // Read disc header to get FST info (logical user bytes via ReadSector) + byte[] header = ReadNgcwSectors(0, 2); // first 0x1000 bytes (need 0x42C) + + if(header == null || header.Length < 0x42C) return ErrorNumber.InOutError; + + // Read extended header for FST pointers (at 0x424) + byte[] extHeader = ReadNgcwSectors(0, (0x440 + userSize - 1) / userSize); + + var fstOffset = BigEndianBitConverter.ToUInt32(extHeader, 0x424); + var fstSize = BigEndianBitConverter.ToUInt32(extHeader, 0x428); + ulong sysEnd = fstOffset + fstSize; + + // Build FST data map + DataRegion[] dataMap = null; + + if(fstSize > 0 && fstSize < 64 * 1024 * 1024) + { + byte[] fst = ReadNgcwBytes(fstOffset, (int)fstSize); + + if(fst != null) dataMap = DataMap.BuildFromFst(fst, 0, 0); + } + + // User-only buffer for LFG / data-region junk detection (same as ConvertGameCubeSectors) + var userBlockBuf = new byte[blockSize]; + var longSectorBufs = new byte[sectorsPerBlock][]; + var sectorStatuses = new SectorStatus[sectorsPerBlock]; + + for(ulong blockOff = 0; blockOff < discSize; blockOff += blockSize) + { + if(_aborted) break; + + int blockBytes = blockSize; + + if(blockOff + (ulong)blockBytes > discSize) blockBytes = (int)(discSize - blockOff); + + ulong baseSector = blockOff / userSize; + + UpdateProgress?.Invoke(string.Format(UI.Converting_sectors_0_to_1, + baseSector, + baseSector + sectorsPerBlock), + (long)baseSector, + (long)totalLogicalSectors); + + // Read long sectors; pack user main data into userBlockBuf; keep full long buffers for output + for(var s = 0; s < sectorsPerBlock && s * userSize < blockBytes; s++) + { + ErrorNumber errno = + _inputImage.ReadSectorLong(baseSector + (ulong)s, false, out byte[] sectorData, out _); + + byte[] stored = new byte[longSectorSize]; + longSectorBufs[s] = stored; + + if(errno != ErrorNumber.NoError || sectorData == null) + { + Array.Clear(userBlockBuf, s * userSize, userSize); + + continue; + } + + int copyLong = sectorData.Length < longSectorSize ? sectorData.Length : longSectorSize; + Array.Copy(sectorData, 0, stored, 0, copyLong); + + if(sectorData.Length >= Sector.NintendoMainDataOffset + userSize) + Array.Copy(sectorData, Sector.NintendoMainDataOffset, userBlockBuf, s * userSize, userSize); + else + Array.Clear(userBlockBuf, s * userSize, userSize); + } + + Junk.DetectJunkInBlock(userBlockBuf, + blockBytes, + blockOff, + dataMap, + sysEnd, + 0xFFFF, + jc, + ref dataSectors, + ref junkSectors, + sectorStatuses); + + int numSectors = blockBytes / userSize; + + for(var si = 0; si < numSectors; si++) + { + ulong sector = baseSector + (ulong)si; + bool ok = _outputImage.WriteSectorLong(longSectorBufs[si], sector, false, sectorStatuses[si]); + + if(!ok) + { + if(_force) + { + ErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_continuing, + _outputImage.ErrorMessage, + sector)); + } + else + { + StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing, + _outputImage.ErrorMessage, + sector)); + + return ErrorNumber.WriteError; + } + } + } + } + + return ErrorNumber.NoError; + } + + /// Wii sector conversion pipeline for long sectors. + ErrorNumber ConvertWiiSectorsLong(ulong discSize, ulong totalLogicalSectors, JunkCollector jc, ref ulong dataSectors, ref ulong junkSectors){ + // TODO: Implement + return ErrorNumber.NoError; + } + } \ No newline at end of file diff --git a/Aaru.Decoders/DVD/Sector.cs b/Aaru.Decoders/DVD/Sector.cs index c6a858185..56d963e29 100644 --- a/Aaru.Decoders/DVD/Sector.cs +++ b/Aaru.Decoders/DVD/Sector.cs @@ -157,6 +157,49 @@ public sealed class Sector return ret; } + static readonly byte[] DvdGfExp = CreateDvdGfExpTable(); + static readonly int[] DvdGfLog = CreateDvdGfLogTable(DvdGfExp); + + static byte[] CreateDvdGfExpTable() + { + const ushort primitive = 0x11D; + byte[] exp = new byte[512]; + exp[0] = 1; + + for(int i = 1; i < 255; i++) + { + ushort x = (ushort)(exp[i - 1] << 1); + + if((x & 0x100) != 0) x ^= primitive; + + exp[i] = (byte)x; + } + + for(int i = 255; i < 512; i++) exp[i] = exp[i - 255]; + + return exp; + } + + static int[] CreateDvdGfLogTable(byte[] exp) + { + int[] log = new int[256]; + + for(int i = 0; i < 256; i++) log[i] = -1; + + for(int i = 0; i < 255; i++) log[exp[i]] = i; + + log[0] = -1; + + return log; + } + + static byte DvdGfMul(byte a, byte b) + { + if(a == 0 || b == 0) return 0; + + return DvdGfExp[DvdGfLog[a] + DvdGfLog[b]]; + } + /// /// Store seed and its cipher in cache /// @@ -207,6 +250,57 @@ public sealed class Sector return true; } + /// + /// Computes the 16-bit ID Error Detection (IED) for bytes 0-3 of a DVD ROM sector (ID + 3-byte PSN), + /// per the ECMA-267 RS remainder step used in gpsxre::dvd::DataFrame::ID::valid() (redumper). + /// + /// Buffer of the sector (must be at least 4 bytes; first 4 bytes are the ID field). + /// The IED value in the same byte order as stored at sector bytes 4-5 (little-endian uint16 layout). + public static ushort ComputeIed(byte[] sectorLong) + { + if(sectorLong == null || sectorLong.Length < 4) return 0; + + // G(x) = x^2 + g1*x + g2, with g1 = alpha^0 + alpha^1, g2 = alpha^0 * alpha^1 in GF(2^8) + byte g1 = (byte)(1 ^ DvdGfExp[1]); + byte g2 = DvdGfExp[1]; + + Span poly = stackalloc byte[6]; + poly[0] = sectorLong[0]; + poly[1] = sectorLong[1]; + poly[2] = sectorLong[2]; + poly[3] = sectorLong[3]; + poly[4] = 0; + poly[5] = 0; + + for(int i = 0; i < 4; i++) + { + byte coef = poly[i]; + + if(coef == 0) continue; + + poly[i] = 0; + poly[i + 1] ^= DvdGfMul(coef, g1); + poly[i + 2] ^= DvdGfMul(coef, g2); + } + + return (ushort)(poly[4] | poly[5] << 8); + } + + /// + /// Check if the IED of a sector is correct (bytes 0-3 vs bytes 4-5, same layout as redumper DataFrame::ID). + /// + /// Buffer of the sector + /// True if IED is correct, False if not + public static bool CheckIed(byte[] sectorLong) + { + if(sectorLong == null || sectorLong.Length < 6) return false; + + ushort computed = ComputeIed(sectorLong); + ushort stored = (ushort)(sectorLong[4] | sectorLong[5] << 8); + + return computed == stored; + } + /// /// Tests if a seed unscrambles a sector correctly /// diff --git a/Aaru.Decoders/Nintendo/Sector.cs b/Aaru.Decoders/Nintendo/Sector.cs index 27c7582b6..40e8bc332 100644 --- a/Aaru.Decoders/Nintendo/Sector.cs +++ b/Aaru.Decoders/Nintendo/Sector.cs @@ -42,14 +42,22 @@ namespace Aaru.Decoders.Nintendo; public sealed class Sector { /// - /// ECMA-267 main_data offset in OmniDrive 2064-byte Nintendo sectors: DVD XOR applies to 2048 bytes from - /// here (same as standard DVD). Bytes 6-11 (cpr_mai) are not scrambled on media. + /// Start of the 2048-byte DVD XOR (scramble) region in a 2064-byte Nintendo sector — same as ECMA-267 + /// main_data for a standard DVD sector. Nintendo still applies the table to these 2048 bytes. /// - public const int NintendoMainDataOffset = 12; + public const int NintendoScrambledDataOffset = 12; /// - /// Derives the Nintendo descramble key from the first 8 bytes of the cpr_mai region (LBA 0 payload). - /// Used when software-descrambling Nintendo sectors. + /// Start of the 2048-byte logical main_data exposed to the game / filesystem in Nintendo GameCube/Wii DVD + /// sectors. Unlike ECMA-267 (where main_data begins at byte 12), Nintendo uses byte 6; CPR_MAI and related + /// fields follow a different layout than on a standard DVD-ROM. The DVD XOR layer still scrambles 2048 bytes + /// starting at ; bytes 6–11 are not part of that scrambled block. + /// + public const int NintendoMainDataOffset = 6; + + /// + /// Derives the Nintendo descramble key from the first 8 bytes at in the + /// LBA 0 sector after descramble (same 8 bytes used for key derivation in the drive/firmware path). /// public static byte DeriveNintendoKey(byte[] cprMaiFirst8) { @@ -76,7 +84,7 @@ public sealed class Sector if(sector is not { Length: 2064 }) return ErrorNumber.NotSupported; int psn = DVD.Sector.GetPsn(sector); - int mainDataStart = NintendoMainDataOffset; + int mainDataStart = NintendoScrambledDataOffset; int tableOffset = (int)((nintendoKey ^ (psn >> 4 & 0xF)) * DVD.Sector.Form1DataSize + 7 * DVD.Sector.Form1DataSize + DVD.Sector.Form1DataSize / 2); diff --git a/Aaru.Decryption/Aaru.Decryption.csproj b/Aaru.Decryption/Aaru.Decryption.csproj index d09a8596d..6285b9124 100644 --- a/Aaru.Decryption/Aaru.Decryption.csproj +++ b/Aaru.Decryption/Aaru.Decryption.csproj @@ -8,7 +8,7 @@ Aaru.Decryption Decryption algorithms used by the Aaru Data Preservation Suite. https://github.com/aaru-dps/ - MIT + (MIT AND GPL-3.0-only) https://github.com/aaru-dps/Aaru.Decryption true en-US @@ -32,9 +32,10 @@ + - + diff --git a/Aaru.Core/Image/Convert/Ngcw/Crypto.cs b/Aaru.Decryption/Ngcw/Crypto.cs similarity index 97% rename from Aaru.Core/Image/Convert/Ngcw/Crypto.cs rename to Aaru.Decryption/Ngcw/Crypto.cs index 2cce0cf4d..4ef08ed78 100644 --- a/Aaru.Core/Image/Convert/Ngcw/Crypto.cs +++ b/Aaru.Decryption/Ngcw/Crypto.cs @@ -5,7 +5,7 @@ // Filename : Crypto.cs // Author(s) : Natalia Portillo // -// Component : Image conversion. +// Component : Aaru.Decryption.Ngcw (GPL-3.0-or-later). // // --[ Description ] ---------------------------------------------------------- // @@ -34,10 +34,10 @@ using System; using System.Security.Cryptography; -namespace Aaru.Core.Image.Ngcw; +namespace Aaru.Decryption.Ngcw; /// Wii disc encryption helpers. -static class Crypto +public static class Crypto { /// Wii physical group size (32 KiB). public const int GROUP_SIZE = 0x8000; diff --git a/Aaru.Core/Image/Convert/Ngcw/DataMap.cs b/Aaru.Decryption/Ngcw/DataMap.cs similarity index 96% rename from Aaru.Core/Image/Convert/Ngcw/DataMap.cs rename to Aaru.Decryption/Ngcw/DataMap.cs index c3683100b..a1726016a 100644 --- a/Aaru.Core/Image/Convert/Ngcw/DataMap.cs +++ b/Aaru.Decryption/Ngcw/DataMap.cs @@ -5,7 +5,7 @@ // Filename : DataMap.cs // Author(s) : Natalia Portillo // -// Component : Image conversion. +// Component : Aaru.Decryption.Ngcw (GPL-3.0-or-later). // // --[ Description ] ---------------------------------------------------------- // @@ -34,10 +34,10 @@ using System; using System.Collections.Generic; using Aaru.Helpers; -namespace Aaru.Core.Image.Ngcw; +namespace Aaru.Decryption.Ngcw; /// A contiguous region of file data on disc. -readonly struct DataRegion : IComparable +public readonly struct DataRegion : IComparable { public readonly ulong Offset; public readonly ulong Length; @@ -55,7 +55,7 @@ readonly struct DataRegion : IComparable /// Sorted map of file data regions parsed from a Nintendo GameCube/Wii FST. /// Used to classify disc sectors as data (file content / system area) or potential junk. /// -static class DataMap +public static class DataMap { /// /// Build a data region map from an FST (File System Table). diff --git a/Aaru.Core/Image/Convert/Ngcw/Junk.cs b/Aaru.Decryption/Ngcw/Junk.cs similarity index 98% rename from Aaru.Core/Image/Convert/Ngcw/Junk.cs rename to Aaru.Decryption/Ngcw/Junk.cs index 6116f6371..8677edaed 100644 --- a/Aaru.Core/Image/Convert/Ngcw/Junk.cs +++ b/Aaru.Decryption/Ngcw/Junk.cs @@ -5,7 +5,7 @@ // Filename : Junk.cs // Author(s) : Natalia Portillo // -// Component : Image conversion. +// Component : Aaru.Decryption.Ngcw (GPL-3.0-or-later). // // --[ Description ] ---------------------------------------------------------- // @@ -34,10 +34,10 @@ using System; using System.Collections.Generic; using Aaru.CommonTypes.Enums; -namespace Aaru.Core.Image.Ngcw; +namespace Aaru.Decryption.Ngcw; /// In-memory junk map entry. -struct JunkEntry +public struct JunkEntry { /// Disc byte offset where junk starts. public ulong Offset; @@ -55,7 +55,7 @@ struct JunkEntry /// /// Collects junk entries during conversion, merging contiguous entries with the same seed. /// -sealed class JunkCollector +public sealed class JunkCollector { public int Count => Entries.Count; @@ -110,7 +110,7 @@ sealed class JunkCollector /// /// Junk map serialization/deserialization and block-level junk detection. /// -static class Junk +public static class Junk { const ushort JUNK_MAP_VERSION = 1; const int JUNK_MAP_HEADER = 8; // version(2) + count(4) + seed_size(2) diff --git a/Aaru.Core/Image/Convert/Ngcw/Lfg.cs b/Aaru.Decryption/Ngcw/Lfg.cs similarity index 98% rename from Aaru.Core/Image/Convert/Ngcw/Lfg.cs rename to Aaru.Decryption/Ngcw/Lfg.cs index 5ff29f212..267ff6c5b 100644 --- a/Aaru.Core/Image/Convert/Ngcw/Lfg.cs +++ b/Aaru.Decryption/Ngcw/Lfg.cs @@ -5,7 +5,7 @@ // Filename : Lfg.cs // Author(s) : Natalia Portillo // -// Component : Image conversion. +// Component : Aaru.Decryption.Ngcw (GPL-3.0-or-later). // // --[ Description ] ---------------------------------------------------------- // @@ -35,10 +35,10 @@ using System; using System.Buffers.Binary; using System.Runtime.InteropServices; -namespace Aaru.Core.Image.Ngcw; +namespace Aaru.Decryption.Ngcw; /// Lagged Fibonacci Generator for Nintendo GameCube/Wii junk fill. -static class Lfg +public static class Lfg { /// LFG buffer size (number of uint32 words in state). const int K = 521; diff --git a/Aaru.Core/Image/Convert/Ngcw/Partitions.cs b/Aaru.Decryption/Ngcw/Partitions.cs similarity index 97% rename from Aaru.Core/Image/Convert/Ngcw/Partitions.cs rename to Aaru.Decryption/Ngcw/Partitions.cs index 460cdd42f..ce3187968 100644 --- a/Aaru.Core/Image/Convert/Ngcw/Partitions.cs +++ b/Aaru.Decryption/Ngcw/Partitions.cs @@ -5,7 +5,7 @@ // Filename : Partitions.cs // Author(s) : Natalia Portillo // -// Component : Image conversion. +// Component : Aaru.Decryption.Ngcw (GPL-3.0-or-later). // // --[ Description ] ---------------------------------------------------------- // @@ -36,10 +36,10 @@ using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; using Aaru.Helpers; -namespace Aaru.Core.Image.Ngcw; +namespace Aaru.Decryption.Ngcw; /// In-memory representation of a Wii partition. -struct WiiPartition +public struct WiiPartition { /// Partition offset on disc. public ulong Offset; @@ -58,7 +58,7 @@ struct WiiPartition } /// Wii partition region for the partition key map. -struct WiiPartitionRegion +public struct WiiPartitionRegion { /// First physical sector (0x8000-byte units). public uint StartSector; @@ -73,7 +73,7 @@ struct WiiPartitionRegion /// /// Wii partition table parsing and key map serialization. /// -static class Partitions +public static class Partitions { /// /// Parse the Wii partition table from a source image, extracting all partitions diff --git a/Aaru.Images/Aaru.Images.csproj b/Aaru.Images/Aaru.Images.csproj index ff1493067..12b18caa4 100644 --- a/Aaru.Images/Aaru.Images.csproj +++ b/Aaru.Images/Aaru.Images.csproj @@ -1,4 +1,4 @@ - + 2.0 @@ -50,6 +50,7 @@ + diff --git a/Aaru.Images/Localization/Localization.Designer.cs b/Aaru.Images/Localization/Localization.Designer.cs index 305b8dfa4..f36992867 100644 --- a/Aaru.Images/Localization/Localization.Designer.cs +++ b/Aaru.Images/Localization/Localization.Designer.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -5937,5 +5937,17 @@ namespace Aaru.Images { return ResourceManager.GetString("WinOnCD_disc_image", resourceCulture); } } + + internal static string Redumper_Name { + get { + return ResourceManager.GetString("Redumper_Name", resourceCulture); + } + } + + internal static string Redumper_disc_image { + get { + return ResourceManager.GetString("Redumper_disc_image", resourceCulture); + } + } } } diff --git a/Aaru.Images/Localization/Localization.es.resx b/Aaru.Images/Localization/Localization.es.resx index 84f6bf467..0504c5d39 100644 --- a/Aaru.Images/Localization/Localization.es.resx +++ b/Aaru.Images/Localization/Localization.es.resx @@ -2970,4 +2970,10 @@ Imagen de disco de WinOnCD + + Volcado DVD crudo de Redumper + + + Imagen de disco DVD crudo de Redumper + \ No newline at end of file diff --git a/Aaru.Images/Localization/Localization.resx b/Aaru.Images/Localization/Localization.resx index 3bf5aecaa..a9e6434d1 100644 --- a/Aaru.Images/Localization/Localization.resx +++ b/Aaru.Images/Localization/Localization.resx @@ -2980,4 +2980,10 @@ WinOnCD disc image + + Redumper raw DVD dump + + + Redumper raw DVD disc image + \ No newline at end of file diff --git a/Aaru.Images/Redumper/Identify.cs b/Aaru.Images/Redumper/Identify.cs new file mode 100644 index 000000000..8532661b9 --- /dev/null +++ b/Aaru.Images/Redumper/Identify.cs @@ -0,0 +1,71 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Identify.cs +// Author(s) : Rebecca Wallander +// +// Component : Disc image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Identifies Redumper raw DVD dump images. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Rebecca Wallander +// ****************************************************************************/ + +using System.IO; +using Aaru.CommonTypes.Interfaces; + +namespace Aaru.Images; + +public sealed partial class Redumper +{ +#region IOpticalMediaImage Members + + /// + public bool Identify(IFilter imageFilter) + { + string filename = imageFilter.Filename; + + if(string.IsNullOrEmpty(filename)) return false; + + string extension = Path.GetExtension(filename)?.ToLower(); + + if(extension != ".state") return false; + + string basePath = filename[..^".state".Length]; + string sdramPath = basePath + ".sdram"; + + if(!File.Exists(sdramPath)) return false; + + long stateLength = imageFilter.DataForkLength; + long sdramLength = new FileInfo(sdramPath).Length; + + if(sdramLength == 0 || stateLength == 0) return false; + + if(sdramLength % RECORDING_FRAME_SIZE != 0) return false; + + long frameCount = sdramLength / RECORDING_FRAME_SIZE; + + return stateLength == frameCount; + } + +#endregion +} \ No newline at end of file diff --git a/Aaru.Images/Redumper/Ngcw.cs b/Aaru.Images/Redumper/Ngcw.cs new file mode 100644 index 000000000..9def68195 --- /dev/null +++ b/Aaru.Images/Redumper/Ngcw.cs @@ -0,0 +1,151 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Ngcw.cs +// Author(s) : Rebecca Wallander +// +// Component : Disc image plugins (Redumper Nintendo GOD/WOD). +// +// --[ Description ] ---------------------------------------------------------- +// +// Nintendo DVD descrambling for GameCube/Wii Redumper dumps. Produces +// 2064-byte long sectors (and 2048-byte user via ReadSector) matching a +// raw dump after the Nintendo layer: Wii AES partition data remains +// ciphertext in user sectors until conversion (see ConvertNgcwSectors). +// Junk maps, partition key tags, and Wii decrypt are handled when +// converting to AaruFormat, not when reading this plugin. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Rebecca Wallander +// ****************************************************************************/ + +using System; +using Aaru.CommonTypes; +using Aaru.CommonTypes.Enums; +using Aaru.Decoders.Nintendo; + +namespace Aaru.Images; + +public sealed partial class Redumper +{ + const int NGCW_LONG_SECTOR_SIZE = 2064; + const int NGCW_SECTORS_PER_GROUP = 16; + + static bool IsNintendoMediaType(MediaType mt) => mt is MediaType.GOD or MediaType.WOD; + + /// + /// Derives the Nintendo disc key from LBA 0 so sectors 16+ can be descrambled. + /// Does not parse partitions, junk, or decrypt Wii groups — conversion does that. + /// + void TryInitializeNgcwAfterOpen() + { + _nintendoDerivedKey = null; + + if(!IsNintendoMediaType(_imageInfo.MediaType)) return; + + EnsureNintendoDerivedKeyFromLba0(); + } + + /// + /// Derives the Nintendo key from LBA 0 so sectors 16+ can be descrambled. + /// + /// True if the Nintendo key was derived successfully, False if not + bool EnsureNintendoDerivedKeyFromLba0() + { + ErrorNumber errno = ReadSectorLongForNgcw(0, false, out byte[] long0, out _); + + return errno == ErrorNumber.NoError && long0 != null && long0.Length >= NGCW_LONG_SECTOR_SIZE; + } + + /// + /// Reads a sector long for Nintendo descrambling. + /// + /// The sector address to read + /// Whether the sector address is negative + /// The buffer to read the sector into + /// The status of the sector + /// The error number + /// + /// This method is used to read a sector long for Nintendo descrambling. + /// It is used to read the sector long for LBA 0 to derive the Nintendo key. + /// + ErrorNumber ReadSectorLongForNgcw(ulong sectorAddress, bool negative, out byte[] buffer, out SectorStatus sectorStatus) + { + buffer = null; + sectorStatus = SectorStatus.NotDumped; + + int lba = negative ? -(int)sectorAddress : (int)sectorAddress; + + long frameIndex = (long)lba - LBA_START; + + if(frameIndex < 0 || frameIndex >= _totalFrames) return ErrorNumber.OutOfRange; + + sectorStatus = MapState(_stateData[frameIndex]); + + byte[] dvdSector = ReadAndFlattenFrame(frameIndex); + + if(dvdSector is null) return ErrorNumber.InvalidArgument; + + if(sectorStatus != SectorStatus.Dumped) + { + buffer = dvdSector; + + return ErrorNumber.NoError; + } + + if(!IsNintendoMediaType(_imageInfo.MediaType)) + { + ErrorNumber error = _decoding.Scramble(dvdSector, out byte[] descrambled); + + buffer = error == ErrorNumber.NoError ? descrambled : dvdSector; + + return ErrorNumber.NoError; + } + + if(!DescrambleNintendo2064InPlace(dvdSector, lba)) + { + buffer = dvdSector; + + return ErrorNumber.NoError; + } + + buffer = dvdSector; + + return ErrorNumber.NoError; + } + + bool DescrambleNintendo2064InPlace(byte[] buffer, int lba) + { + byte[] one = new byte[NGCW_LONG_SECTOR_SIZE]; + Array.Copy(buffer, 0, one, 0, NGCW_LONG_SECTOR_SIZE); + byte key = lba < NGCW_SECTORS_PER_GROUP ? (byte)0 : (_nintendoDerivedKey ?? (byte)0); + + ErrorNumber error = _nintendoDecoder.Scramble(one, key, out byte[] decoded); + + if(error != ErrorNumber.NoError) + { + Array.Clear(buffer, 0, NGCW_LONG_SECTOR_SIZE); + + return false; + } + + if(decoded != null) Array.Copy(decoded, 0, buffer, 0, NGCW_LONG_SECTOR_SIZE); + + if(lba == 0 && decoded != null) + { + byte[] keyMaterial = new byte[8]; + Array.Copy(decoded, Sector.NintendoMainDataOffset, keyMaterial, 0, 8); + _nintendoDerivedKey = Sector.DeriveNintendoKey(keyMaterial); + } + + return true; + } +} diff --git a/Aaru.Images/Redumper/Properties.cs b/Aaru.Images/Redumper/Properties.cs new file mode 100644 index 000000000..5e926f3bc --- /dev/null +++ b/Aaru.Images/Redumper/Properties.cs @@ -0,0 +1,120 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Properties.cs +// Author(s) : Rebecca Wallander +// +// Component : Disc image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Contains properties for Redumper raw DVD dump images. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Rebecca Wallander +// ****************************************************************************/ + +using System; +using System.Collections.Generic; +using Aaru.CommonTypes; +using Aaru.CommonTypes.AaruMetadata; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Structs; +using Partition = Aaru.CommonTypes.Partition; +using Track = Aaru.CommonTypes.Structs.Track; +using Session = Aaru.CommonTypes.Structs.Session; + +namespace Aaru.Images; + +public sealed partial class Redumper +{ + #region IOpticalMediaImage Members + + /// + // ReSharper disable once ConvertToAutoProperty + public ImageInfo Info => _imageInfo; + + /// + public string Name => Localization.Redumper_Name; + + /// + public Guid Id => new("F2D3E4A5-B6C7-4D8E-9F0A-1B2C3D4E5F60"); + + /// + public string Author => Authors.RebeccaWallander; + + /// + public string Format => Localization.Redumper_disc_image; + + /// + public List Partitions { get; private set; } + + /// + public List Tracks { get; private set; } + + /// + public List Sessions { get; private set; } + + /// + public List DumpHardware => null; + + /// + public Metadata AaruMetadata => null; + + /// + public IEnumerable SupportedMediaTags => + [ + MediaTagType.DVD_PFI, + MediaTagType.DVD_PFI_2ndLayer, + MediaTagType.DVD_DMI, + MediaTagType.DVD_BCA + ]; + + /// + public IEnumerable SupportedSectorTags => + [ + SectorTagType.DvdSectorInformation, + SectorTagType.DvdSectorNumber, + SectorTagType.DvdSectorIed, + SectorTagType.DvdSectorCmi, + SectorTagType.DvdSectorTitleKey, + SectorTagType.DvdSectorEdc + ]; + + /// + public IEnumerable SupportedMediaTypes => + [ + MediaType.DVDROM, MediaType.DVDR, MediaType.DVDRDL, MediaType.DVDRW, MediaType.DVDRWDL, MediaType.DVDRAM, + MediaType.DVDPR, MediaType.DVDPRDL, MediaType.DVDPRW, MediaType.DVDPRWDL, MediaType.GOD, MediaType.WOD, + ]; + + /// + public IEnumerable<(string name, Type type, string description, object @default)> SupportedOptions => []; + + /// + public IEnumerable KnownExtensions => [".state"]; + + /// + public bool IsWriting => false; + + /// + public string ErrorMessage { get; private set; } + + #endregion +} \ No newline at end of file diff --git a/Aaru.Images/Redumper/Read.cs b/Aaru.Images/Redumper/Read.cs new file mode 100644 index 000000000..fa45c764c --- /dev/null +++ b/Aaru.Images/Redumper/Read.cs @@ -0,0 +1,469 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Read.cs +// Author(s) : Rebecca Wallander +// +// Component : Disc image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Reads Redumper raw DVD dump images (.sdram + .state). +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Rebecca Wallander +// ****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Aaru.CommonTypes; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; +using Aaru.Decoders.DVD; +using Aaru.Helpers; +using Aaru.Logging; +using Partition = Aaru.CommonTypes.Partition; +using Track = Aaru.CommonTypes.Structs.Track; +using TrackType = Aaru.CommonTypes.Enums.TrackType; +using Session = Aaru.CommonTypes.Structs.Session; + +namespace Aaru.Images; + +public sealed partial class Redumper +{ +#region IOpticalMediaImage Members + + /// + public ErrorNumber Open(IFilter imageFilter) + { + string filename = imageFilter.Filename; + + if(string.IsNullOrEmpty(filename)) return ErrorNumber.InvalidArgument; + + string basePath = filename[..^".state".Length]; + string sdramPath = basePath + ".sdram"; + + if(!File.Exists(sdramPath)) return ErrorNumber.NoSuchFile; + + long stateLength = imageFilter.DataForkLength; + long sdramLength = new FileInfo(sdramPath).Length; + + if(sdramLength % RECORDING_FRAME_SIZE != 0) return ErrorNumber.InvalidArgument; + + _totalFrames = sdramLength / RECORDING_FRAME_SIZE; + + if(stateLength != _totalFrames) return ErrorNumber.InvalidArgument; + + _imageFilter = imageFilter; + + // Read entire state file into memory (1 byte per frame, manageable size) + Stream stateStream = imageFilter.GetDataForkStream(); + _stateData = new byte[stateLength]; + stateStream.Seek(0, SeekOrigin.Begin); + stateStream.EnsureRead(_stateData, 0, (int)stateLength); + + // Open sdram via filter system + _sdramFilter = PluginRegister.Singleton.GetFilter(sdramPath); + + if(_sdramFilter is null) return ErrorNumber.NoSuchFile; + + // Compute sector counts + // Frames map to physical LBAs: frame[i] → LBA (LBA_START + i) + // Negative LBAs: LBA_START .. -1, count = min(-LBA_START, _totalFrames) + // Positive LBAs: 0 .. (_totalFrames + LBA_START - 1) + long negativeLbaCount = Math.Min(-LBA_START, _totalFrames); + long positiveLbaCount = Math.Max(0, _totalFrames + LBA_START); + + _imageInfo.NegativeSectors = (uint)negativeLbaCount; + _imageInfo.Sectors = (ulong)positiveLbaCount; + _imageInfo.SectorSize = DVD_USER_DATA_SIZE; + _imageInfo.ImageSize = _imageInfo.Sectors * DVD_USER_DATA_SIZE; + + _imageInfo.CreationTime = imageFilter.CreationTime; + _imageInfo.LastModificationTime = imageFilter.LastWriteTime; + _imageInfo.MediaTitle = Path.GetFileNameWithoutExtension(imageFilter.Filename); + _imageInfo.MetadataMediaType = MetadataMediaType.OpticalDisc; + _imageInfo.HasPartitions = true; + _imageInfo.HasSessions = true; + + // Load media tag sidecars + _mediaTags = new Dictionary(); + LoadMediaTagSidecars(basePath); + + // Determine media type from PFI if available + _imageInfo.MediaType = MediaType.DVDROM; + + if(_mediaTags.TryGetValue(MediaTagType.DVD_PFI, out byte[] pfi)) + { + PFI.PhysicalFormatInformation? decodedPfi = PFI.Decode(pfi, _imageInfo.MediaType); + + if(decodedPfi.HasValue) + { + _imageInfo.MediaType = decodedPfi.Value.DiskCategory switch + { + DiskCategory.DVDPR => MediaType.DVDPR, + DiskCategory.DVDPRDL => MediaType.DVDPRDL, + DiskCategory.DVDPRW => MediaType.DVDPRW, + DiskCategory.DVDPRWDL => MediaType.DVDPRWDL, + DiskCategory.DVDR => decodedPfi.Value.PartVersion >= 6 ? MediaType.DVDRDL : MediaType.DVDR, + DiskCategory.DVDRAM => MediaType.DVDRAM, + DiskCategory.DVDRW => decodedPfi.Value.PartVersion >= 15 ? MediaType.DVDRWDL : MediaType.DVDRW, + DiskCategory.Nintendo => decodedPfi.Value.DiscSize == DVDSize.Eighty + ? MediaType.GOD + : MediaType.WOD, + _ => MediaType.DVDROM + }; + } + } + + TryInitializeNgcwAfterOpen(); + + _imageInfo.ReadableMediaTags = [.._mediaTags.Keys]; + + // Sector tags available from DVD RecordingFrame structure + _imageInfo.ReadableSectorTags = + [ + SectorTagType.DvdSectorInformation, + SectorTagType.DvdSectorNumber, + SectorTagType.DvdSectorIed, + SectorTagType.DvdSectorCmi, + SectorTagType.DvdSectorTitleKey, + SectorTagType.DvdSectorEdc + ]; + + // Set up single track and session covering positive LBAs + Tracks = + [ + new Track + { + Sequence = 1, + Session = 1, + Type = TrackType.Data, + StartSector = 0, + EndSector = _imageInfo.Sectors > 0 ? _imageInfo.Sectors - 1 : 0, + Pregap = 0, + FileType = "BINARY", + Filter = _sdramFilter, + File = sdramPath, + BytesPerSector = DVD_USER_DATA_SIZE, + RawBytesPerSector = DVD_SECTOR_SIZE + } + ]; + + Sessions = + [ + new Session + { + Sequence = 1, + StartSector = 0, + EndSector = _imageInfo.Sectors > 0 ? _imageInfo.Sectors - 1 : 0, + StartTrack = 1, + EndTrack = 1 + } + ]; + + Partitions = + [ + new Partition + { + Sequence = 0, + Start = 0, + Length = _imageInfo.Sectors, + Size = _imageInfo.Sectors * _imageInfo.SectorSize, + Offset = 0, + Type = "DVD Data" + } + ]; + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber ReadMediaTag(MediaTagType tag, out byte[] buffer) + { + buffer = null; + + if(!_mediaTags.TryGetValue(tag, out byte[] data)) return ErrorNumber.NoData; + + buffer = new byte[data.Length]; + Array.Copy(data, buffer, data.Length); + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber ReadSector(ulong sectorAddress, bool negative, out byte[] buffer, out SectorStatus sectorStatus) + { + buffer = new byte[DVD_USER_DATA_SIZE]; + ErrorNumber errno = ReadSectorLong(sectorAddress, negative, out byte[] long_buffer, out sectorStatus); + if(errno != ErrorNumber.NoError) return errno; + + Array.Copy(long_buffer, Aaru.Decoders.Nintendo.Sector.NintendoMainDataOffset, buffer, 0, DVD_USER_DATA_SIZE); + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber ReadSectors(ulong sectorAddress, bool negative, uint length, out byte[] buffer, + out SectorStatus[] sectorStatus) + { + buffer = null; + sectorStatus = null; + + buffer = new byte[length * DVD_USER_DATA_SIZE]; + sectorStatus = new SectorStatus[length]; + + for(uint i = 0; i < length; i++) + { + ulong addr = negative ? sectorAddress - i : sectorAddress + i; + ErrorNumber errno = ReadSector(addr, negative, out byte[] sector, out SectorStatus status); + + if(errno != ErrorNumber.NoError) return errno; + + Array.Copy(sector, 0, buffer, i * DVD_USER_DATA_SIZE, DVD_USER_DATA_SIZE); + sectorStatus[i] = status; + } + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber ReadSectorLong(ulong sectorAddress, bool negative, out byte[] buffer, + out SectorStatus sectorStatus) => + ReadSectorLongForNgcw(sectorAddress, negative, out buffer, out sectorStatus); + + /// + public ErrorNumber ReadSectorsLong(ulong sectorAddress, bool negative, uint length, out byte[] buffer, + out SectorStatus[] sectorStatus) + { + buffer = null; + sectorStatus = null; + + buffer = new byte[length * DVD_SECTOR_SIZE]; + sectorStatus = new SectorStatus[length]; + + for(uint i = 0; i < length; i++) + { + ErrorNumber errno = ReadSectorLong(sectorAddress + i, negative, out byte[] sector, out SectorStatus status); + + if(errno != ErrorNumber.NoError) return errno; + + Array.Copy(sector, 0, buffer, i * DVD_SECTOR_SIZE, DVD_SECTOR_SIZE); + sectorStatus[i] = status; + } + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber ReadSectorTag(ulong sectorAddress, bool negative, SectorTagType tag, out byte[] buffer) + { + buffer = null; + + return ReadSectorsTag(sectorAddress, negative, 1, tag, out buffer); + } + + /// + public ErrorNumber ReadSectorsTag(ulong sectorAddress, bool negative, uint length, SectorTagType tag, + out byte[] buffer) + { + buffer = null; + + uint sectorOffset; + uint sectorSize; + + switch(tag) + { + case SectorTagType.DvdSectorInformation: + sectorOffset = 0; + sectorSize = 1; + + break; + case SectorTagType.DvdSectorNumber: + sectorOffset = 1; + sectorSize = 3; + + break; + case SectorTagType.DvdSectorIed: + sectorOffset = 4; + sectorSize = 2; + + break; + case SectorTagType.DvdSectorCmi: + sectorOffset = 6; + sectorSize = 1; + + break; + case SectorTagType.DvdSectorTitleKey: + sectorOffset = 7; + sectorSize = 5; + + break; + case SectorTagType.DvdSectorEdc: + sectorOffset = 2060; + sectorSize = 4; + + break; + default: + return ErrorNumber.NotSupported; + } + + buffer = new byte[sectorSize * length]; + + for(uint i = 0; i < length; i++) + { + ErrorNumber errno = ReadSectorLong(sectorAddress + i, negative, out byte[] sector, out _); + + if(errno != ErrorNumber.NoError) return errno; + + Array.Copy(sector, sectorOffset, buffer, i * sectorSize, sectorSize); + } + + return ErrorNumber.NoError; + } + + /// + public ErrorNumber ReadSector(ulong sectorAddress, uint track, out byte[] buffer, out SectorStatus sectorStatus) => + ReadSector(sectorAddress, false, out buffer, out sectorStatus); + + /// + public ErrorNumber ReadSectorLong(ulong sectorAddress, uint track, out byte[] buffer, + out SectorStatus sectorStatus) => + ReadSectorLong(sectorAddress, false, out buffer, out sectorStatus); + + /// + public ErrorNumber ReadSectors(ulong sectorAddress, uint length, uint track, out byte[] buffer, + out SectorStatus[] sectorStatus) => + ReadSectors(sectorAddress, false, length, out buffer, out sectorStatus); + + /// + public ErrorNumber ReadSectorsLong(ulong sectorAddress, uint length, uint track, out byte[] buffer, + out SectorStatus[] sectorStatus) => + ReadSectorsLong(sectorAddress, false, length, out buffer, out sectorStatus); + + /// + public ErrorNumber ReadSectorTag(ulong sectorAddress, uint track, SectorTagType tag, out byte[] buffer) => + ReadSectorTag(sectorAddress, false, tag, out buffer); + + /// + public ErrorNumber ReadSectorsTag(ulong sectorAddress, uint length, uint track, SectorTagType tag, + out byte[] buffer) => + ReadSectorsTag(sectorAddress, false, length, tag, out buffer); + + /// + public List GetSessionTracks(Session session) => Tracks; + + /// + public List GetSessionTracks(ushort session) => Tracks; + +#endregion + + /// + /// Reads one RecordingFrame from the .sdram file and flattens it into a 2064-byte DVD sector + /// by extracting only the 172-byte main_data portion from each of the 12 rows (discarding PI/PO parity). + /// + byte[] ReadAndFlattenFrame(long frameIndex) + { + Stream stream = _sdramFilter.GetDataForkStream(); + long offset = frameIndex * RECORDING_FRAME_SIZE; + + if(offset + RECORDING_FRAME_SIZE > stream.Length) return null; + + var frame = new byte[RECORDING_FRAME_SIZE]; + stream.Seek(offset, SeekOrigin.Begin); + stream.EnsureRead(frame, 0, RECORDING_FRAME_SIZE); + + // Flatten: copy 172 main-data bytes from each of the 12 rows into a contiguous 2064-byte buffer + var dvdSector = new byte[DVD_SECTOR_SIZE]; + int rowStride = ROW_MAIN_DATA_SIZE + ROW_PARITY_INNER_SIZE; + + for(int row = 0; row < RECORDING_FRAME_ROWS; row++) + Array.Copy(frame, row * rowStride, dvdSector, row * ROW_MAIN_DATA_SIZE, ROW_MAIN_DATA_SIZE); + + return dvdSector; + } + + /// Maps a Redumper state byte to an Aaru SectorStatus. + static SectorStatus MapState(byte state) => + state switch + { + 0 => SectorStatus.NotDumped, // ERROR_SKIP + 1 => SectorStatus.Errored, // ERROR_C2 + _ => SectorStatus.Dumped // SUCCESS_C2_OFF (2), SUCCESS_SCSI_OFF (3), SUCCESS (4) + }; + + /// Loads Redumper sidecar files (.physical, .manufacturer, .bca) as media tags. + void LoadMediaTagSidecars(string basePath) + { + // PFI layer 0: prefer unindexed, fall back to .0.physical + LoadScsiSidecar(basePath, ".physical", ".0.physical", MediaTagType.DVD_PFI); + + // PFI layer 1 + LoadScsiSidecar(basePath, ".1.physical", null, MediaTagType.DVD_PFI_2ndLayer); + + // DMI layer 0: prefer unindexed, fall back to .0.manufacturer + LoadScsiSidecar(basePath, ".manufacturer", ".0.manufacturer", MediaTagType.DVD_DMI); + + // BCA (no SCSI header stripping — redumper writes raw BCA data) + string bcaPath = basePath + ".bca"; + + if(File.Exists(bcaPath)) + { + byte[] bcaData = File.ReadAllBytes(bcaPath); + + if(bcaData.Length > 0) + { + _mediaTags[MediaTagType.DVD_BCA] = bcaData; + AaruLogging.Debug(MODULE_NAME, Localization.Found_media_tag_0, MediaTagType.DVD_BCA); + } + } + } + + /// + /// Loads a SCSI READ DVD STRUCTURE response sidecar, strips the 4-byte parameter list header, + /// and stores the 2048-byte payload as a media tag. + /// + void LoadScsiSidecar(string basePath, string primarySuffix, string fallbackSuffix, MediaTagType tag) + { + string path = basePath + primarySuffix; + + if(!File.Exists(path)) + { + if(fallbackSuffix is null) return; + + path = basePath + fallbackSuffix; + + if(!File.Exists(path)) return; + } + + byte[] data = File.ReadAllBytes(path); + + if(data.Length <= SCSI_HEADER_SIZE) return; + + // Strip the 4-byte SCSI parameter list header + byte[] payload = new byte[data.Length - SCSI_HEADER_SIZE]; + Array.Copy(data, SCSI_HEADER_SIZE, payload, 0, payload.Length); + + _mediaTags[tag] = payload; + AaruLogging.Debug(MODULE_NAME, Localization.Found_media_tag_0, tag); + } +} \ No newline at end of file diff --git a/Aaru.Images/Redumper/Redumper.cs b/Aaru.Images/Redumper/Redumper.cs new file mode 100644 index 000000000..4347274e5 --- /dev/null +++ b/Aaru.Images/Redumper/Redumper.cs @@ -0,0 +1,118 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Redumper.cs +// Author(s) : Rebecca Wallander +// +// Component : Disc image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Manages Redumper raw DVD dump images (.sdram + .state). +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Rebecca Wallander +// ****************************************************************************/ + +using System.Collections.Generic; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Structs; + +namespace Aaru.Images; + +/// +/// +/// Implements reading Redumper raw DVD dump images (.sdram with .state sidecar). +/// The .sdram file stores scrambled DVD RecordingFrames (2366 bytes each, including +/// inner and outer Reed–Solomon parity). The .state file has one byte per frame +/// indicating dump status. The first frame in the file corresponds to physical +/// sector number LBA_START (-0x30000 / -196608). +/// +public sealed partial class Redumper : IOpticalMediaImage +{ + const string MODULE_NAME = "Redumper plugin"; + + /// Size of a single DVD RecordingFrame: 12 rows of (172 main + 10 PI) + 182 PO. + const int RECORDING_FRAME_SIZE = 2366; + + /// Size of a DVD sector without parity (ID + CPR_MAI + user data + EDC). + const int DVD_SECTOR_SIZE = 2064; + + /// DVD user data size. + const int DVD_USER_DATA_SIZE = 2048; + + /// Number of main-data bytes per row in a RecordingFrame. + const int ROW_MAIN_DATA_SIZE = 172; + + /// Number of inner-parity bytes per row. + const int ROW_PARITY_INNER_SIZE = 10; + + /// Number of rows in a RecordingFrame. + const int RECORDING_FRAME_ROWS = 12; + + /// Size of the outer parity block. + const int PARITY_OUTER_SIZE = 182; + + /// + /// First physical LBA stored at file offset 0 in the .sdram/.state files. + /// DVD user-data LBA 0 starts at file index -LBA_START (196608). + /// + const int LBA_START = -0x30000; + + /// SCSI READ DVD STRUCTURE parameter list header size (4 bytes). + const int SCSI_HEADER_SIZE = 4; + + readonly Decoders.DVD.Sector _decoding = new(); + readonly Decoders.Nintendo.Sector _nintendoDecoder = new(); + + /// Derived Nintendo key from LBA 0 so sectors 16+ can be descrambled. + byte? _nintendoDerivedKey; + + IFilter _imageFilter; + ImageInfo _imageInfo; + Dictionary _mediaTags; + byte[] _stateData; + IFilter _sdramFilter; + long _totalFrames; + + public Redumper() => _imageInfo = new ImageInfo + { + ReadableSectorTags = [], + ReadableMediaTags = [], + HasPartitions = true, + HasSessions = true, + Version = null, + Application = "Redumper", + ApplicationVersion = null, + Creator = null, + Comments = null, + MediaManufacturer = null, + MediaModel = null, + MediaSerialNumber = null, + MediaBarcode = null, + MediaPartNumber = null, + MediaSequence = 0, + LastMediaSequence = 0, + DriveManufacturer = null, + DriveModel = null, + DriveSerialNumber = null, + DriveFirmwareRevision = null + }; +} \ No newline at end of file diff --git a/Aaru.Images/Redumper/Verify.cs b/Aaru.Images/Redumper/Verify.cs new file mode 100644 index 000000000..c1d30ac1c --- /dev/null +++ b/Aaru.Images/Redumper/Verify.cs @@ -0,0 +1,105 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Verify.cs +// Author(s) : Rebecca Wallander +// +// Component : Disc image plugins. +// +// --[ Description ] ---------------------------------------------------------- +// +// Verifies Redumper raw DVD dump images. +// +// --[ License ] -------------------------------------------------------------- +// +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation; either version 2.1 of the +// License, or (at your option) any later version. +// +// This library 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 +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Rebecca Wallander +// ****************************************************************************/ + +using System.Collections.Generic; +using Aaru.CommonTypes.Enums; +using Aaru.Decoders.DVD; + +namespace Aaru.Images; + +public sealed partial class Redumper +{ +#region IOpticalMediaImage Members + + /// + public bool? VerifySector(ulong sectorAddress) + { + long frameIndex = (long)sectorAddress - LBA_START; + + if(frameIndex < 0 || frameIndex >= _totalFrames) return null; + + if(MapState(_stateData[frameIndex]) != SectorStatus.Dumped) return null; + + byte[] dvdSector = ReadAndFlattenFrame(frameIndex); + + if(dvdSector is null) return null; + + if(IsNintendoMediaType(_imageInfo.MediaType)) + { + byte[] work = (byte[])dvdSector.Clone(); + + if(!DescrambleNintendo2064InPlace(work, (int)sectorAddress)) return null; + + dvdSector = work; + } + + if(!Sector.CheckIed(dvdSector)) return false; + + return Sector.CheckEdc(dvdSector); + } + + /// + public bool? VerifySectors(ulong sectorAddress, uint length, out List failingLbas, + out List unknownLbas) + { + failingLbas = []; + unknownLbas = []; + + for(ulong i = 0; i < length; i++) + { + bool? result = VerifySector(sectorAddress + i); + + switch(result) + { + case null: + unknownLbas.Add(sectorAddress + i); + + break; + case false: + failingLbas.Add(sectorAddress + i); + + break; + } + } + + if(unknownLbas.Count > 0) return null; + + return failingLbas.Count <= 0; + } + + /// + public bool? VerifySectors(ulong sectorAddress, uint length, uint track, out List failingLbas, + out List unknownLbas) => + VerifySectors(sectorAddress, length, out failingLbas, out unknownLbas); + +#endregion +} \ No newline at end of file