From 030093dd815aa58e721c72a3fbc6344fe6092f1a Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sat, 14 Mar 2026 00:01:09 +0100 Subject: [PATCH 01/16] Check EDC of OmniDrive sectors --- Aaru.Decoders/DVD/Sector.cs | 28 ++++++++++++++++--- Aaru.Devices/Device/ScsiCommands/OmniDrive.cs | 3 ++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Aaru.Decoders/DVD/Sector.cs b/Aaru.Decoders/DVD/Sector.cs index c7e7fd675..dcba08b3f 100644 --- a/Aaru.Decoders/DVD/Sector.cs +++ b/Aaru.Decoders/DVD/Sector.cs @@ -137,6 +137,28 @@ public sealed class Sector return edc; } + /// + /// Check if the EDC of a sector is correct + /// + /// Buffer of the sector + /// True if EDC is correct, False if not + public static bool CheckEdc(byte[] sectorLong) => ComputeEdc(0, sectorLong, 2060) == BigEndianBitConverter.ToUInt32(sectorLong, 2060); + + /// + /// Check if the EDC of sectors is correct + /// + /// Buffer of the sector + /// The number of sectors to check + /// True if EDC is correct, False if not + public static bool CheckEdc(byte[] sectorLong, uint transferLength) + { + for(uint i = 0; i < transferLength; i++) + { + if(!CheckEdc(sectorLong.Skip((int)(i * 2064)).Take(2064).ToArray())) return false; + } + return true; + } + /// /// Tests if a seed unscrambles a sector correctly /// @@ -152,7 +174,7 @@ public sealed class Sector for(var i = 12; i < 2060; i++) tmp[i] ^= LfsrByte(); - return ComputeEdc(0, tmp, 2060) == BigEndianBitConverter.ToUInt32(sector, 2060); + return CheckEdc(tmp); } /// @@ -208,9 +230,7 @@ public sealed class Sector for(var i = 0; i < 2048; i++) scrambled[i + 12] = (byte)(sector[i + 12] ^ cipher[i]); - return ComputeEdc(0, scrambled, 2060) != BigEndianBitConverter.ToUInt32(sector, 2060) - ? ErrorNumber.NotVerifiable - : ErrorNumber.NoError; + return !CheckEdc(scrambled) ? ErrorNumber.NotVerifiable : ErrorNumber.NoError; } public ErrorNumber Scramble(byte[] sector, out byte[] scrambled) diff --git a/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs b/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs index 2d75a548b..7b0e5e9cd 100644 --- a/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs +++ b/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs @@ -35,6 +35,7 @@ using System; using Aaru.CommonTypes.Structs.Devices.SCSI; using Aaru.Logging; +using Aaru.Decoders.DVD; namespace Aaru.Devices; @@ -99,6 +100,8 @@ public partial class Device cdb[11] = 0; // control LastError = SendScsiCommand(cdb, ref buffer, timeout, ScsiDirection.In, out duration, out bool sense); + + if(!Sector.CheckEdc(buffer, transferLength)) return true; Error = LastError != 0; From 44b5a6e3d2125055e9dd008ac49cf79b6923894d Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Wed, 18 Mar 2026 22:29:50 +0100 Subject: [PATCH 02/16] Add Nintendo DVD descrambler --- Aaru.Decoders/DVD/Sector.cs | 64 +++++++++++++++++++ Aaru.Decoders/Nintendo/Sector.cs | 103 +++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 Aaru.Decoders/Nintendo/Sector.cs diff --git a/Aaru.Decoders/DVD/Sector.cs b/Aaru.Decoders/DVD/Sector.cs index dcba08b3f..c6a858185 100644 --- a/Aaru.Decoders/DVD/Sector.cs +++ b/Aaru.Decoders/DVD/Sector.cs @@ -43,6 +43,54 @@ namespace Aaru.Decoders.DVD; [SuppressMessage("ReSharper", "UnusedMember.Global")] public sealed class Sector { + /// Offset of main user data in a standard ECMA-267 DVD sector (bytes 12-2059). + const int DvdMainDataOffset = 12; + + public const int Form1DataSize = 2048; + const int Ecma267TableSize = 16 * Form1DataSize; // 32768 + + static readonly byte[] _ecma267Table = BuildEcma267Table(); + + static byte[] BuildEcma267Table() + { + byte[] table = new byte[Ecma267TableSize]; + ushort shiftRegister = 0x0001; + + for(int t = 0; t < Ecma267TableSize; t++) + { + table[t] = (byte)shiftRegister; + + for(int b = 0; b < 8; b++) + { + int lsb = (shiftRegister >> 14 & 1) ^ (shiftRegister >> 10 & 1); + shiftRegister = (ushort)((shiftRegister << 1 | (ushort)lsb) & 0x7FFF); + } + } + + return table; + } + + /// Gets the Physical Sector Number (PSN) from the sector header (bytes 1-3, big-endian). + public static int GetPsn(byte[] sector) + { + if(sector == null || sector.Length < 4) return 0; + + return (sector[1] << 16) | (sector[2] << 8) | sector[3]; + } + + public static void ApplyTableWithWrap(byte[] sector, int dataStart, int tableOffset) + { + for(int i = 0; i < Form1DataSize; i++) + { + int index = tableOffset + i; + + if(index >= Ecma267TableSize) + index -= Ecma267TableSize - 1; + + sector[dataStart + i] ^= _ecma267Table[index]; + } + } + static readonly ushort[] _ecma267InitialValues = [ 0x0001, 0x5500, 0x0002, 0x2A00, 0x0004, 0x5400, 0x0008, 0x2800, 0x0010, 0x5000, 0x0020, 0x2001, 0x0040, @@ -233,12 +281,28 @@ public sealed class Sector return !CheckEdc(scrambled) ? ErrorNumber.NotVerifiable : ErrorNumber.NoError; } + /// + /// Descrambles a DVD sector. Uses PSN from header (bytes 1-3) to select XOR table. + /// + /// Scrambled 2064-byte sector + /// Descrambled sector output public ErrorNumber Scramble(byte[] sector, out byte[] scrambled) { scrambled = new byte[sector.Length]; if(sector is not { Length: 2064 }) return ErrorNumber.NotSupported; + int psn = GetPsn(sector); + + // Standard DVD: try PSN-based descramble first + int offset = (psn >> 4 & 0xF) * Form1DataSize; + Array.Copy(sector, 0, scrambled, 0, sector.Length); + ApplyTableWithWrap(scrambled, DvdMainDataOffset, offset); + + if(CheckEdc(scrambled)) + return ErrorNumber.NoError; + + // Fallback: seed search for non-standard byte[]? cipher = GetSeed(sector); return cipher == null ? ErrorNumber.UnrecognizedFormat : UnscrambleSector(sector, cipher, out scrambled); diff --git a/Aaru.Decoders/Nintendo/Sector.cs b/Aaru.Decoders/Nintendo/Sector.cs new file mode 100644 index 000000000..e0fa40a16 --- /dev/null +++ b/Aaru.Decoders/Nintendo/Sector.cs @@ -0,0 +1,103 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Sector.cs +// Author(s) : Rebecca Wallander +// +// Component : Device structures decoders. +// +// --[ Description ] ---------------------------------------------------------- +// +// Decodes and descrambles Nintendo (GameCube/Wii) DVD sectors. +// +// --[ 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 +// ****************************************************************************/ + +#nullable enable +using System; +using System.Linq; +using System.Diagnostics.CodeAnalysis; +using Aaru.CommonTypes.Enums; + +namespace Aaru.Decoders.Nintendo; + +[SuppressMessage("ReSharper", "UnusedMember.Global")] +public sealed class Sector +{ + /// Offset of main user data in a Nintendo (GameCube/Wii) DVD sector (bytes 6-2053). + public const int NintendoMainDataOffset = 6; + + /// + /// Derives the Nintendo descramble key from the first 8 bytes of the cpr_mai region (LBA 0 payload). + /// Used when software-descrambling Nintendo sectors. + /// + public static byte DeriveNintendoKey(byte[] cprMaiFirst8) + { + if(cprMaiFirst8 == null || cprMaiFirst8.Length < 8) return 0; + + int sum = 0; + + for(int i = 0; i < 8; i++) + sum += cprMaiFirst8[i]; + + return (byte)(((sum >> 4) + sum) & 0xF); + } + + /// + /// Descrambles a Nintendo DVD sector. Uses PSN from header (bytes 1-3) to select XOR table and Nintendo key. + /// + /// Scrambled 2064-byte sector + /// Nintendo key (0-15) + /// Descrambled sector output + public ErrorNumber Scramble(byte[] sector, byte nintendoKey, out byte[] scrambled) + { + scrambled = new byte[sector.Length]; + + if(sector is not { Length: 2064 }) return ErrorNumber.NotSupported; + + int psn = DVD.Sector.GetPsn(sector); + int mainDataStart = NintendoMainDataOffset; + + int tableOffset = (int)((nintendoKey ^ (psn >> 4 & 0xF)) * DVD.Sector.Form1DataSize + + 7 * DVD.Sector.Form1DataSize + DVD.Sector.Form1DataSize / 2); + Array.Copy(sector, 0, scrambled, 0, sector.Length); + DVD.Sector.ApplyTableWithWrap(scrambled, mainDataStart, tableOffset); + + return DVD.Sector.CheckEdc(scrambled) ? ErrorNumber.NoError : ErrorNumber.NotVerifiable; + } + + public ErrorNumber Scramble(byte[] sector, uint transferLength, byte nintendoKey, out byte[] scrambled) + { + scrambled = new byte[sector.Length]; + + if(sector.Length % 2064 != 0 || sector.Length / 2064 != transferLength) return ErrorNumber.NotSupported; + + for(uint i = 0; i < transferLength; i++) + { + ErrorNumber error = Scramble(sector.Skip((int)(i * 2064)).Take(2064).ToArray(), nintendoKey, out byte[]? currentSector); + + if(error != ErrorNumber.NoError) return error; + + Array.Copy(currentSector, 0, scrambled, i * 2064, 2064); + } + + return ErrorNumber.NoError; + } +} \ No newline at end of file From 491a56c105ec06afb56161340a807df951b698d0 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Fri, 20 Mar 2026 22:08:09 +0100 Subject: [PATCH 03/16] Make the title key resume a hashmap for speed --- Aaru.Core/Devices/Dumping/Dump.cs | 3 + Aaru.Core/Devices/Dumping/Sbc/Data.cs | 6 +- Aaru.Core/Devices/Dumping/Sbc/Dump.cs | 7 +- Aaru.Core/Devices/Dumping/Sbc/Error.cs | 15 ++-- Aaru.Core/Devices/Dumping/Sbc/RawDvd.cs | 4 +- Aaru.Core/Devices/Dumping/Sbc/Trim.cs | 4 +- Aaru.Core/Devices/Dumping/TitleKeys.cs | 100 ++++++++++++++++++++++++ 7 files changed, 121 insertions(+), 18 deletions(-) create mode 100644 Aaru.Core/Devices/Dumping/TitleKeys.cs diff --git a/Aaru.Core/Devices/Dumping/Dump.cs b/Aaru.Core/Devices/Dumping/Dump.cs index ef53151e9..b592cc97d 100644 --- a/Aaru.Core/Devices/Dumping/Dump.cs +++ b/Aaru.Core/Devices/Dumping/Dump.cs @@ -114,6 +114,8 @@ public partial class Dump Database.Models.Device _dbDev; // Device database entry bool _dumpFirstTrackPregap; bool _fixOffset; + HashSet _missingTitleKeysLookup; + bool _missingTitleKeysDirty; uint _maximumReadable; // Maximum number of sectors drive can read at once IMediaGraph _mediaGraph; Resume _resume; @@ -294,6 +296,7 @@ public partial class Dump if(_resume == null || !_doResume) return; _resume.LastWriteDate = DateTime.UtcNow; + SyncMissingTitleKeysToResume(); _resume.BadBlocks.Sort(); if(_createGraph && _mediaGraph is not null) diff --git a/Aaru.Core/Devices/Dumping/Sbc/Data.cs b/Aaru.Core/Devices/Dumping/Sbc/Data.cs index 1b33bb79d..e5b240870 100644 --- a/Aaru.Core/Devices/Dumping/Sbc/Data.cs +++ b/Aaru.Core/Devices/Dumping/Sbc/Data.cs @@ -116,7 +116,7 @@ partial class Dump { if(_aborted) break; - if(!_resume.MissingTitleKeys.Contains(i + j)) + if(!ContainsMissingTitleKey(i + j)) // Key is already dumped. continue; @@ -147,13 +147,13 @@ partial class Dump outputFormat.WriteSectorTag(new byte[5], i + j, false, SectorTagType.DvdTitleKeyDecrypted); - _resume.MissingTitleKeys.Remove(i + j); + MarkTitleKeyDumped(i + j); continue; } outputFormat.WriteSectorTag(titleKey.Value.Key, i + j, false, SectorTagType.DvdSectorTitleKey); - _resume.MissingTitleKeys.Remove(i + j); + MarkTitleKeyDumped(i + j); CSS.DecryptTitleKey(discKey, titleKey.Value.Key, out tmpBuf); outputFormat.WriteSectorTag(tmpBuf, i + j, false, SectorTagType.DvdTitleKeyDecrypted); diff --git a/Aaru.Core/Devices/Dumping/Sbc/Dump.cs b/Aaru.Core/Devices/Dumping/Sbc/Dump.cs index 3dc39aa7a..6c8328b3d 100644 --- a/Aaru.Core/Devices/Dumping/Sbc/Dump.cs +++ b/Aaru.Core/Devices/Dumping/Sbc/Dump.cs @@ -1,4 +1,4 @@ -// /*************************************************************************** +// /*************************************************************************** // Aaru Data Preservation Suite // ---------------------------------------------------------------------------- // @@ -791,6 +791,7 @@ partial class Dump { UpdateStatus?.Invoke(Localization.Core.Title_keys_dumping_is_enabled_This_will_be_very_slow); _resume.MissingTitleKeys ??= [..Enumerable.Range(0, (int)blocks).Select(static n => (ulong)n)]; + InitializeMissingTitleKeysCache(); } if(_dev.ScsiType == PeripheralDeviceTypes.OpticalDevice) @@ -920,7 +921,9 @@ partial class Dump if(_resume.BadBlocks.Count > 0 && !_aborted && _retryPasses > 0) RetrySbcData(scsiReader, currentTry, extents, ref totalDuration, blankExtents, mediaTag ?? null); - if(_resume.MissingTitleKeys?.Count > 0 && + SyncMissingTitleKeysToResume(); + + if(MissingTitleKeyCount() > 0 && !_aborted && _retryPasses > 0 && Settings.Settings.Current.EnableDecryption && diff --git a/Aaru.Core/Devices/Dumping/Sbc/Error.cs b/Aaru.Core/Devices/Dumping/Sbc/Error.cs index a85059f37..b789ec1ab 100644 --- a/Aaru.Core/Devices/Dumping/Sbc/Error.cs +++ b/Aaru.Core/Devices/Dumping/Sbc/Error.cs @@ -316,13 +316,13 @@ partial class Dump { outputFormat.WriteSectorTag(new byte[5], badSector, false, SectorTagType.DvdTitleKeyDecrypted); - _resume.MissingTitleKeys?.Remove(badSector); + MarkTitleKeyDumped(badSector); } else { CSS.DecryptTitleKey(discKey, key, out byte[] tmpBuf); outputFormat.WriteSectorTag(tmpBuf, badSector, false, SectorTagType.DvdTitleKeyDecrypted); - _resume.MissingTitleKeys?.Remove(badSector); + MarkTitleKeyDumped(badSector); cmi[0] = buffer[6]; } @@ -406,7 +406,7 @@ partial class Dump InitProgress?.Invoke(); repeatRetry: - ulong[] tmpArray = _resume.MissingTitleKeys.ToArray(); + ulong[] tmpArray = MissingTitleKeysSnapshot(forward); foreach(ulong missingKey in tmpArray) { @@ -450,7 +450,7 @@ partial class Dump outputFormat.WriteSectorTag(new byte[5], missingKey, false, SectorTagType.DvdTitleKeyDecrypted); - _resume.MissingTitleKeys.Remove(missingKey); + MarkTitleKeyDumped(missingKey); UpdateStatus?.Invoke(string.Format(Localization.Core.Correctly_retried_title_key_0_in_pass_1, missingKey, @@ -459,7 +459,7 @@ partial class Dump else { outputFormat.WriteSectorTag(titleKey.Value.Key, missingKey, false, SectorTagType.DvdSectorTitleKey); - _resume.MissingTitleKeys.Remove(missingKey); + MarkTitleKeyDumped(missingKey); if(discKey != null) { @@ -473,13 +473,10 @@ partial class Dump } } - if(pass < _retryPasses && !_aborted && _resume.MissingTitleKeys.Count > 0) + if(pass < _retryPasses && !_aborted && MissingTitleKeyCount() > 0) { pass++; forward = !forward; - _resume.MissingTitleKeys.Sort(); - - if(!forward) _resume.MissingTitleKeys.Reverse(); goto repeatRetry; } diff --git a/Aaru.Core/Devices/Dumping/Sbc/RawDvd.cs b/Aaru.Core/Devices/Dumping/Sbc/RawDvd.cs index 833fa66f7..c6c4f2efe 100644 --- a/Aaru.Core/Devices/Dumping/Sbc/RawDvd.cs +++ b/Aaru.Core/Devices/Dumping/Sbc/RawDvd.cs @@ -220,14 +220,14 @@ partial class Dump { outputFormat.WriteSectorTag(new byte[5], i + j, false, SectorTagType.DvdTitleKeyDecrypted); - _resume.MissingTitleKeys?.Remove(i + j); + MarkTitleKeyDumped(i + j); continue; } CSS.DecryptTitleKey(discKey, key, out tmpBuf); outputFormat.WriteSectorTag(tmpBuf, i + j, false, SectorTagType.DvdTitleKeyDecrypted); - _resume.MissingTitleKeys?.Remove(i + j); + MarkTitleKeyDumped(i + j); if(_storeEncrypted) continue; diff --git a/Aaru.Core/Devices/Dumping/Sbc/Trim.cs b/Aaru.Core/Devices/Dumping/Sbc/Trim.cs index 74c0ac712..03028cd37 100644 --- a/Aaru.Core/Devices/Dumping/Sbc/Trim.cs +++ b/Aaru.Core/Devices/Dumping/Sbc/Trim.cs @@ -109,13 +109,13 @@ partial class Dump { outputFormat.WriteSectorTag(new byte[5], badSector, false, SectorTagType.DvdTitleKeyDecrypted); - _resume.MissingTitleKeys?.Remove(badSector); + MarkTitleKeyDumped(badSector); } else { CSS.DecryptTitleKey(discKey, key, out byte[] tmpBuf); outputFormat.WriteSectorTag(tmpBuf, badSector, false, SectorTagType.DvdTitleKeyDecrypted); - _resume.MissingTitleKeys?.Remove(badSector); + MarkTitleKeyDumped(badSector); cmi[0] = buffer[6]; } diff --git a/Aaru.Core/Devices/Dumping/TitleKeys.cs b/Aaru.Core/Devices/Dumping/TitleKeys.cs new file mode 100644 index 000000000..661f81045 --- /dev/null +++ b/Aaru.Core/Devices/Dumping/TitleKeys.cs @@ -0,0 +1,100 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : TitleKeys.cs +// Author(s) : Rebecca Wallander +// +// --[ 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 . +// +// ---------------------------------------------------------------------------- +// Copyright © 2020-2026 Rebecca Wallander +// ****************************************************************************/ + +using System.Collections.Generic; +using System.Linq; + +namespace Aaru.Core.Devices.Dumping; + +partial class Dump +{ + void InitializeMissingTitleKeysCache() + { + if(_resume?.MissingTitleKeys is null) + { + _missingTitleKeysLookup = null; + _missingTitleKeysDirty = false; + + return; + } + + _missingTitleKeysLookup = [.._resume.MissingTitleKeys]; + _missingTitleKeysDirty = false; + } + + bool ContainsMissingTitleKey(ulong sector) + { + if(_resume?.MissingTitleKeys is null) return false; + + _missingTitleKeysLookup ??= [.._resume.MissingTitleKeys]; + + return _missingTitleKeysLookup.Contains(sector); + } + + bool MarkTitleKeyDumped(ulong sector) + { + if(_resume?.MissingTitleKeys is null) return false; + + _missingTitleKeysLookup ??= [.._resume.MissingTitleKeys]; + + bool removed = _missingTitleKeysLookup.Remove(sector); + + if(removed) _missingTitleKeysDirty = true; + + return removed; + } + + int MissingTitleKeyCount() + { + if(_resume?.MissingTitleKeys is null) return 0; + + _missingTitleKeysLookup ??= [.._resume.MissingTitleKeys]; + + return _missingTitleKeysLookup.Count; + } + + ulong[] MissingTitleKeysSnapshot(bool forward) + { + if(_resume?.MissingTitleKeys is null) return []; + + _missingTitleKeysLookup ??= [.._resume.MissingTitleKeys]; + + return forward + ? [.._missingTitleKeysLookup.OrderBy(static k => k)] + : [.._missingTitleKeysLookup.OrderByDescending(static k => k)]; + } + + void SyncMissingTitleKeysToResume() + { + if(_resume?.MissingTitleKeys is null || + _missingTitleKeysLookup is null || + !_missingTitleKeysDirty) + return; + + _resume.MissingTitleKeys = [.._missingTitleKeysLookup.OrderBy(static k => k)]; + _missingTitleKeysDirty = false; + } +} From c1f703d8e7811a11844ef5607ff82cc1576bffc5 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Tue, 24 Mar 2026 21:30:25 +0100 Subject: [PATCH 04/16] Start adding NGCW dumping --- Aaru.Core/Devices/Dumping/MMC.cs | 7 +- Aaru.Core/Devices/Dumping/Sbc/Dump.cs | 34 +- Aaru.Core/Devices/Dumping/Sbc/Error.cs | 70 +- Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs | 606 ++++++++++++++++++ Aaru.Core/Devices/Dumping/Sbc/RawDvd.cs | 107 ++-- Aaru.Core/Devices/Dumping/Sbc/Trim.cs | 68 +- Aaru.Core/Devices/Reader.cs | 2 + Aaru.Core/Devices/ReaderSCSI.cs | 28 +- Aaru.Decoders/Nintendo/Sector.cs | 7 +- Aaru.Devices/Device/ScsiCommands/OmniDrive.cs | 89 ++- Aaru.Localization/Core.es.resx | 12 + Aaru.Localization/Core.resx | 12 + Aaru.Localization/UI.Designer.cs | 14 +- Aaru.Localization/UI.es.resx | 6 + Aaru.Localization/UI.resx | 6 + 15 files changed, 936 insertions(+), 132 deletions(-) create mode 100644 Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs diff --git a/Aaru.Core/Devices/Dumping/MMC.cs b/Aaru.Core/Devices/Dumping/MMC.cs index f300a47d5..5770333f5 100644 --- a/Aaru.Core/Devices/Dumping/MMC.cs +++ b/Aaru.Core/Devices/Dumping/MMC.cs @@ -1,4 +1,4 @@ -// /*************************************************************************** +// /*************************************************************************** // Aaru Data Preservation Suite // ---------------------------------------------------------------------------- // @@ -348,10 +348,7 @@ partial class Dump if(nintendoPfi is { DiskCategory: DiskCategory.Nintendo, PartVersion: 15 }) { - StoppingErrorMessage?.Invoke(Localization.Core - .Dumping_Nintendo_GameCube_or_Wii_discs_is_not_yet_implemented); - - return; + dskType = nintendoPfi.Value.DiscSize == DVDSize.Eighty ? MediaType.GOD : MediaType.WOD; } } diff --git a/Aaru.Core/Devices/Dumping/Sbc/Dump.cs b/Aaru.Core/Devices/Dumping/Sbc/Dump.cs index 6c8328b3d..8aed913ed 100644 --- a/Aaru.Core/Devices/Dumping/Sbc/Dump.cs +++ b/Aaru.Core/Devices/Dumping/Sbc/Dump.cs @@ -35,6 +35,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http.Headers; using System.Text.Json; using Aaru.CommonTypes; using Aaru.CommonTypes.AaruMetadata; @@ -337,6 +338,30 @@ partial class Dump } } + bool ngcwMode = dskType is MediaType.GOD or MediaType.WOD; + + if(ngcwMode) + { + if(!scsiReader.OmniDriveReadRaw) + { + StoppingErrorMessage?.Invoke(Localization.Core.Dumping_Nintendo_GameCube_or_Wii_discs_is_not_yet_implemented); + + return; + } + + if(outputFormat.Format != "Aaru") + { + StoppingErrorMessage?.Invoke(string.Format(Localization.Core.Output_format_does_not_support_0, + MediaTagType.NgcwJunkMap)); + + return; + } + + if(blocksToRead > 16) blocksToRead = 16; + } + + scsiReader.OmniDriveNintendoMode = ngcwMode; + ret = true; foreach(MediaTagType tag in mediaTags.Keys.Where(tag => !outputFormat.SupportedMediaTags.Contains(tag))) @@ -406,7 +431,9 @@ partial class Dump ((dskType >= MediaType.DVDROM && dskType <= MediaType.DVDDownload) || dskType == MediaType.PS2DVD || dskType == MediaType.PS3DVD - || dskType == MediaType.Nuon)) + || dskType == MediaType.Nuon + || dskType == MediaType.GOD + || dskType == MediaType.WOD)) nominalNegativeSectors = Math.Min(nominalNegativeSectors, DvdLeadinSectors); mediaTags.TryGetValue(MediaTagType.BD_DI, out byte[] di); @@ -783,6 +810,9 @@ partial class Dump var newTrim = false; + if(ngcwMode && !InitializeNgcwContext(dskType, scsiReader, outputFormat)) + return; + if(mediaTags.TryGetValue(MediaTagType.DVD_CMI, out byte[] cmi) && Settings.Settings.Current.EnableDecryption && _titleKeys && @@ -938,6 +968,8 @@ partial class Dump #endregion Error handling + if(ngcwMode) FinalizeNgcwContext(outputFormat); + if(opticalDisc) { foreach(KeyValuePair tag in mediaTags) diff --git a/Aaru.Core/Devices/Dumping/Sbc/Error.cs b/Aaru.Core/Devices/Dumping/Sbc/Error.cs index b789ec1ab..56018b509 100644 --- a/Aaru.Core/Devices/Dumping/Sbc/Error.cs +++ b/Aaru.Core/Devices/Dumping/Sbc/Error.cs @@ -308,46 +308,56 @@ partial class Dump if(scsiReader.ReadBuffer3CReadRaw || scsiReader.OmniDriveReadRaw || scsiReader.HldtstReadRaw) { - var cmi = new byte[1]; - - byte[] key = buffer.Skip(7).Take(5).ToArray(); - - if(key.All(static k => k == 0)) + if(_ngcwEnabled) { - outputFormat.WriteSectorTag(new byte[5], badSector, false, SectorTagType.DvdTitleKeyDecrypted); - - MarkTitleKeyDumped(badSector); + if(TransformNgcwLongSectors(scsiReader, buffer, badSector, 1, out SectorStatus[] statuses)) + outputFormat.WriteSectorLong(buffer, badSector, false, statuses[0]); + else + outputFormat.WriteSectorLong(buffer, badSector, false, SectorStatus.NotDumped); } else { - CSS.DecryptTitleKey(discKey, key, out byte[] tmpBuf); - outputFormat.WriteSectorTag(tmpBuf, badSector, false, SectorTagType.DvdTitleKeyDecrypted); - MarkTitleKeyDumped(badSector); + var cmi = new byte[1]; - cmi[0] = buffer[6]; - } + byte[] key = buffer.Skip(7).Take(5).ToArray(); - if(!_storeEncrypted) - { - ErrorNumber errno = - outputFormat.ReadSectorsTag(badSector, - false, - 1, - SectorTagType.DvdTitleKeyDecrypted, - out byte[] titleKey); - - if(errno != ErrorNumber.NoError) + if(key.All(static k => k == 0)) { - ErrorMessage?.Invoke(string.Format(Localization.Core - .Error_retrieving_title_key_for_sector_0, - badSector)); + outputFormat.WriteSectorTag(new byte[5], badSector, false, SectorTagType.DvdTitleKeyDecrypted); + + MarkTitleKeyDumped(badSector); } else - buffer = CSS.DecryptSectorLong(buffer, titleKey, cmi); - } + { + CSS.DecryptTitleKey(discKey, key, out byte[] tmpBuf); + outputFormat.WriteSectorTag(tmpBuf, badSector, false, SectorTagType.DvdTitleKeyDecrypted); + MarkTitleKeyDumped(badSector); - _resume.BadBlocks.Remove(badSector); - outputFormat.WriteSectorLong(buffer, badSector, false, SectorStatus.Dumped); + cmi[0] = buffer[6]; + } + + if(!_storeEncrypted) + { + ErrorNumber errno = + outputFormat.ReadSectorsTag(badSector, + false, + 1, + SectorTagType.DvdTitleKeyDecrypted, + out byte[] titleKey); + + if(errno != ErrorNumber.NoError) + { + ErrorMessage?.Invoke(string.Format(Localization.Core + .Error_retrieving_title_key_for_sector_0, + badSector)); + } + else + buffer = CSS.DecryptSectorLong(buffer, titleKey, cmi); + } + + _resume.BadBlocks.Remove(badSector); + outputFormat.WriteSectorLong(buffer, badSector, false, SectorStatus.Dumped); + } } else outputFormat.WriteSector(buffer, badSector, false, SectorStatus.Dumped); diff --git a/Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs b/Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs new file mode 100644 index 000000000..3e8f4994a --- /dev/null +++ b/Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs @@ -0,0 +1,606 @@ +// /*************************************************************************** +// Aaru Data Preservation Suite +// ---------------------------------------------------------------------------- +// +// Filename : Ngcw.cs +// Author(s) : Natalia Portillo +// +// --[ Description ] ---------------------------------------------------------- +// +// NGCW (GameCube/Wii) helpers for OmniDrive raw DVD dumping. +// +// --[ 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 . +// +// ---------------------------------------------------------------------------- +// Copyright © 2011-2026 Natalia Portillo +// Copyright © 2020-2026 Rebecca Wallander +// ****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Linq; +using Aaru.CommonTypes; +using Aaru.CommonTypes.Enums; +using Aaru.CommonTypes.Interfaces; +using Aaru.Core.Image.Ngcw; +using Aaru.Decoders.Nintendo; +using Aaru.Helpers; +using Aaru.Localization; +using NgcwPartitions = Aaru.Core.Image.Ngcw.Partitions; + +namespace Aaru.Core.Devices.Dumping; + +partial class Dump +{ + const int NGCW_LONG_SECTOR_SIZE = 2064; + const int NGCW_PAYLOAD_OFFSET = 6; + const int NGCW_SECTOR_SIZE = 2048; + const int NGCW_SECTORS_PER_GROUP = 16; + + bool _ngcwEnabled; + MediaType _ngcwMediaType; + JunkCollector _ngcwJunkCollector; + List _ngcwPartitions; + DataRegion[][] _ngcwPartDataMaps; + ulong[] _ngcwPartSysEnd; + DataRegion[] _ngcwGcDataMap; + ulong _ngcwGcSysEnd; + + bool _omniDriveNintendoSoftwareDescramble; + byte? _nintendoDerivedDiscKey; + readonly Aaru.Decoders.Nintendo.Sector _nintendoSectorDecoder = new Aaru.Decoders.Nintendo.Sector(); + + bool InitializeNgcwContext(MediaType dskType, Reader scsiReader, IWritableImage outputFormat) + { + _ngcwEnabled = dskType is MediaType.GOD or MediaType.WOD; + _ngcwMediaType = dskType; + _ngcwJunkCollector = new JunkCollector(); + _omniDriveNintendoSoftwareDescramble = scsiReader.OmniDriveNintendoMode; + + if(!_ngcwEnabled) return true; + + if(_omniDriveNintendoSoftwareDescramble) + { + UpdateStatus?.Invoke(UI.Ngcw_nintendo_software_descramble); + + if(!EnsureNintendoDerivedKeyFromLba0(scsiReader)) return false; + } + + if(dskType == MediaType.GOD) + return InitializeGameCubeContext(scsiReader); + + return InitializeWiiContext(scsiReader, outputFormat); + } + + void FinalizeNgcwContext(IWritableImage outputFormat) + { + if(!_ngcwEnabled || _ngcwJunkCollector is null || _ngcwJunkCollector.Count == 0) return; + + byte[] junkMapData = Junk.Serialize(_ngcwJunkCollector.Entries); + outputFormat.WriteMediaTag(junkMapData, MediaTagType.NgcwJunkMap); + UpdateStatus?.Invoke(string.Format(UI.Ngcw_stored_junk_map_0_entries_1_bytes, + _ngcwJunkCollector.Count, + junkMapData.Length)); + } + + bool TransformNgcwLongSectors(Reader scsiReader, byte[] longBuffer, ulong startSector, uint sectors, + out SectorStatus[] statuses) + { + statuses = new SectorStatus[sectors]; + + if(!_ngcwEnabled) + { + for(int i = 0; i < sectors; i++) statuses[i] = SectorStatus.Dumped; + + return true; + } + + if(_omniDriveNintendoSoftwareDescramble) + DescrambleNintendoLongBuffer(longBuffer, startSector, sectors); + + if(_ngcwMediaType == MediaType.GOD) + return TransformGameCubeLongSectors(longBuffer, startSector, sectors, statuses); + + return TransformWiiLongSectors(scsiReader, longBuffer, startSector, sectors, statuses); + } + + bool InitializeGameCubeContext(Reader scsiReader) + { + byte[] extHeader = ReadDiscBytesFromDevice(scsiReader, 0, 0x440); + + if(extHeader == null || extHeader.Length < 0x42C) + { + StoppingErrorMessage?.Invoke(Localization.Core.Unable_to_read_medium); + + return false; + } + + uint fstOffset = BigEndianBitConverter.ToUInt32(extHeader, 0x424); + uint fstSize = BigEndianBitConverter.ToUInt32(extHeader, 0x428); + _ngcwGcSysEnd = fstOffset + fstSize; + + if(fstSize > 0 && fstSize < 64 * 1024 * 1024) + { + byte[] fst = ReadDiscBytesFromDevice(scsiReader, fstOffset, (int)fstSize); + + if(fst != null) _ngcwGcDataMap = DataMap.BuildFromFst(fst, 0, 0); + } + + return true; + } + + bool InitializeWiiContext(Reader scsiReader, IWritableImage outputFormat) + { + UpdateStatus?.Invoke(UI.Ngcw_parsing_partition_table); + _ngcwPartitions = ParseWiiPartitionsFromDevice(scsiReader); + + if(_ngcwPartitions == null) + { + StoppingErrorMessage?.Invoke(Localization.Core.Unable_to_read_medium); + + return false; + } + + WiiPartitionRegion[] regions = NgcwPartitions.BuildRegionMap(_ngcwPartitions); + byte[] keyMapData = NgcwPartitions.SerializeKeyMap(regions); + + outputFormat.WriteMediaTag(keyMapData, MediaTagType.WiiPartitionKeyMap); + UpdateStatus?.Invoke(UI.Ngcw_written_partition_key_map); + + BuildWiiPartitionFstMaps(scsiReader); + + return true; + } + + void BuildWiiPartitionFstMaps(Reader scsiReader) + { + if(_ngcwPartitions == null || _ngcwPartitions.Count == 0) return; + + _ngcwPartDataMaps = new DataRegion[_ngcwPartitions.Count][]; + _ngcwPartSysEnd = new ulong[_ngcwPartitions.Count]; + + for(int p = 0; p < _ngcwPartitions.Count; p++) + { + byte[] encGrp0 = ReadRawPayloadSectors(scsiReader, _ngcwPartitions[p].DataOffset / NGCW_SECTOR_SIZE, 16); + + if(encGrp0 == null || encGrp0.Length < Crypto.GROUP_SIZE) continue; + + byte[] hb0 = new byte[Crypto.GROUP_HASH_SIZE]; + byte[] gd0 = new byte[Crypto.GROUP_DATA_SIZE]; + Crypto.DecryptGroup(_ngcwPartitions[p].TitleKey, encGrp0, hb0, gd0); + + uint fstOffset = BigEndianBitConverter.ToUInt32(gd0, 0x424) << 2; + uint fstSize = BigEndianBitConverter.ToUInt32(gd0, 0x428) << 2; + + _ngcwPartSysEnd[p] = fstOffset + fstSize; + + if(fstSize == 0 || fstSize >= 64 * 1024 * 1024) continue; + + byte[] fstBuffer = new byte[fstSize]; + uint fstRead = 0; + bool ok = true; + + while(fstRead < fstSize) + { + ulong logicalOffset = fstOffset + fstRead; + ulong groupIndex = logicalOffset / Crypto.GROUP_DATA_SIZE; + int groupOffset = (int)(logicalOffset % Crypto.GROUP_DATA_SIZE); + ulong discOffset = _ngcwPartitions[p].DataOffset + groupIndex * Crypto.GROUP_SIZE; + + byte[] encGroup = ReadRawPayloadSectors(scsiReader, discOffset / NGCW_SECTOR_SIZE, 16); + + if(encGroup == null || encGroup.Length < Crypto.GROUP_SIZE) + { + ok = false; + + break; + } + + byte[] hb = new byte[Crypto.GROUP_HASH_SIZE]; + byte[] gd = new byte[Crypto.GROUP_DATA_SIZE]; + Crypto.DecryptGroup(_ngcwPartitions[p].TitleKey, encGroup, hb, gd); + + int available = Crypto.GROUP_DATA_SIZE - groupOffset; + int chunk = fstSize - (int)fstRead < available ? (int)(fstSize - fstRead) : available; + Array.Copy(gd, groupOffset, fstBuffer, fstRead, chunk); + fstRead += (uint)chunk; + } + + if(ok) _ngcwPartDataMaps[p] = DataMap.BuildFromFst(fstBuffer, 0, 2); + } + } + + bool TransformGameCubeLongSectors(byte[] longBuffer, ulong startSector, uint sectors, SectorStatus[] statuses) + { + byte[] payload = new byte[sectors * NGCW_SECTOR_SIZE]; + + for(uint i = 0; i < sectors; i++) + Array.Copy(longBuffer, i * NGCW_LONG_SECTOR_SIZE + NGCW_PAYLOAD_OFFSET, payload, i * NGCW_SECTOR_SIZE, + NGCW_SECTOR_SIZE); + + ulong dataSectors = 0; + ulong junkSectors = 0; + Junk.DetectJunkInBlock(payload, + payload.Length, + startSector * NGCW_SECTOR_SIZE, + _ngcwGcDataMap, + _ngcwGcSysEnd, + 0xFFFF, + _ngcwJunkCollector, + ref dataSectors, + ref junkSectors, + statuses); + + return true; + } + + bool TransformWiiLongSectors(Reader scsiReader, byte[] longBuffer, ulong startSector, uint sectors, SectorStatus[] statuses) + { + ulong discOffset = startSector * NGCW_SECTOR_SIZE; + int partIndex = NgcwPartitions.FindPartitionAtOffset(_ngcwPartitions, discOffset); + + if(partIndex < 0) + { + byte[] payload = new byte[sectors * NGCW_SECTOR_SIZE]; + + for(uint i = 0; i < sectors; i++) + Array.Copy(longBuffer, i * NGCW_LONG_SECTOR_SIZE + NGCW_PAYLOAD_OFFSET, payload, i * NGCW_SECTOR_SIZE, + NGCW_SECTOR_SIZE); + + ulong dataSectors = 0; + ulong junkSectors = 0; + Junk.DetectJunkInBlock(payload, + payload.Length, + discOffset, + null, + 0x50000, + 0xFFFF, + _ngcwJunkCollector, + ref dataSectors, + ref junkSectors, + statuses); + + return true; + } + + ulong groupStartOffset = _ngcwPartitions[partIndex].DataOffset + + (discOffset - _ngcwPartitions[partIndex].DataOffset) / Crypto.GROUP_SIZE * Crypto.GROUP_SIZE; + + byte[] groupPayload; + + if(sectors == NGCW_SECTORS_PER_GROUP && discOffset == groupStartOffset) + { + groupPayload = new byte[Crypto.GROUP_SIZE]; + + for(uint i = 0; i < sectors; i++) + Array.Copy(longBuffer, i * NGCW_LONG_SECTOR_SIZE + NGCW_PAYLOAD_OFFSET, groupPayload, i * NGCW_SECTOR_SIZE, + NGCW_SECTOR_SIZE); + } + else + { + groupPayload = ReadRawPayloadSectors(scsiReader, groupStartOffset / NGCW_SECTOR_SIZE, NGCW_SECTORS_PER_GROUP); + + if(groupPayload == null || groupPayload.Length < Crypto.GROUP_SIZE) return false; + } + + byte[] hashBlock = new byte[Crypto.GROUP_HASH_SIZE]; + byte[] groupData = new byte[Crypto.GROUP_DATA_SIZE]; + Crypto.DecryptGroup(_ngcwPartitions[partIndex].TitleKey, groupPayload, hashBlock, groupData); + + ulong groupNumber = (groupStartOffset - _ngcwPartitions[partIndex].DataOffset) / Crypto.GROUP_SIZE; + ulong logicalOffset = groupNumber * Crypto.GROUP_DATA_SIZE; + bool[] sectorIsData = new bool[NGCW_SECTORS_PER_GROUP]; + int userDataCount = 0; + + for(ulong off = 0; off < Crypto.GROUP_DATA_SIZE; off += NGCW_SECTOR_SIZE) + { + ulong chunk = Crypto.GROUP_DATA_SIZE - off; + + if(chunk > NGCW_SECTOR_SIZE) chunk = NGCW_SECTOR_SIZE; + + if(logicalOffset + off < _ngcwPartSysEnd[partIndex]) + sectorIsData[userDataCount] = true; + else if(_ngcwPartDataMaps[partIndex] != null) + { + sectorIsData[userDataCount] = DataMap.IsDataRegion(_ngcwPartDataMaps[partIndex], logicalOffset + off, chunk); + } + else + sectorIsData[userDataCount] = true; + + userDataCount++; + } + + ulong blockPhase = logicalOffset % Crypto.GROUP_SIZE; + ulong block2Start = blockPhase > 0 ? Crypto.GROUP_SIZE - blockPhase : Crypto.GROUP_DATA_SIZE; + if(block2Start > Crypto.GROUP_DATA_SIZE) block2Start = Crypto.GROUP_DATA_SIZE; + + bool haveSeed1 = false; + uint[] seed1 = new uint[Lfg.SEED_SIZE]; + bool haveSeed2 = false; + uint[] seed2 = new uint[Lfg.SEED_SIZE]; + + for(int s = 0; s < userDataCount; s++) + { + if(sectorIsData[s]) continue; + + ulong sectorOffset = (ulong)s * NGCW_SECTOR_SIZE; + bool inBlock2 = sectorOffset >= block2Start; + + if(inBlock2 && haveSeed2) continue; + if(!inBlock2 && haveSeed1) continue; + + int available = Crypto.GROUP_DATA_SIZE - (int)sectorOffset; + int dataOffset = (int)((logicalOffset + sectorOffset) % Crypto.GROUP_SIZE); + + if(available < Lfg.MIN_SEED_DATA_BYTES) continue; + + uint[] destination = inBlock2 ? seed2 : seed1; + int matched = Lfg.GetSeed(groupData.AsSpan((int)sectorOffset, available), dataOffset, destination); + + if(matched > 0) + { + if(inBlock2) + haveSeed2 = true; + else + haveSeed1 = true; + } + + if(haveSeed1 && haveSeed2) break; + } + + byte[] decryptedGroup = new byte[Crypto.GROUP_SIZE]; + Array.Copy(hashBlock, 0, decryptedGroup, 0, Crypto.GROUP_HASH_SIZE); + + for(int s = 0; s < userDataCount; s++) + { + ulong off = (ulong)s * NGCW_SECTOR_SIZE; + int chunk = Crypto.GROUP_DATA_SIZE - (int)off; + int outOff = Crypto.GROUP_HASH_SIZE + (int)off; + + if(chunk > NGCW_SECTOR_SIZE) chunk = NGCW_SECTOR_SIZE; + + if(sectorIsData[s]) + { + Array.Copy(groupData, (int)off, decryptedGroup, outOff, chunk); + + continue; + } + + bool inBlock2 = off >= block2Start; + bool haveSeed = inBlock2 ? haveSeed2 : haveSeed1; + uint[] seed = inBlock2 ? seed2 : seed1; + + if(!haveSeed) + { + Array.Copy(groupData, (int)off, decryptedGroup, outOff, chunk); + + continue; + } + + uint[] lfgBuffer = new uint[Lfg.MIN_SEED_DATA_BYTES / sizeof(uint)]; + uint[] seedCopy = new uint[Lfg.SEED_SIZE]; + int position = 0; + byte[] expectedData = new byte[NGCW_SECTOR_SIZE]; + Array.Copy(seed, seedCopy, Lfg.SEED_SIZE); + Lfg.SetSeed(lfgBuffer, seedCopy); + + int advance = (int)((logicalOffset + off) % Crypto.GROUP_SIZE); + + if(advance > 0) + { + byte[] discard = new byte[4096]; + int remain = advance; + + while(remain > 0) + { + int step = remain > discard.Length ? discard.Length : remain; + Lfg.GetBytes(lfgBuffer, ref position, discard, 0, step); + remain -= step; + } + } + + Lfg.GetBytes(lfgBuffer, ref position, expectedData, 0, chunk); + + if(groupData.AsSpan((int)off, chunk).SequenceEqual(expectedData.AsSpan(0, chunk))) + { + Array.Clear(decryptedGroup, outOff, chunk); + _ngcwJunkCollector.Add(groupStartOffset + Crypto.GROUP_HASH_SIZE + off, (ulong)chunk, (ushort)partIndex, seed); + } + else + Array.Copy(groupData, (int)off, decryptedGroup, outOff, chunk); + } + + for(uint i = 0; i < sectors; i++) + { + ulong absoluteSector = startSector + i; + int groupIndex = (int)(absoluteSector - groupStartOffset / NGCW_SECTOR_SIZE); + if(groupIndex < 0 || groupIndex >= NGCW_SECTORS_PER_GROUP) continue; + + Array.Copy(decryptedGroup, + groupIndex * NGCW_SECTOR_SIZE, + longBuffer, + i * NGCW_LONG_SECTOR_SIZE + NGCW_PAYLOAD_OFFSET, + NGCW_SECTOR_SIZE); + statuses[i] = SectorStatus.Unencrypted; + } + + return true; + } + + bool EnsureNintendoDerivedKeyFromLba0(Reader scsiReader) + { + if(!_omniDriveNintendoSoftwareDescramble || _nintendoDerivedDiscKey.HasValue) return true; + + bool sense = scsiReader.ReadBlock(out byte[] raw, 0, out _, out _, out _); + + if(sense || _dev.Error || raw == null || raw.Length < NGCW_LONG_SECTOR_SIZE) + { + StoppingErrorMessage?.Invoke(Localization.Core.Unable_to_read_medium); + + return false; + } + + _ = _nintendoSectorDecoder.Scramble(raw, 0, out byte[] descrambled); + + if(descrambled == null) + { + StoppingErrorMessage?.Invoke(Localization.Core.Unable_to_read_medium); + + return false; + } + + byte[] cprMai8 = new byte[8]; + Array.Copy(descrambled, 6, cprMai8, 0, 8); + _nintendoDerivedDiscKey = Sector.DeriveNintendoKey(cprMai8); + UpdateStatus?.Invoke(string.Format(UI.Ngcw_nintendo_derived_key_0, _nintendoDerivedDiscKey.Value)); + + return true; + } + + void DescrambleNintendoLongBuffer(byte[] longBuffer, ulong startSector, uint sectors) + { + if(!_omniDriveNintendoSoftwareDescramble) return; + + for(uint i = 0; i < sectors; i++) + DescrambleNintendo2064At(longBuffer, (int)(i * NGCW_LONG_SECTOR_SIZE), startSector + i); + } + + void DescrambleNintendo2064At(byte[] buffer, int offset, ulong lba) + { + byte[] one = new byte[NGCW_LONG_SECTOR_SIZE]; + Array.Copy(buffer, offset, one, 0, NGCW_LONG_SECTOR_SIZE); + byte key = lba < NGCW_SECTORS_PER_GROUP ? (byte)0 : (_nintendoDerivedDiscKey ?? (byte)0); + + var error = _nintendoSectorDecoder.Scramble(one, key, out byte[] decoded); + + if(error != ErrorNumber.NoError) + return; + + if(decoded != null) Array.Copy(decoded, 0, buffer, offset, NGCW_LONG_SECTOR_SIZE); + + if(lba == 0 && decoded != null) + { + byte[] cprMai8 = new byte[8]; + Array.Copy(decoded, 6, cprMai8, 0, 8); + _nintendoDerivedDiscKey = Sector.DeriveNintendoKey(cprMai8); + } + } + + List ParseWiiPartitionsFromDevice(Reader scsiReader) + { + byte[] partitionTable = ReadDiscBytesFromDevice(scsiReader, 0x40000, 32); + + if(partitionTable == null) return null; + + List partitions = new List(); + uint[] counts = new uint[4]; + uint[] offsets = new uint[4]; + + for(int t = 0; t < 4; t++) + { + counts[t] = BigEndianBitConverter.ToUInt32(partitionTable, t * 8); + offsets[t] = BigEndianBitConverter.ToUInt32(partitionTable, t * 8 + 4); + } + + for(int t = 0; t < 4; t++) + { + if(counts[t] == 0) continue; + + ulong tableOffset = (ulong)offsets[t] << 2; + int tableSize = (int)counts[t] * 8; + byte[] tableData = ReadDiscBytesFromDevice(scsiReader, tableOffset, tableSize); + + if(tableData == null) return null; + + for(uint p = 0; p < counts[t]; p++) + { + ulong partitionOffset = (ulong)BigEndianBitConverter.ToUInt32(tableData, (int)p * 8) << 2; + uint partType = BigEndianBitConverter.ToUInt32(tableData, (int)p * 8 + 4); + byte[] ticket = ReadDiscBytesFromDevice(scsiReader, partitionOffset, 0x2A4); + + if(ticket == null) return null; + + byte[] titleKey = Crypto.DecryptTitleKey(ticket); + byte[] header = ReadDiscBytesFromDevice(scsiReader, partitionOffset + 0x2B8, 8); + + if(header == null) return null; + + ulong dataOffset = partitionOffset + ((ulong)BigEndianBitConverter.ToUInt32(header, 0) << 2); + ulong dataSize = (ulong)BigEndianBitConverter.ToUInt32(header, 4) << 2; + + partitions.Add(new WiiPartition + { + Offset = partitionOffset, + DataOffset = dataOffset, + DataSize = dataSize, + Type = partType, + TitleKey = titleKey + }); + } + } + + return partitions; + } + + byte[] ReadDiscBytesFromDevice(Reader scsiReader, ulong byteOffset, int length) + { + byte[] result = new byte[length]; + int read = 0; + + while(read < length) + { + ulong sector = (byteOffset + (ulong)read) / NGCW_SECTOR_SIZE; + int sectorOff = (int)((byteOffset + (ulong)read) % NGCW_SECTOR_SIZE); + int chunk = NGCW_SECTOR_SIZE - sectorOff; + + if(chunk > length - read) chunk = length - read; + + bool sense = scsiReader.ReadBlock(out byte[] rawSector, sector, out _, out _, out _); + + if(sense || _dev.Error || rawSector == null || rawSector.Length < NGCW_PAYLOAD_OFFSET + NGCW_SECTOR_SIZE) + return null; + + if(_omniDriveNintendoSoftwareDescramble && rawSector.Length >= NGCW_LONG_SECTOR_SIZE) + DescrambleNintendo2064At(rawSector, 0, sector); + + Array.Copy(rawSector, NGCW_PAYLOAD_OFFSET + sectorOff, result, read, chunk); + read += chunk; + } + + return result; + } + + byte[] ReadRawPayloadSectors(Reader scsiReader, ulong startSector, uint count) + { + bool sense = scsiReader.ReadBlocks(out byte[] rawBuffer, startSector, count, out _, out _, out _); + + if(sense || _dev.Error || rawBuffer == null) return null; + + if(_omniDriveNintendoSoftwareDescramble && rawBuffer.Length >= count * NGCW_LONG_SECTOR_SIZE) + { + for(uint i = 0; i < count; i++) + DescrambleNintendo2064At(rawBuffer, (int)(i * NGCW_LONG_SECTOR_SIZE), startSector + i); + } + + byte[] payload = new byte[count * NGCW_SECTOR_SIZE]; + + for(uint i = 0; i < count; i++) + Array.Copy(rawBuffer, i * NGCW_LONG_SECTOR_SIZE + NGCW_PAYLOAD_OFFSET, payload, i * NGCW_SECTOR_SIZE, + NGCW_SECTOR_SIZE); + + return payload; + } +} diff --git a/Aaru.Core/Devices/Dumping/Sbc/RawDvd.cs b/Aaru.Core/Devices/Dumping/Sbc/RawDvd.cs index c6c4f2efe..b54ad7ad3 100644 --- a/Aaru.Core/Devices/Dumping/Sbc/RawDvd.cs +++ b/Aaru.Core/Devices/Dumping/Sbc/RawDvd.cs @@ -209,55 +209,82 @@ partial class Dump _writeStopwatch.Restart(); - byte[] tmpBuf; - var cmi = new byte[blocksToRead]; - - for(uint j = 0; j < blocksToRead; j++) + if(_ngcwEnabled) { - byte[] key = buffer.Skip((int)(2064 * j + 7)).Take(5).ToArray(); - - if(key.All(static k => k == 0)) + if(!TransformNgcwLongSectors(scsiReader, buffer, i, blocksToRead, out SectorStatus[] statuses)) { - outputFormat.WriteSectorTag(new byte[5], i + j, false, SectorTagType.DvdTitleKeyDecrypted); + if(_stopOnError) return; - MarkTitleKeyDumped(i + j); + outputFormat.WriteSectorsLong(new byte[blockSize * _skip], + i, + false, + _skip, + Enumerable.Repeat(SectorStatus.NotDumped, (int)_skip).ToArray()); - continue; - } + for(ulong b = i; b < i + _skip; b++) _resume.BadBlocks.Add(b); - CSS.DecryptTitleKey(discKey, key, out tmpBuf); - outputFormat.WriteSectorTag(tmpBuf, i + j, false, SectorTagType.DvdTitleKeyDecrypted); - MarkTitleKeyDumped(i + j); - - if(_storeEncrypted) continue; - - cmi[j] = buffer[2064 * j + 6]; - } - - // Todo: Flag in the outputFormat that a sector has been decrypted - if(!_storeEncrypted) - { - ErrorNumber errno = - outputFormat.ReadSectorsTag(i, - false, - blocksToRead, - SectorTagType.DvdTitleKeyDecrypted, - out byte[] titleKey); - - if(errno != ErrorNumber.NoError) - { - ErrorMessage?.Invoke(string.Format(Localization.Core.Error_retrieving_title_key_for_sector_0, - i)); + mhddLog.Write(i, cmdDuration < 500 ? 65535 : cmdDuration, _skip); + ibgLog.Write(i, 0); + AaruLogging.WriteLine(Localization.Core.Skipping_0_blocks_from_errored_block_1, _skip, i); + i += _skip - blocksToRead; + newTrim = true; } else - buffer = CSS.DecryptSectorLong(buffer, titleKey, cmi, blocksToRead); + { + outputFormat.WriteSectorsLong(buffer, i, false, blocksToRead, statuses); + } } + else + { + var cmi = new byte[blocksToRead]; - outputFormat.WriteSectorsLong(buffer, - i, - false, - blocksToRead, - Enumerable.Repeat(SectorStatus.Dumped, (int)blocksToRead).ToArray()); + for (uint j = 0; j < blocksToRead; j++) + { + byte[] key = buffer.Skip((int)(2064 * j + 7)).Take(5).ToArray(); + + if(key.All(static k => k == 0)) + { + outputFormat.WriteSectorTag(new byte[5], i + j, false, SectorTagType.DvdTitleKeyDecrypted); + + MarkTitleKeyDumped(i + j); + + continue; + } + + CSS.DecryptTitleKey(discKey, key, out byte[] tmpBuf); + outputFormat.WriteSectorTag(tmpBuf, i + j, false, SectorTagType.DvdTitleKeyDecrypted); + MarkTitleKeyDumped(i + j); + + if(_storeEncrypted) continue; + + cmi[j] = buffer[2064 * j + 6]; + } + + // Todo: Flag in the outputFormat that a sector has been decrypted + if(!_storeEncrypted) + { + ErrorNumber errno = + outputFormat.ReadSectorsTag(i, + false, + blocksToRead, + SectorTagType.DvdTitleKeyDecrypted, + out byte[] titleKey); + + if(errno != ErrorNumber.NoError) + { + ErrorMessage?.Invoke(string.Format(Localization.Core.Error_retrieving_title_key_for_sector_0, + i)); + } + else + buffer = CSS.DecryptSectorLong(buffer, titleKey, cmi, blocksToRead); + } + + outputFormat.WriteSectorsLong(buffer, + i, + false, + blocksToRead, + Enumerable.Repeat(SectorStatus.Dumped, (int)blocksToRead).ToArray()); + } imageWriteDuration += _writeStopwatch.Elapsed.TotalSeconds; extents.Add(i, blocksToRead, true); diff --git a/Aaru.Core/Devices/Dumping/Sbc/Trim.cs b/Aaru.Core/Devices/Dumping/Sbc/Trim.cs index 03028cd37..f23cfda1e 100644 --- a/Aaru.Core/Devices/Dumping/Sbc/Trim.cs +++ b/Aaru.Core/Devices/Dumping/Sbc/Trim.cs @@ -101,45 +101,55 @@ partial class Dump if(scsiReader.ReadBuffer3CReadRaw || scsiReader.OmniDriveReadRaw || scsiReader.HldtstReadRaw) { - var cmi = new byte[1]; - - byte[] key = buffer.Skip(7).Take(5).ToArray(); - - if(key.All(static k => k == 0)) + if(_ngcwEnabled) { - outputFormat.WriteSectorTag(new byte[5], badSector, false, SectorTagType.DvdTitleKeyDecrypted); - - MarkTitleKeyDumped(badSector); + if(TransformNgcwLongSectors(scsiReader, buffer, badSector, 1, out SectorStatus[] statuses)) + outputFormat.WriteSectorLong(buffer, badSector, false, statuses[0]); + else + outputFormat.WriteSectorLong(buffer, badSector, false, SectorStatus.NotDumped); } else { - CSS.DecryptTitleKey(discKey, key, out byte[] tmpBuf); - outputFormat.WriteSectorTag(tmpBuf, badSector, false, SectorTagType.DvdTitleKeyDecrypted); - MarkTitleKeyDumped(badSector); + var cmi = new byte[1]; - cmi[0] = buffer[6]; - } + byte[] key = buffer.Skip(7).Take(5).ToArray(); - if(!_storeEncrypted) - { - ErrorNumber errno = - outputFormat.ReadSectorsTag(badSector, - false, - 1, - SectorTagType.DvdTitleKeyDecrypted, - out byte[] titleKey); - - if(errno != ErrorNumber.NoError) + if(key.All(static k => k == 0)) { - ErrorMessage?.Invoke(string.Format(Localization.Core.Error_retrieving_title_key_for_sector_0, - badSector)); + outputFormat.WriteSectorTag(new byte[5], badSector, false, SectorTagType.DvdTitleKeyDecrypted); + + MarkTitleKeyDumped(badSector); } else - buffer = CSS.DecryptSectorLong(buffer, titleKey, cmi); - } + { + CSS.DecryptTitleKey(discKey, key, out byte[] tmpBuf); + outputFormat.WriteSectorTag(tmpBuf, badSector, false, SectorTagType.DvdTitleKeyDecrypted); + MarkTitleKeyDumped(badSector); - _resume.BadBlocks.Remove(badSector); - outputFormat.WriteSectorLong(buffer, badSector, false, SectorStatus.Dumped); + cmi[0] = buffer[6]; + } + + if(!_storeEncrypted) + { + ErrorNumber errno = + outputFormat.ReadSectorsTag(badSector, + false, + 1, + SectorTagType.DvdTitleKeyDecrypted, + out byte[] titleKey); + + if(errno != ErrorNumber.NoError) + { + ErrorMessage?.Invoke(string.Format(Localization.Core.Error_retrieving_title_key_for_sector_0, + badSector)); + } + else + buffer = CSS.DecryptSectorLong(buffer, titleKey, cmi); + } + + _resume.BadBlocks.Remove(badSector); + outputFormat.WriteSectorLong(buffer, badSector, false, SectorStatus.Dumped); + } } else outputFormat.WriteSector(buffer, badSector, false, SectorStatus.Dumped); diff --git a/Aaru.Core/Devices/Reader.cs b/Aaru.Core/Devices/Reader.cs index b396ab0c7..e3389052a 100644 --- a/Aaru.Core/Devices/Reader.cs +++ b/Aaru.Core/Devices/Reader.cs @@ -77,6 +77,8 @@ sealed partial class Reader internal uint PhysicalBlockSize { get; private set; } internal uint LongBlockSize { get; private set; } internal bool CanReadRaw { get; private set; } + /// When true with OmniDrive raw reads, use descramble=0 and software Nintendo descrambling (GameCube/Wii). + internal bool OmniDriveNintendoMode { get; set; } internal bool CanSeek => _ataSeek || _seek6 || _seek10; internal bool CanSeekLba => _ataSeekLba || _seek6 || _seek10; diff --git a/Aaru.Core/Devices/ReaderSCSI.cs b/Aaru.Core/Devices/ReaderSCSI.cs index a13767160..58a72c3ed 100644 --- a/Aaru.Core/Devices/ReaderSCSI.cs +++ b/Aaru.Core/Devices/ReaderSCSI.cs @@ -588,11 +588,12 @@ sealed partial class Reader ReadBuffer3CReadRaw = !_dev.ReadBuffer3CRawDvd(out _, out senseBuf, 0, 1, _timeout, out _, layerbreak, otp); - // Try OmniDrive on drives with OmniDrive firmware + // Try OmniDrive on drives with OmniDrive firmware (standard descramble=1 and Nintendo descramble=0) if(_dev.IsOmniDriveFirmware()) { - OmniDriveReadRaw = - !_dev.OmniDriveReadRawDvd(out _, out senseBuf, 0, 1, _timeout, out _); + bool omniStandardOk = !_dev.OmniDriveReadRawDvd(out _, out senseBuf, 0, 1, _timeout, out _); + bool omniNintendoOk = !_dev.OmniDriveReadNintendoDvd(out _, out senseBuf, 0, 1, _timeout, out _); + OmniDriveReadRaw = omniStandardOk || omniNintendoOk; } if(HldtstReadRaw || _plextorReadRaw || ReadBuffer3CReadRaw || OmniDriveReadRaw) @@ -848,12 +849,21 @@ sealed partial class Reader else if(OmniDriveReadRaw) { uint lba = negative ? (uint)(-(long)block) : (uint)block; - sense = _dev.OmniDriveReadRawDvd(out buffer, - out senseBuf, - lba, - count, - _timeout, - out duration); + + if(OmniDriveNintendoMode) + sense = _dev.OmniDriveReadNintendoDvd(out buffer, + out senseBuf, + lba, + count, + _timeout, + out duration); + else + sense = _dev.OmniDriveReadRawDvd(out buffer, + out senseBuf, + lba, + count, + _timeout, + out duration); } else if(ReadBuffer3CReadRaw) { diff --git a/Aaru.Decoders/Nintendo/Sector.cs b/Aaru.Decoders/Nintendo/Sector.cs index e0fa40a16..27c7582b6 100644 --- a/Aaru.Decoders/Nintendo/Sector.cs +++ b/Aaru.Decoders/Nintendo/Sector.cs @@ -41,8 +41,11 @@ namespace Aaru.Decoders.Nintendo; [SuppressMessage("ReSharper", "UnusedMember.Global")] public sealed class Sector { - /// Offset of main user data in a Nintendo (GameCube/Wii) DVD sector (bytes 6-2053). - public const int NintendoMainDataOffset = 6; + /// + /// 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. + /// + public const int NintendoMainDataOffset = 12; /// /// Derives the Nintendo descramble key from the first 8 bytes of the cpr_mai region (LBA 0 payload). diff --git a/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs b/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs index 7b0e5e9cd..dec0d670c 100644 --- a/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs +++ b/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs @@ -41,6 +41,45 @@ namespace Aaru.Devices; public partial class Device { + enum OmniDriveDiscType + { + CD = 0, + DVD = 1, + BD = 2 + } + + /// + /// Encodes byte 1 of the OmniDrive READ CDB to match redumper's CDB12_ReadOmniDrive + /// (scsi/mmc.ixx): disc_type :2 (LSB), raw_addressing :1, fua :1, descramble :1, reserved :3. + /// + /// 0 = CD, 1 = DVD, 2 = BD (redumper OmniDrive_DiscType). + static byte EncodeOmniDriveReadCdb1(OmniDriveDiscType discType, bool rawAddressing, bool fua, bool descramble) + { + int d = (byte)discType & 3; + int r = rawAddressing ? 1 : 0; + int f = fua ? 1 : 0; + int s = descramble ? 1 : 0; + + return (byte)(d | (r << 2) | (f << 3) | (s << 4)); + } + + static void FillOmniDriveReadDvdCdb(Span cdb, uint lba, uint transferLength, byte cdbByte1) + { + cdb.Clear(); + cdb[0] = (byte)ScsiCommands.ReadOmniDrive; + cdb[1] = cdbByte1; + cdb[2] = (byte)((lba >> 24) & 0xFF); + cdb[3] = (byte)((lba >> 16) & 0xFF); + cdb[4] = (byte)((lba >> 8) & 0xFF); + cdb[5] = (byte)(lba & 0xFF); + cdb[6] = (byte)((transferLength >> 24) & 0xFF); + cdb[7] = (byte)((transferLength >> 16) & 0xFF); + cdb[8] = (byte)((transferLength >> 8) & 0xFF); + cdb[9] = (byte)(transferLength & 0xFF); + cdb[10] = 0; // subchannels=NONE, c2=0 + cdb[11] = 0; // control + } + /// /// Checks if the drive has OmniDrive firmware by inspecting INQUIRY Reserved5 (bytes 74+) for "OmniDrive", /// matching redumper's is_omnidrive_firmware behaviour. @@ -77,27 +116,19 @@ public partial class Device /// Number of 2064-byte sectors to read. /// Timeout in seconds. /// Duration in milliseconds it took for the device to execute the command. + /// Set to true if the command should use FUA. + /// Set to true if the data should be descrambled by the device. public bool OmniDriveReadRawDvd(out byte[] buffer, out ReadOnlySpan senseBuffer, uint lba, uint transferLength, - uint timeout, out double duration) + uint timeout, out double duration, bool fua = false, bool descramble = true) { senseBuffer = SenseBuffer; Span cdb = CdbBuffer[..12]; - cdb.Clear(); - buffer = new byte[2064 * transferLength]; - cdb[0] = (byte)ScsiCommands.ReadOmniDrive; - cdb[1] = 0x11; // disc_type=1 (DVD), raw_addressing=0 (LBA), fua=0, descramble=1 - cdb[2] = (byte)((lba >> 24) & 0xFF); - cdb[3] = (byte)((lba >> 16) & 0xFF); - cdb[4] = (byte)((lba >> 8) & 0xFF); - cdb[5] = (byte)(lba & 0xFF); - cdb[6] = (byte)((transferLength >> 24) & 0xFF); - cdb[7] = (byte)((transferLength >> 16) & 0xFF); - cdb[8] = (byte)((transferLength >> 8) & 0xFF); - cdb[9] = (byte)(transferLength & 0xFF); - cdb[10] = 0; // subchannels=NONE, c2=0 - cdb[11] = 0; // control + FillOmniDriveReadDvdCdb(cdb, + lba, + transferLength, + EncodeOmniDriveReadCdb1(OmniDriveDiscType.DVD, false, fua, descramble)); LastError = SendScsiCommand(cdb, ref buffer, timeout, ScsiDirection.In, out duration, out bool sense); @@ -109,4 +140,32 @@ public partial class Device return sense; } + + /// + /// Reads raw Nintendo GameCube/Wii DVD sectors (2064 bytes) on OmniDrive. Default matches redumper raw DVD + /// (descramble off); use software descramble via Aaru.Decoders.Nintendo.Sector when needed. + /// + /// Drive-side DVD descramble (redumper raw DVD uses false). + /// true if the command failed and contains the sense buffer. + public bool OmniDriveReadNintendoDvd(out byte[] buffer, out ReadOnlySpan senseBuffer, uint lba, uint transferLength, + uint timeout, out double duration, bool descramble = false) + { + senseBuffer = SenseBuffer; + Span cdb = CdbBuffer[..12]; + buffer = new byte[2064 * transferLength]; + + FillOmniDriveReadDvdCdb(cdb, + lba, + transferLength, + EncodeOmniDriveReadCdb1(OmniDriveDiscType.DVD, false, false, descramble)); + + LastError = SendScsiCommand(cdb, ref buffer, timeout, ScsiDirection.In, out duration, out bool sense); + + // Scrambled Nintendo sectors do not pass standard DVD EDC until software-descrambled. + Error = LastError != 0; + + AaruLogging.Debug(SCSI_MODULE_NAME, "OmniDrive READ NINTENDO DVD took {0} ms", duration); + + return sense; + } } diff --git a/Aaru.Localization/Core.es.resx b/Aaru.Localization/Core.es.resx index cc56a0a9d..f51bef025 100644 --- a/Aaru.Localization/Core.es.resx +++ b/Aaru.Localization/Core.es.resx @@ -925,6 +925,18 @@ [red]El volcado de discos de Nintendo GameCube o Wii no está implementado todavía.[/] + + [red]El volcado de discos de Nintendo GameCube o Wii con OmniDrive requiere formato de salida Aaru.[/] + + + [slateblue1]Analizando tabla de particiones de Wii...[/] + + + [slateblue1]Se guardó el mapa de claves de partición de Wii.[/] + + + [slateblue1]Se guardó el mapa de basura de Nintendo con [lime]{0}[/] entradas.[/] + ################# Registro de progreso del volcado ################# diff --git a/Aaru.Localization/Core.resx b/Aaru.Localization/Core.resx index 88999eaeb..7500469b5 100644 --- a/Aaru.Localization/Core.resx +++ b/Aaru.Localization/Core.resx @@ -1395,6 +1395,18 @@ [red]Dumping Nintendo GameCube or Wii discs is not yet implemented.[/] + + [red]Dumping Nintendo GameCube or Wii discs with OmniDrive requires Aaru output format.[/] + + + [slateblue1]Parsing Wii partition table...[/] + + + [slateblue1]Stored Wii partition key map.[/] + + + [slateblue1]Stored Nintendo junk map with [lime]{0}[/] entries.[/] + [slateblue1]Reading [italic]Disc Manufacturing Information[/][/] diff --git a/Aaru.Localization/UI.Designer.cs b/Aaru.Localization/UI.Designer.cs index b74abafe5..15672f15f 100644 --- a/Aaru.Localization/UI.Designer.cs +++ b/Aaru.Localization/UI.Designer.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by a tool. // @@ -1983,6 +1983,18 @@ namespace Aaru.Localization { } } + public static string Ngcw_nintendo_software_descramble { + get { + return ResourceManager.GetString("Ngcw_nintendo_software_descramble", resourceCulture); + } + } + + public static string Ngcw_nintendo_derived_key_0 { + get { + return ResourceManager.GetString("Ngcw_nintendo_derived_key_0", resourceCulture); + } + } + public static string PS3_disc_key_resolved_from_0 { get { return ResourceManager.GetString("PS3_disc_key_resolved_from_0", resourceCulture); diff --git a/Aaru.Localization/UI.es.resx b/Aaru.Localization/UI.es.resx index 57d6b1668..211104bac 100644 --- a/Aaru.Localization/UI.es.resx +++ b/Aaru.Localization/UI.es.resx @@ -663,6 +663,12 @@ [slateblue1]Número de disco: [green]{0}[/].[/] + + + [slateblue1]Descifrado por software de Nintendo activado (OmniDrive en bruto, descramble=0).[/] + + + [slateblue1]Clave de disco Nintendo derivada: [green]{0}[/].[/] [slateblue1]Clave de disco PS3 resuelta desde [aqua]{0}[/].[/] diff --git a/Aaru.Localization/UI.resx b/Aaru.Localization/UI.resx index 6ee530635..f39e0c4a6 100644 --- a/Aaru.Localization/UI.resx +++ b/Aaru.Localization/UI.resx @@ -1002,6 +1002,12 @@ In you are unsure, please press N to not continue. [slateblue1]Disc number: [green]{0}[/].[/] + + + [slateblue1]Nintendo software descrambling enabled (OmniDrive raw, descramble=0).[/] + + + [slateblue1]Derived Nintendo disc key: [green]{0}[/].[/] [slateblue1]PS3 disc key resolved from [aqua]{0}[/].[/] From b0e302e4a7c9652d70f27e4cdf0e19bc17e22d6a Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Tue, 24 Mar 2026 21:34:37 +0100 Subject: [PATCH 05/16] mark sector bad if edc fail --- Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs | 42 +++++++++++++++++++-------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs b/Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs index 3e8f4994a..3e119bb13 100644 --- a/Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs +++ b/Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs @@ -108,8 +108,9 @@ partial class Dump return true; } - if(_omniDriveNintendoSoftwareDescramble) - DescrambleNintendoLongBuffer(longBuffer, startSector, sectors); + if(_omniDriveNintendoSoftwareDescramble && + !DescrambleNintendoLongBuffer(longBuffer, startSector, sectors)) + return false; if(_ngcwMediaType == MediaType.GOD) return TransformGameCubeLongSectors(longBuffer, startSector, sectors, statuses); @@ -453,9 +454,9 @@ partial class Dump return false; } - _ = _nintendoSectorDecoder.Scramble(raw, 0, out byte[] descrambled); + ErrorNumber errno = _nintendoSectorDecoder.Scramble(raw, 0, out byte[] descrambled); - if(descrambled == null) + if(errno != ErrorNumber.NoError || descrambled == null) { StoppingErrorMessage?.Invoke(Localization.Core.Unable_to_read_medium); @@ -470,24 +471,33 @@ partial class Dump return true; } - void DescrambleNintendoLongBuffer(byte[] longBuffer, ulong startSector, uint sectors) + bool DescrambleNintendoLongBuffer(byte[] longBuffer, ulong startSector, uint sectors) { - if(!_omniDriveNintendoSoftwareDescramble) return; + if(!_omniDriveNintendoSoftwareDescramble) return true; for(uint i = 0; i < sectors; i++) - DescrambleNintendo2064At(longBuffer, (int)(i * NGCW_LONG_SECTOR_SIZE), startSector + i); + { + if(!DescrambleNintendo2064At(longBuffer, (int)(i * NGCW_LONG_SECTOR_SIZE), startSector + i)) + return false; + } + + return true; } - void DescrambleNintendo2064At(byte[] buffer, int offset, ulong lba) + bool DescrambleNintendo2064At(byte[] buffer, int offset, ulong lba) { byte[] one = new byte[NGCW_LONG_SECTOR_SIZE]; Array.Copy(buffer, offset, one, 0, NGCW_LONG_SECTOR_SIZE); byte key = lba < NGCW_SECTORS_PER_GROUP ? (byte)0 : (_nintendoDerivedDiscKey ?? (byte)0); - var error = _nintendoSectorDecoder.Scramble(one, key, out byte[] decoded); + ErrorNumber error = _nintendoSectorDecoder.Scramble(one, key, out byte[] decoded); if(error != ErrorNumber.NoError) - return; + { + Array.Clear(buffer, offset, NGCW_LONG_SECTOR_SIZE); + + return false; + } if(decoded != null) Array.Copy(decoded, 0, buffer, offset, NGCW_LONG_SECTOR_SIZE); @@ -497,6 +507,8 @@ partial class Dump Array.Copy(decoded, 6, cprMai8, 0, 8); _nintendoDerivedDiscKey = Sector.DeriveNintendoKey(cprMai8); } + + return true; } List ParseWiiPartitionsFromDevice(Reader scsiReader) @@ -574,7 +586,10 @@ partial class Dump return null; if(_omniDriveNintendoSoftwareDescramble && rawSector.Length >= NGCW_LONG_SECTOR_SIZE) - DescrambleNintendo2064At(rawSector, 0, sector); + { + if(!DescrambleNintendo2064At(rawSector, 0, sector)) + return null; + } Array.Copy(rawSector, NGCW_PAYLOAD_OFFSET + sectorOff, result, read, chunk); read += chunk; @@ -592,7 +607,10 @@ partial class Dump if(_omniDriveNintendoSoftwareDescramble && rawBuffer.Length >= count * NGCW_LONG_SECTOR_SIZE) { for(uint i = 0; i < count; i++) - DescrambleNintendo2064At(rawBuffer, (int)(i * NGCW_LONG_SECTOR_SIZE), startSector + i); + { + if(!DescrambleNintendo2064At(rawBuffer, (int)(i * NGCW_LONG_SECTOR_SIZE), startSector + i)) + return null; + } } byte[] payload = new byte[count * NGCW_SECTOR_SIZE]; From f1434ef33f49ac0b243e70bead2ee79f63929495 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Fri, 27 Mar 2026 20:22:44 +0100 Subject: [PATCH 06/16] 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 From 326de0a5658e95bcc704f26b144c8c0f5e1f9db9 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sat, 28 Mar 2026 20:24:23 +0100 Subject: [PATCH 07/16] Move color coding of CPR_MAI on Nintendo discs --- Aaru.Gui/ViewModels/Windows/ViewSectorViewModel.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Aaru.Gui/ViewModels/Windows/ViewSectorViewModel.cs b/Aaru.Gui/ViewModels/Windows/ViewSectorViewModel.cs index 06c82c0c5..79c2de483 100644 --- a/Aaru.Gui/ViewModels/Windows/ViewSectorViewModel.cs +++ b/Aaru.Gui/ViewModels/Windows/ViewSectorViewModel.cs @@ -33,6 +33,7 @@ using System.Collections.Generic; using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Interfaces; +using Aaru.CommonTypes.Metadata; using Aaru.Gui.Controls; using Avalonia.Media; using CommunityToolkit.Mvvm.ComponentModel; @@ -136,6 +137,15 @@ public sealed partial class ViewSectorViewModel : ViewModelBase End = 11 }; + if (_inputFormat.Info.MediaType is CommonTypes.MediaType.GOD or CommonTypes.MediaType.WOD){ + dvd_cprmai = new ColorRange + { + Color = Brushes.Orange, + Start = 2054, + End = 2059 + }; + } + ColorRange dvd_edc = new ColorRange { Color = Brushes.LimeGreen, From 0f275686ce60e26c3a136b5cddc9afae61c415a0 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sat, 28 Mar 2026 20:49:54 +0100 Subject: [PATCH 08/16] Fix verify for redumper --- Aaru.Images/Redumper/Verify.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/Aaru.Images/Redumper/Verify.cs b/Aaru.Images/Redumper/Verify.cs index c1d30ac1c..ae6a6c788 100644 --- a/Aaru.Images/Redumper/Verify.cs +++ b/Aaru.Images/Redumper/Verify.cs @@ -49,22 +49,13 @@ public sealed partial class Redumper if(MapState(_stateData[frameIndex]) != SectorStatus.Dumped) return null; - byte[] dvdSector = ReadAndFlattenFrame(frameIndex); + ErrorNumber errno = ReadSectorLong(sectorAddress, false, out byte[] dvdSector, out _); - 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(errno != ErrorNumber.NoError || dvdSector is null) return null; if(!Sector.CheckIed(dvdSector)) return false; - return Sector.CheckEdc(dvdSector); + return Sector.CheckEdc(dvdSector, 1); } /// From 2414a8a780103de3ff3522a3988009674112f185 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sat, 28 Mar 2026 20:58:30 +0100 Subject: [PATCH 09/16] Convert negative sectors on Nintendo discs --- Aaru.Core/Image/Convert/Convert.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Aaru.Core/Image/Convert/Convert.cs b/Aaru.Core/Image/Convert/Convert.cs index acefaf57a..ad5957b2c 100644 --- a/Aaru.Core/Image/Convert/Convert.cs +++ b/Aaru.Core/Image/Convert/Convert.cs @@ -367,16 +367,14 @@ public partial class Convert } } - // TODO: Enable for Ngcw - if(!isPs3Conversion && !isWiiuConversion && !isNgcwConversion && _negativeSectors > 0) + if(!isPs3Conversion && !isWiiuConversion && _negativeSectors > 0) { errno = ConvertNegativeSectors(useLong); if(errno != ErrorNumber.NoError) return errno; } - // TODO: Enable for Ngcw - if(!isPs3Conversion && !isWiiuConversion && !isNgcwConversion && _overflowSectors > 0) + if(!isPs3Conversion && !isWiiuConversion && _overflowSectors > 0) { errno = ConvertOverflowSectors(useLong); From 0dfef8ccb31e73d22b9285d9a35c9079230c1a4d Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Sun, 29 Mar 2026 12:10:53 +0200 Subject: [PATCH 10/16] Dump NGCW --- Aaru.Core/Devices/Dumping/Dump.cs | 5 +- Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs | 101 +--- Aaru.Core/Devices/Reader.cs | 6 + Aaru.Core/Devices/ReaderSCSI.cs | 9 +- Aaru.Core/Image/Convert/Ngcw/ConvertNgcw.cs | 555 +++++++++++++----- Aaru.Decoders/DVD/Sector.cs | 15 + Aaru.Devices/Device/ScsiCommands/OmniDrive.cs | 99 +++- .../ViewModels/Windows/MediaDumpViewModel.cs | 3 +- Aaru.Localization/UI.Designer.cs | 6 + Aaru.Localization/UI.es.resx | 3 + Aaru.Localization/UI.resx | 5 +- Aaru/Commands/Media/Dump.cs | 8 +- 12 files changed, 555 insertions(+), 260 deletions(-) diff --git a/Aaru.Core/Devices/Dumping/Dump.cs b/Aaru.Core/Devices/Dumping/Dump.cs index b592cc97d..08b6e0a9e 100644 --- a/Aaru.Core/Devices/Dumping/Dump.cs +++ b/Aaru.Core/Devices/Dumping/Dump.cs @@ -104,6 +104,7 @@ public partial class Dump readonly Stopwatch _speedStopwatch; readonly bool _stopOnError; readonly bool _storeEncrypted; + readonly bool _bypassWiiDecryption; readonly DumpSubchannel _subchannel; readonly bool _titleKeys; readonly bool _trim; @@ -171,6 +172,7 @@ public partial class Dump /// Dimensions of graph in pixels for a square /// Check sectors integrity before writing to image /// Try to fix sectors integrity + /// When dumping Wii (WOD), skip partition AES decryption and store encrypted data public Dump(bool doResume, Device dev, string devicePath, IBaseWritableImage outputPlugin, ushort retryPasses, bool force, bool dumpRaw, bool persistent, bool stopOnError, Resume resume, Encoding encoding, string outputPrefix, string outputPath, Dictionary formatOptions, Metadata preSidecar, @@ -179,7 +181,7 @@ public partial class Dump bool fixSubchannel, bool fixSubchannelCrc, bool skipCdireadyHole, ErrorLog errorLog, bool generateSubchannels, uint maximumReadable, bool useBufferedReads, bool storeEncrypted, bool titleKeys, uint ignoreCdrRunOuts, bool createGraph, uint dimensions, bool paranoia, - bool cureParanoia) + bool cureParanoia, bool bypassWiiDecryption) { _doResume = doResume; _dev = dev; @@ -223,6 +225,7 @@ public partial class Dump _dimensions = dimensions; _paranoia = paranoia; _cureParanoia = cureParanoia; + _bypassWiiDecryption = bypassWiiDecryption; _dumpStopwatch = new Stopwatch(); _sidecarStopwatch = new Stopwatch(); _speedStopwatch = new Stopwatch(); diff --git a/Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs b/Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs index 76f4f2e37..eebfe0490 100644 --- a/Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs +++ b/Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs @@ -58,21 +58,17 @@ partial class Dump ulong[] _ngcwPartSysEnd; DataRegion[] _ngcwGcDataMap; ulong _ngcwGcSysEnd; - - bool _omniDriveNintendoSoftwareDescramble; byte? _nintendoDerivedDiscKey; - readonly Aaru.Decoders.Nintendo.Sector _nintendoSectorDecoder = new Aaru.Decoders.Nintendo.Sector(); bool InitializeNgcwContext(MediaType dskType, Reader scsiReader, IWritableImage outputFormat) { _ngcwEnabled = dskType is MediaType.GOD or MediaType.WOD; _ngcwMediaType = dskType; _ngcwJunkCollector = new JunkCollector(); - _omniDriveNintendoSoftwareDescramble = scsiReader.OmniDriveNintendoMode; if(!_ngcwEnabled) return true; - if(_omniDriveNintendoSoftwareDescramble) + if(scsiReader.OmniDriveNintendoMode) { UpdateStatus?.Invoke(UI.Ngcw_nintendo_software_descramble); @@ -108,10 +104,6 @@ partial class Dump return true; } - if(_omniDriveNintendoSoftwareDescramble && - !DescrambleNintendoLongBuffer(longBuffer, startSector, sectors)) - return false; - if(_ngcwMediaType == MediaType.GOD) return TransformGameCubeLongSectors(longBuffer, startSector, sectors, statuses); @@ -155,6 +147,15 @@ partial class Dump return false; } + UpdateStatus?.Invoke(string.Format(UI.Ngcw_found_0_partitions, _ngcwPartitions.Count)); + + if(_bypassWiiDecryption) + { + UpdateStatus?.Invoke(UI.Ngcw_wii_dump_bypass_decryption); + + return true; + } + WiiPartitionRegion[] regions = NgcwPartitions.BuildRegionMap(_ngcwPartitions); byte[] keyMapData = NgcwPartitions.SerializeKeyMap(regions); @@ -253,6 +254,13 @@ partial class Dump ulong discOffset = startSector * NGCW_SECTOR_SIZE; int partIndex = NgcwPartitions.FindPartitionAtOffset(_ngcwPartitions, discOffset); + if(_bypassWiiDecryption && partIndex >= 0) + { + for(int i = 0; i < sectors; i++) statuses[i] = SectorStatus.Encrypted; + + return true; + } + if(partIndex < 0) { byte[] payload = new byte[sectors * NGCW_SECTOR_SIZE]; @@ -443,7 +451,9 @@ partial class Dump bool EnsureNintendoDerivedKeyFromLba0(Reader scsiReader) { - if(!_omniDriveNintendoSoftwareDescramble || _nintendoDerivedDiscKey.HasValue) return true; + if(_nintendoDerivedDiscKey.HasValue) return true; + + if(!scsiReader.OmniDriveNintendoMode) return true; bool sense = scsiReader.ReadBlock(out byte[] raw, 0, out _, out _, out _); @@ -454,63 +464,15 @@ partial class Dump return false; } - ErrorNumber errno = _nintendoSectorDecoder.Scramble(raw, 0, out byte[] descrambled); - - if(errno != ErrorNumber.NoError || descrambled == null) - { - StoppingErrorMessage?.Invoke(Localization.Core.Unable_to_read_medium); - - return false; - } - - byte[] cprMai8 = new byte[8]; - Array.Copy(descrambled, 6, cprMai8, 0, 8); - _nintendoDerivedDiscKey = Sector.DeriveNintendoKey(cprMai8); + byte[] keyMaterial = new byte[8]; + Array.Copy(raw, Sector.NintendoMainDataOffset, keyMaterial, 0, 8); + _nintendoDerivedDiscKey = Sector.DeriveNintendoKey(keyMaterial); + scsiReader.NintendoDerivedDiscKey = _nintendoDerivedDiscKey; UpdateStatus?.Invoke(string.Format(UI.Ngcw_nintendo_derived_key_0, _nintendoDerivedDiscKey.Value)); return true; } - bool DescrambleNintendoLongBuffer(byte[] longBuffer, ulong startSector, uint sectors) - { - if(!_omniDriveNintendoSoftwareDescramble) return true; - - for(uint i = 0; i < sectors; i++) - { - if(!DescrambleNintendo2064At(longBuffer, (int)(i * NGCW_LONG_SECTOR_SIZE), startSector + i)) - return false; - } - - return true; - } - - bool DescrambleNintendo2064At(byte[] buffer, int offset, ulong lba) - { - byte[] one = new byte[NGCW_LONG_SECTOR_SIZE]; - Array.Copy(buffer, offset, one, 0, NGCW_LONG_SECTOR_SIZE); - byte key = lba < NGCW_SECTORS_PER_GROUP ? (byte)0 : (_nintendoDerivedDiscKey ?? (byte)0); - - ErrorNumber error = _nintendoSectorDecoder.Scramble(one, key, out byte[] decoded); - - if(error != ErrorNumber.NoError) - { - Array.Clear(buffer, offset, NGCW_LONG_SECTOR_SIZE); - - return false; - } - - if(decoded != null) Array.Copy(decoded, 0, buffer, offset, NGCW_LONG_SECTOR_SIZE); - - if(lba == 0 && decoded != null) - { - byte[] cprMai8 = new byte[8]; - Array.Copy(decoded, 6, cprMai8, 0, 8); - _nintendoDerivedDiscKey = Sector.DeriveNintendoKey(cprMai8); - } - - return true; - } - List ParseWiiPartitionsFromDevice(Reader scsiReader) { byte[] partitionTable = ReadDiscBytesFromDevice(scsiReader, 0x40000, 32); @@ -585,12 +547,6 @@ partial class Dump if(sense || _dev.Error || rawSector == null || rawSector.Length < NGCW_PAYLOAD_OFFSET + NGCW_SECTOR_SIZE) return null; - if(_omniDriveNintendoSoftwareDescramble && rawSector.Length >= NGCW_LONG_SECTOR_SIZE) - { - if(!DescrambleNintendo2064At(rawSector, 0, sector)) - return null; - } - Array.Copy(rawSector, NGCW_PAYLOAD_OFFSET + sectorOff, result, read, chunk); read += chunk; } @@ -604,15 +560,6 @@ partial class Dump if(sense || _dev.Error || rawBuffer == null) return null; - if(_omniDriveNintendoSoftwareDescramble && rawBuffer.Length >= count * NGCW_LONG_SECTOR_SIZE) - { - for(uint i = 0; i < count; i++) - { - if(!DescrambleNintendo2064At(rawBuffer, (int)(i * NGCW_LONG_SECTOR_SIZE), startSector + i)) - return null; - } - } - byte[] payload = new byte[count * NGCW_SECTOR_SIZE]; for(uint i = 0; i < count; i++) diff --git a/Aaru.Core/Devices/Reader.cs b/Aaru.Core/Devices/Reader.cs index e3389052a..8e6dcb93a 100644 --- a/Aaru.Core/Devices/Reader.cs +++ b/Aaru.Core/Devices/Reader.cs @@ -79,6 +79,12 @@ sealed partial class Reader internal bool CanReadRaw { get; private set; } /// When true with OmniDrive raw reads, use descramble=0 and software Nintendo descrambling (GameCube/Wii). internal bool OmniDriveNintendoMode { get; set; } + + /// + /// Disc-wide Nintendo XOR key (0–15) from LBA 0 CPR_MAI, set after the dump pipeline reads LBA 0. Used for + /// OmniDrive Nintendo reads at LBAs ≥ 16; null until derived. + /// + internal byte? NintendoDerivedDiscKey { get; set; } internal bool CanSeek => _ataSeek || _seek6 || _seek10; internal bool CanSeekLba => _ataSeekLba || _seek6 || _seek10; diff --git a/Aaru.Core/Devices/ReaderSCSI.cs b/Aaru.Core/Devices/ReaderSCSI.cs index 58a72c3ed..667653c61 100644 --- a/Aaru.Core/Devices/ReaderSCSI.cs +++ b/Aaru.Core/Devices/ReaderSCSI.cs @@ -591,8 +591,8 @@ sealed partial class Reader // Try OmniDrive on drives with OmniDrive firmware (standard descramble=1 and Nintendo descramble=0) if(_dev.IsOmniDriveFirmware()) { - bool omniStandardOk = !_dev.OmniDriveReadRawDvd(out _, out senseBuf, 0, 1, _timeout, out _); - bool omniNintendoOk = !_dev.OmniDriveReadNintendoDvd(out _, out senseBuf, 0, 1, _timeout, out _); + bool omniStandardOk = !_dev.OmniDriveReadRawDvd(out _, out senseBuf, 0, 1, _timeout, out _, true, true); + bool omniNintendoOk = !_dev.OmniDriveReadNintendoDvd(out _, out senseBuf, 0, 1, _timeout, out _, true, true); OmniDriveReadRaw = omniStandardOk || omniNintendoOk; } @@ -856,7 +856,10 @@ sealed partial class Reader lba, count, _timeout, - out duration); + out duration, + false, + true, + NintendoDerivedDiscKey); else sense = _dev.OmniDriveReadRawDvd(out buffer, out senseBuf, diff --git a/Aaru.Core/Image/Convert/Ngcw/ConvertNgcw.cs b/Aaru.Core/Image/Convert/Ngcw/ConvertNgcw.cs index 264722eb6..38903edd0 100644 --- a/Aaru.Core/Image/Convert/Ngcw/ConvertNgcw.cs +++ b/Aaru.Core/Image/Convert/Ngcw/ConvertNgcw.cs @@ -271,6 +271,216 @@ public partial class Convert return ErrorNumber.NoError; } + /// Fill a 32 KiB Wii encrypted group from 2048-byte logical ReadSector slices. + void FillWiiEncryptedGroupFromShortSectors(ulong firstLogicalSector, byte[] encGrp) + { + const int sectorSize = Crypto.SECTOR_SIZE; + const int sectorsPerBlock = Crypto.LOGICAL_PER_GROUP; + + for(int s = 0; s < sectorsPerBlock; s++) + { + ulong sec = firstLogicalSector + (ulong)s; + ErrorNumber errno = _inputImage.ReadSector(sec, false, out byte[] sd, out _); + + if(errno != ErrorNumber.NoError || sd == null) + Array.Clear(encGrp, s * sectorSize, sectorSize); + else + Array.Copy(sd, 0, encGrp, s * sectorSize, sectorSize); + } + } + + /// + /// Read one encrypted Wii group from long sectors: copy main_data into and + /// keep full long buffers for output (one ReadSectorLong per logical sector). + /// + void ReadWiiEncryptedGroupFromLongSectors(ulong firstLogicalSector, byte[] encGrp, byte[][] longSectorBuffers, + int longSectorSize) + { + const int sectorSize = Crypto.SECTOR_SIZE; + const int sectorsPerBlock = Crypto.LOGICAL_PER_GROUP; + + for(int s = 0; s < sectorsPerBlock; s++) + { + ulong sec = firstLogicalSector + (ulong)s; + ErrorNumber errno = _inputImage.ReadSectorLong(sec, false, out byte[] sd, out _); + + byte[] stored = longSectorBuffers[s]; + + if(stored == null || stored.Length != longSectorSize) + { + stored = new byte[longSectorSize]; + longSectorBuffers[s] = stored; + } + + if(errno != ErrorNumber.NoError || sd == null) + { + Array.Clear(encGrp, s * sectorSize, sectorSize); + Array.Clear(stored, 0, longSectorSize); + + continue; + } + + int copyLong = sd.Length < longSectorSize ? sd.Length : longSectorSize; + Array.Copy(sd, 0, stored, 0, copyLong); + + if(copyLong < longSectorSize) + Array.Clear(stored, copyLong, longSectorSize - copyLong); + + if(sd.Length >= Sector.NintendoMainDataOffset + sectorSize) + Array.Copy(sd, Sector.NintendoMainDataOffset, encGrp, s * sectorSize, sectorSize); + else + Array.Clear(encGrp, s * sectorSize, sectorSize); + } + } + + /// + /// Classify FST regions, run LFG junk detection on decrypted user data, and build the 32 KiB + /// plaintext group (hash + user, junk zeroed). + /// + byte[] ProcessWiiDecryptedGroup(int inPart, ulong groupDiscOff, byte[] hashBlock, byte[] groupData, + JunkCollector jc, ref ulong dataSectors, ref ulong junkSectors) + { + const int groupSize = Crypto.GROUP_SIZE; + const int hashSize = Crypto.GROUP_HASH_SIZE; + const int groupDataSize = Crypto.GROUP_DATA_SIZE; + const int sectorSize = Crypto.SECTOR_SIZE; + + ulong groupNum = (groupDiscOff - _ngcwPartitions[inPart].DataOffset) / groupSize; + ulong logicalOffset = groupNum * groupDataSize; + + bool[] sectorIsData = new bool[16]; + int udCount = 0; + + for(ulong off = 0; off < groupDataSize; off += sectorSize) + { + ulong chunk = groupDataSize - off; + + if(chunk > sectorSize) chunk = sectorSize; + + if(logicalOffset + off < _ngcwPartSysEnd[inPart]) + sectorIsData[udCount] = true; + else if(_ngcwPartDataMaps[inPart] != null) + { + sectorIsData[udCount] = DataMap.IsDataRegion(_ngcwPartDataMaps[inPart], + logicalOffset + off, + chunk); + } + else + sectorIsData[udCount] = true; + + udCount++; + } + + ulong blockPhase = logicalOffset % groupSize; + ulong block2Start = blockPhase > 0 ? groupSize - blockPhase : groupDataSize; + + if(block2Start > groupDataSize) block2Start = groupDataSize; + + bool haveSeed1 = false; + uint[] seed1 = new uint[Lfg.SEED_SIZE]; + bool haveSeed2 = false; + uint[] seed2 = new uint[Lfg.SEED_SIZE]; + + for(int s = 0; s < udCount; s++) + { + if(sectorIsData[s]) continue; + + ulong soff = (ulong)s * sectorSize; + bool inBlock2 = soff >= block2Start; + + if(inBlock2 && haveSeed2) continue; + if(!inBlock2 && haveSeed1) continue; + + int avail = (int)(groupDataSize - soff); + int doff = (int)((logicalOffset + soff) % groupSize); + + if(avail < Lfg.MIN_SEED_DATA_BYTES) continue; + + uint[] dst = inBlock2 ? seed2 : seed1; + int m = Lfg.GetSeed(groupData.AsSpan((int)soff, avail), doff, dst); + + if(m > 0) + { + if(inBlock2) + haveSeed2 = true; + else + haveSeed1 = true; + } + + if(haveSeed1 && haveSeed2) break; + } + + byte[] decryptedGroup = new byte[groupSize]; + Array.Copy(hashBlock, 0, decryptedGroup, 0, hashSize); + + for(int s = 0; s < udCount; s++) + { + ulong off = (ulong)s * sectorSize; + int chunk = groupDataSize - (int)off; + int outOff = hashSize + (int)off; + + if(chunk > sectorSize) chunk = sectorSize; + + if(sectorIsData[s]) + { + Array.Copy(groupData, (int)off, decryptedGroup, outOff, chunk); + dataSectors++; + + continue; + } + + bool inBlock2 = off >= block2Start; + bool haveSeed = inBlock2 ? haveSeed2 : haveSeed1; + uint[] theSeed = inBlock2 ? seed2 : seed1; + + if(!haveSeed) + { + Array.Copy(groupData, (int)off, decryptedGroup, outOff, chunk); + dataSectors++; + + continue; + } + + uint[] lfgBuffer = new uint[Lfg.MIN_SEED_DATA_BYTES / sizeof(uint)]; + uint[] seedCopy = new uint[Lfg.SEED_SIZE]; + Array.Copy(theSeed, seedCopy, Lfg.SEED_SIZE); + Lfg.SetSeed(lfgBuffer, seedCopy); + int positionBytes = 0; + + int adv = (int)((logicalOffset + off) % groupSize); + + if(adv > 0) + { + byte[] discard = new byte[4096]; + int rem = adv; + + while(rem > 0) + { + int step = rem > discard.Length ? discard.Length : rem; + Lfg.GetBytes(lfgBuffer, ref positionBytes, discard, 0, step); + rem -= step; + } + } + + byte[] expected = new byte[sectorSize]; + Lfg.GetBytes(lfgBuffer, ref positionBytes, expected, 0, chunk); + + if(groupData.AsSpan((int)off, chunk).SequenceEqual(expected.AsSpan(0, chunk))) + { + Array.Clear(decryptedGroup, outOff, chunk); + jc.Add(groupDiscOff + hashSize + off, (ulong)chunk, (ushort)inPart, theSeed); + junkSectors++; + } + else + { + Array.Copy(groupData, (int)off, decryptedGroup, outOff, chunk); + dataSectors++; + } + } + + return decryptedGroup; + } + /// Wii sector conversion pipeline. ErrorNumber ConvertWiiSectors(ulong discSize, ulong totalLogicalSectors, JunkCollector jc, ref ulong dataSectors, ref ulong junkSectors) @@ -307,162 +517,16 @@ public partial class Convert ulong groupDiscOff = _ngcwPartitions[inPart].DataOffset + (offset - _ngcwPartitions[inPart].DataOffset) / groupSize * groupSize; - // Read encrypted group var encGrp = new byte[groupSize]; + FillWiiEncryptedGroupFromShortSectors(groupDiscOff / sectorSize, encGrp); - for(var s = 0; s < sectorsPerBlock; s++) - { - ulong sec = groupDiscOff / sectorSize + (ulong)s; - ErrorNumber errno = _inputImage.ReadSector(sec, false, out byte[] sd, out _); - - if(errno != ErrorNumber.NoError || sd == null) - Array.Clear(encGrp, s * sectorSize, sectorSize); - else - Array.Copy(sd, 0, encGrp, s * sectorSize, sectorSize); - } - - // Decrypt var hashBlock = new byte[hashSize]; var groupData = new byte[groupDataSize]; Crypto.DecryptGroup(_ngcwPartitions[inPart].TitleKey, encGrp, hashBlock, groupData); - // Classify user data sectors - ulong groupNum = (groupDiscOff - _ngcwPartitions[inPart].DataOffset) / groupSize; - ulong logicalOffset = groupNum * groupDataSize; - - var sectorIsData = new bool[16]; - var udCount = 0; - - for(ulong off = 0; off < groupDataSize; off += sectorSize) - { - ulong chunk = groupDataSize - off; - - if(chunk > sectorSize) chunk = sectorSize; - - if(logicalOffset + off < _ngcwPartSysEnd[inPart]) - sectorIsData[udCount] = true; - else if(_ngcwPartDataMaps[inPart] != null) - { - sectorIsData[udCount] = DataMap.IsDataRegion(_ngcwPartDataMaps[inPart], - logicalOffset + off, - chunk); - } - else - sectorIsData[udCount] = true; - - udCount++; - } - - // Extract LFG seeds (up to 2 per group for block boundaries) - ulong blockPhase = logicalOffset % groupSize; - ulong block2Start = blockPhase > 0 ? groupSize - blockPhase : groupDataSize; - - if(block2Start > groupDataSize) block2Start = groupDataSize; - - var haveSeed1 = false; - var seed1 = new uint[Lfg.SEED_SIZE]; - var haveSeed2 = false; - var seed2 = new uint[Lfg.SEED_SIZE]; - - for(var s = 0; s < udCount; s++) - { - if(sectorIsData[s]) continue; - - ulong soff = (ulong)s * sectorSize; - bool inBlock2 = soff >= block2Start; - - if(inBlock2 && haveSeed2) continue; - if(!inBlock2 && haveSeed1) continue; - - var avail = (int)(groupDataSize - soff); - var doff = (int)((logicalOffset + soff) % groupSize); - - if(avail < Lfg.MIN_SEED_DATA_BYTES) continue; - - uint[] dst = inBlock2 ? seed2 : seed1; - int m = Lfg.GetSeed(groupData.AsSpan((int)soff, avail), doff, dst); - - if(m > 0) - { - if(inBlock2) - haveSeed2 = true; - else - haveSeed1 = true; - } - - if(haveSeed1 && haveSeed2) break; - } - - // Build decrypted group: hash_block + processed user_data - var decryptedGroup = new byte[groupSize]; - Array.Copy(hashBlock, 0, decryptedGroup, 0, hashSize); - - for(var s = 0; s < udCount; s++) - { - ulong off = (ulong)s * sectorSize; - int chunk = groupDataSize - (int)off; - int outOff = hashSize + (int)off; - - if(chunk > sectorSize) chunk = sectorSize; - - if(sectorIsData[s]) - { - Array.Copy(groupData, (int)off, decryptedGroup, outOff, chunk); - dataSectors++; - - continue; - } - - bool inBlock2 = off >= block2Start; - bool haveSeed = inBlock2 ? haveSeed2 : haveSeed1; - uint[] theSeed = inBlock2 ? seed2 : seed1; - - if(!haveSeed) - { - Array.Copy(groupData, (int)off, decryptedGroup, outOff, chunk); - dataSectors++; - - continue; - } - - // Verify sector against LFG - var lfgBuffer = new uint[Lfg.MIN_SEED_DATA_BYTES / sizeof(uint)]; - var seedCopy = new uint[Lfg.SEED_SIZE]; - Array.Copy(theSeed, seedCopy, Lfg.SEED_SIZE); - Lfg.SetSeed(lfgBuffer, seedCopy); - var positionBytes = 0; - - var adv = (int)((logicalOffset + off) % groupSize); - - if(adv > 0) - { - var discard = new byte[4096]; - int rem = adv; - - while(rem > 0) - { - int step = rem > discard.Length ? discard.Length : rem; - Lfg.GetBytes(lfgBuffer, ref positionBytes, discard, 0, step); - rem -= step; - } - } - - var expected = new byte[sectorSize]; - Lfg.GetBytes(lfgBuffer, ref positionBytes, expected, 0, chunk); - - if(groupData.AsSpan((int)off, chunk).SequenceEqual(expected.AsSpan(0, chunk))) - { - // Junk — zero it out, record in junk map - Array.Clear(decryptedGroup, outOff, chunk); - jc.Add(groupDiscOff + hashSize + off, (ulong)chunk, (ushort)inPart, theSeed); - junkSectors++; - } - else - { - Array.Copy(groupData, (int)off, decryptedGroup, outOff, chunk); - dataSectors++; - } - } + byte[] decryptedGroup = + ProcessWiiDecryptedGroup(inPart, groupDiscOff, hashBlock, groupData, jc, ref dataSectors, + ref junkSectors); // Write all 16 sectors as SectorStatusUnencrypted for(var s = 0; s < sectorsPerBlock; s++) @@ -879,9 +943,194 @@ public partial class Convert 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 + /// Wii sector conversion pipeline for long sectors (main_data + framing). + ErrorNumber ConvertWiiSectorsLong(ulong discSize, ulong totalLogicalSectors, JunkCollector jc, + ref ulong dataSectors, ref ulong junkSectors) + { + const int groupSize = Crypto.GROUP_SIZE; + const int hashSize = Crypto.GROUP_HASH_SIZE; + const int groupDataSize = Crypto.GROUP_DATA_SIZE; + const int sectorSize = Crypto.SECTOR_SIZE; + const int sectorsPerBlock = Crypto.LOGICAL_PER_GROUP; + + ErrorNumber probeErr = _inputImage.ReadSectorLong(0, false, out byte[] longProbe, out _); + + if(probeErr != ErrorNumber.NoError || longProbe == null || + longProbe.Length < Sector.NintendoMainDataOffset + sectorSize) + return ErrorNumber.InOutError; + + int longSectorSize = longProbe.Length; + + BuildWiiPartitionFstMaps(); + + var longSectorBufs = new byte[sectorsPerBlock][]; + + ulong offset = 0; + + while(offset < discSize) + { + if(_aborted) break; + + ulong baseSector = offset / sectorSize; + + UpdateProgress?.Invoke(string.Format(UI.Converting_sectors_0_to_1, + baseSector, + baseSector + sectorsPerBlock), + (long)baseSector, + (long)totalLogicalSectors); + + int inPart = NgcwPartitions.FindPartitionAtOffset(_ngcwPartitions, offset); + + if(inPart >= 0) + { + ulong groupDiscOff = _ngcwPartitions[inPart].DataOffset + + (offset - _ngcwPartitions[inPart].DataOffset) / groupSize * groupSize; + + var encGrp = new byte[groupSize]; + ReadWiiEncryptedGroupFromLongSectors(groupDiscOff / sectorSize, encGrp, longSectorBufs, longSectorSize); + + var hashBlock = new byte[hashSize]; + var groupData = new byte[groupDataSize]; + Crypto.DecryptGroup(_ngcwPartitions[inPart].TitleKey, encGrp, hashBlock, groupData); + + byte[] decryptedGroup = + ProcessWiiDecryptedGroup(inPart, groupDiscOff, hashBlock, groupData, jc, ref dataSectors, + ref junkSectors); + + for(int s = 0; s < sectorsPerBlock; s++) + { + int dataOff = s * sectorSize; + int outOff = hashSize + dataOff; + + if(dataOff >= groupDataSize) + { + Array.Clear(longSectorBufs[s], Sector.NintendoMainDataOffset, sectorSize); + + continue; + } + + int copyLen = groupDataSize - dataOff; + + if(copyLen > sectorSize) + copyLen = sectorSize; + + Array.Copy(decryptedGroup, outOff, longSectorBufs[s], Sector.NintendoMainDataOffset, copyLen); + + if(copyLen < sectorSize) + Array.Clear(longSectorBufs[s], Sector.NintendoMainDataOffset + copyLen, sectorSize - copyLen); + } + + for(int s = 0; s < sectorsPerBlock; s++) + { + ulong sector = groupDiscOff / sectorSize + (ulong)s; + bool ok = _outputImage.WriteSectorLong(longSectorBufs[s], + sector, + false, + SectorStatus.Unencrypted); + + 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; + } + } + } + + offset = groupDiscOff + groupSize; + } + else + { + const int blockSize = groupSize; + ulong alignedOff = offset & ~(ulong)(blockSize - 1); + + int blockBytes = blockSize; + + if(alignedOff + (ulong)blockBytes > discSize) blockBytes = (int)(discSize - alignedOff); + + var blockBuf = new byte[blockSize]; + + for(int s = 0; s < sectorsPerBlock && s * sectorSize < blockBytes; s++) + { + ulong sec = alignedOff / sectorSize + (ulong)s; + ErrorNumber errno = _inputImage.ReadSectorLong(sec, false, out byte[] sd, out _); + + byte[] stored = new byte[longSectorSize]; + longSectorBufs[s] = stored; + + if(errno != ErrorNumber.NoError || sd == null) + { + Array.Clear(blockBuf, s * sectorSize, sectorSize); + Array.Clear(stored, 0, longSectorSize); + + continue; + } + + int copyLong = sd.Length < longSectorSize ? sd.Length : longSectorSize; + Array.Copy(sd, 0, stored, 0, copyLong); + + if(copyLong < longSectorSize) + Array.Clear(stored, copyLong, longSectorSize - copyLong); + + if(sd.Length >= Sector.NintendoMainDataOffset + sectorSize) + Array.Copy(sd, Sector.NintendoMainDataOffset, blockBuf, s * sectorSize, sectorSize); + else + Array.Clear(blockBuf, s * sectorSize, sectorSize); + } + + var sectorStatuses = new SectorStatus[sectorsPerBlock]; + + Junk.DetectJunkInBlock(blockBuf, + blockBytes, + alignedOff, + null, + 0x50000, + 0xFFFF, + jc, + ref dataSectors, + ref junkSectors, + sectorStatuses); + + int numSectors = blockBytes / sectorSize; + + for(int si = 0; si < numSectors; si++) + { + ulong sector = alignedOff / sectorSize + (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; + } + } + } + + offset = alignedOff + (ulong)blockBytes; + } + } + return ErrorNumber.NoError; } diff --git a/Aaru.Decoders/DVD/Sector.cs b/Aaru.Decoders/DVD/Sector.cs index 56d963e29..63033a30f 100644 --- a/Aaru.Decoders/DVD/Sector.cs +++ b/Aaru.Decoders/DVD/Sector.cs @@ -301,6 +301,21 @@ public sealed class Sector return computed == stored; } + /// + /// Check if the IED of sectors is correct + /// + /// Buffer of the sector + /// The number of sectors to check + /// True if IED is correct, False if not + public static bool CheckIed(byte[] sectorLong, uint transferLength) + { + for(uint i = 0; i < transferLength; i++) + { + if(!CheckIed(sectorLong.Skip((int)(i * 2064)).Take(2064).ToArray())) return false; + } + return true; + } + /// /// Tests if a seed unscrambles a sector correctly /// diff --git a/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs b/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs index dec0d670c..b28f984b1 100644 --- a/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs +++ b/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs @@ -33,14 +33,17 @@ // ****************************************************************************/ using System; +using Aaru.CommonTypes.Enums; using Aaru.CommonTypes.Structs.Devices.SCSI; using Aaru.Logging; using Aaru.Decoders.DVD; +using NintendoSector = Aaru.Decoders.Nintendo.Sector; namespace Aaru.Devices; public partial class Device { + readonly NintendoSector _nintendoSectorDecoder = new NintendoSector(); enum OmniDriveDiscType { CD = 0, @@ -66,16 +69,16 @@ public partial class Device static void FillOmniDriveReadDvdCdb(Span cdb, uint lba, uint transferLength, byte cdbByte1) { cdb.Clear(); - cdb[0] = (byte)ScsiCommands.ReadOmniDrive; - cdb[1] = cdbByte1; - cdb[2] = (byte)((lba >> 24) & 0xFF); - cdb[3] = (byte)((lba >> 16) & 0xFF); - cdb[4] = (byte)((lba >> 8) & 0xFF); - cdb[5] = (byte)(lba & 0xFF); - cdb[6] = (byte)((transferLength >> 24) & 0xFF); - cdb[7] = (byte)((transferLength >> 16) & 0xFF); - cdb[8] = (byte)((transferLength >> 8) & 0xFF); - cdb[9] = (byte)(transferLength & 0xFF); + cdb[0] = (byte)ScsiCommands.ReadOmniDrive; + cdb[1] = cdbByte1; + cdb[2] = (byte)((lba >> 24) & 0xFF); + cdb[3] = (byte)((lba >> 16) & 0xFF); + cdb[4] = (byte)((lba >> 8) & 0xFF); + cdb[5] = (byte)(lba & 0xFF); + cdb[6] = (byte)((transferLength >> 24) & 0xFF); + cdb[7] = (byte)((transferLength >> 16) & 0xFF); + cdb[8] = (byte)((transferLength >> 8) & 0xFF); + cdb[9] = (byte)(transferLength & 0xFF); cdb[10] = 0; // subchannels=NONE, c2=0 cdb[11] = 0; // control } @@ -89,20 +92,20 @@ public partial class Device { bool sense = ScsiInquiry(out byte[] buffer, out _, Timeout, out _); - if(sense || buffer == null) return false; + if (sense || buffer == null) return false; Inquiry? inquiry = Inquiry.Decode(buffer); - if(!inquiry.HasValue || inquiry.Value.Reserved5 == null || inquiry.Value.Reserved5.Length < 11) + if (!inquiry.HasValue || inquiry.Value.Reserved5 == null || inquiry.Value.Reserved5.Length < 11) return false; byte[] reserved5 = inquiry.Value.Reserved5; byte[] omnidrive = [0x4F, 0x6D, 0x6E, 0x69, 0x44, 0x72, 0x69, 0x76, 0x65]; // "OmniDrive" - if(reserved5.Length < omnidrive.Length) return false; + if (reserved5.Length < omnidrive.Length) return false; - for(int i = 0; i < omnidrive.Length; i++) - if(reserved5[i] != omnidrive[i]) + for (int i = 0; i < omnidrive.Length; i++) + if (reserved5[i] != omnidrive[i]) return false; return true; @@ -131,8 +134,11 @@ public partial class Device EncodeOmniDriveReadCdb1(OmniDriveDiscType.DVD, false, fua, descramble)); LastError = SendScsiCommand(cdb, ref buffer, timeout, ScsiDirection.In, out duration, out bool sense); - - if(!Sector.CheckEdc(buffer, transferLength)) return true; + + if (!Sector.CheckIed(buffer, transferLength)) return true; + + if(descramble) + if (!Sector.CheckEdc(buffer, transferLength)) return true; Error = LastError != 0; @@ -142,13 +148,16 @@ public partial class Device } /// - /// Reads raw Nintendo GameCube/Wii DVD sectors (2064 bytes) on OmniDrive. Default matches redumper raw DVD - /// (descramble off); use software descramble via Aaru.Decoders.Nintendo.Sector when needed. + /// Reads Nintendo GameCube/Wii DVD sectors (2064 bytes) on OmniDrive. The drive returns DVD-layer data with + /// drive-side DVD descramble off; this method applies per-sector Nintendo XOR descramble and returns the result. + /// LBAs 0–15 use key 0; LBAs ≥ 16 use (from LBA 0 CPR_MAI), or 0 if not yet + /// derived. /// - /// Drive-side DVD descramble (redumper raw DVD uses false). + /// Disc key from LBA 0 (0–15); null until the host has read and derived it. /// true if the command failed and contains the sense buffer. public bool OmniDriveReadNintendoDvd(out byte[] buffer, out ReadOnlySpan senseBuffer, uint lba, uint transferLength, - uint timeout, out double duration, bool descramble = false) + uint timeout, out double duration, bool fua = false, bool descramble = true, + byte? derivedDiscKey = null) { senseBuffer = SenseBuffer; Span cdb = CdbBuffer[..12]; @@ -157,11 +166,55 @@ public partial class Device FillOmniDriveReadDvdCdb(cdb, lba, transferLength, - EncodeOmniDriveReadCdb1(OmniDriveDiscType.DVD, false, false, descramble)); + EncodeOmniDriveReadCdb1(OmniDriveDiscType.DVD, false, fua, false)); LastError = SendScsiCommand(cdb, ref buffer, timeout, ScsiDirection.In, out duration, out bool sense); - // Scrambled Nintendo sectors do not pass standard DVD EDC until software-descrambled. + if(!Sector.CheckIed(buffer, transferLength)) return true; + + if(descramble) { + const int sectorBytes = 2064; + byte[] outBuf = new byte[sectorBytes * transferLength]; + + if(lba < 16 && lba + transferLength > 16) { + for(uint i = 0; i < transferLength; i++) + { + var slice = new byte[sectorBytes]; + Array.Copy(buffer, i * sectorBytes, slice, 0, sectorBytes); + uint absLba = lba + i; + byte key = absLba < 16 ? (byte)0 : (derivedDiscKey ?? (byte)0); + + ErrorNumber errno = _nintendoSectorDecoder.Scramble(slice, key, out byte[] descrambled); + + if(errno != ErrorNumber.NoError || descrambled == null) + { + LastError = (int)errno; + Error = true; + + return true; + } + + Array.Copy(descrambled, 0, outBuf, i * sectorBytes, sectorBytes); + } + } else { + ErrorNumber errno = _nintendoSectorDecoder.Scramble( + buffer, + transferLength, + lba < 16 || lba > 0xffffff ? (byte)0 : (derivedDiscKey ?? (byte)0), + out outBuf); + + if(errno != ErrorNumber.NoError || outBuf == null) + { + LastError = (int)errno; + Error = true; + + return true; + } + } + + buffer = outBuf; + } + Error = LastError != 0; AaruLogging.Debug(SCSI_MODULE_NAME, "OmniDrive READ NINTENDO DVD took {0} ms", duration); diff --git a/Aaru.Gui/ViewModels/Windows/MediaDumpViewModel.cs b/Aaru.Gui/ViewModels/Windows/MediaDumpViewModel.cs index efde3ef7f..4227b60cb 100644 --- a/Aaru.Gui/ViewModels/Windows/MediaDumpViewModel.cs +++ b/Aaru.Gui/ViewModels/Windows/MediaDumpViewModel.cs @@ -753,7 +753,8 @@ public sealed partial class MediaDumpViewModel : ViewModelBase true, 1080, Paranoia, - CureParanoia); + CureParanoia, + false); new Thread(DoWork).Start(); } diff --git a/Aaru.Localization/UI.Designer.cs b/Aaru.Localization/UI.Designer.cs index 15672f15f..d8d2164c6 100644 --- a/Aaru.Localization/UI.Designer.cs +++ b/Aaru.Localization/UI.Designer.cs @@ -1995,6 +1995,12 @@ namespace Aaru.Localization { } } + public static string Ngcw_wii_dump_bypass_decryption { + get { + return ResourceManager.GetString("Ngcw_wii_dump_bypass_decryption", resourceCulture); + } + } + public static string PS3_disc_key_resolved_from_0 { get { return ResourceManager.GetString("PS3_disc_key_resolved_from_0", resourceCulture); diff --git a/Aaru.Localization/UI.es.resx b/Aaru.Localization/UI.es.resx index 211104bac..47fc9cd65 100644 --- a/Aaru.Localization/UI.es.resx +++ b/Aaru.Localization/UI.es.resx @@ -669,6 +669,9 @@ [slateblue1]Clave de disco Nintendo derivada: [green]{0}[/].[/] + + + [slateblue1]Omisión del descifrado de particiones Wii: guardando datos de partición cifrados tal como se leen.[/] [slateblue1]Clave de disco PS3 resuelta desde [aqua]{0}[/].[/] diff --git a/Aaru.Localization/UI.resx b/Aaru.Localization/UI.resx index f39e0c4a6..3311592bb 100644 --- a/Aaru.Localization/UI.resx +++ b/Aaru.Localization/UI.resx @@ -971,7 +971,7 @@ In you are unsure, please press N to not continue. Skip Wii U disc encryption processing during conversion. - Skip Wii disc encryption processing during conversion. + Skip Wii disc encryption processing during conversion or dump. [slateblue1]Parsing Wii partition table...[/] @@ -1008,6 +1008,9 @@ In you are unsure, please press N to not continue. [slateblue1]Derived Nintendo disc key: [green]{0}[/].[/] + + + [slateblue1]Wii partition decryption bypass: storing encrypted partition data as read.[/] [slateblue1]PS3 disc key resolved from [aqua]{0}[/].[/] diff --git a/Aaru/Commands/Media/Dump.cs b/Aaru/Commands/Media/Dump.cs index 374aa505c..151d09372 100644 --- a/Aaru/Commands/Media/Dump.cs +++ b/Aaru/Commands/Media/Dump.cs @@ -116,6 +116,7 @@ sealed class DumpMediaCommand : Command AaruLogging.Debug(MODULE_NAME, "--max-blocks={0}", maxBlocks); AaruLogging.Debug(MODULE_NAME, "--use-buffered-reads={0}", settings.UseBufferedReads); AaruLogging.Debug(MODULE_NAME, "--store-encrypted={0}", settings.StoreEncrypted); + AaruLogging.Debug(MODULE_NAME, "--bypass-wii-decryption={0}", settings.BypassWiiDecryption); AaruLogging.Debug(MODULE_NAME, "--title-keys={0}", settings.TitleKeys); AaruLogging.Debug(MODULE_NAME, "--ignore-cdr-runouts={0}", settings.IgnoreCdrRunOuts); AaruLogging.Debug(MODULE_NAME, "--create-graph={0}", settings.CreateGraph); @@ -526,7 +527,8 @@ sealed class DumpMediaCommand : Command settings.CreateGraph, (uint)settings.Dimensions, settings.Paranoia, - settings.CureParanoia); + settings.CureParanoia, + settings.BypassWiiDecryption); AnsiConsole.Progress() .AutoClear(true) @@ -757,6 +759,10 @@ sealed class DumpMediaCommand : Command [CommandOption("--store-encrypted")] [DefaultValue(true)] public bool StoreEncrypted { get; init; } + [LocalizedDescription(nameof(UI.Bypass_Wii_decryption_help))] + [CommandOption("--bypass-wii-decryption")] + [DefaultValue(false)] + public bool BypassWiiDecryption { get; init; } [LocalizedDescription(nameof(UI.Try_to_read_the_title_keys_from_CSS_DVDs))] [CommandOption("--title-keys")] [DefaultValue(true)] From c4be5ca3783fd56068b9ffb6a9fb6d2a9c8f5440 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Fri, 3 Apr 2026 09:46:13 +0200 Subject: [PATCH 11/16] Descramble Leadin/Leadout --- Aaru.Core/Devices/ReaderSCSI.cs | 20 ++++++- Aaru.Devices/Device/ScsiCommands/OmniDrive.cs | 60 ++++++++++--------- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/Aaru.Core/Devices/ReaderSCSI.cs b/Aaru.Core/Devices/ReaderSCSI.cs index 667653c61..c65c1e093 100644 --- a/Aaru.Core/Devices/ReaderSCSI.cs +++ b/Aaru.Core/Devices/ReaderSCSI.cs @@ -592,7 +592,17 @@ sealed partial class Reader if(_dev.IsOmniDriveFirmware()) { bool omniStandardOk = !_dev.OmniDriveReadRawDvd(out _, out senseBuf, 0, 1, _timeout, out _, true, true); - bool omniNintendoOk = !_dev.OmniDriveReadNintendoDvd(out _, out senseBuf, 0, 1, _timeout, out _, true, true); + bool omniNintendoOk = !_dev.OmniDriveReadNintendoDvd(out _, + out senseBuf, + 0, + 1, + _timeout, + out _, + true, + true, + null, + false, + 0); OmniDriveReadRaw = omniStandardOk || omniNintendoOk; } @@ -851,6 +861,9 @@ sealed partial class Reader uint lba = negative ? (uint)(-(long)block) : (uint)block; if(OmniDriveNintendoMode) + { + ulong regularDataEndExclusive = Blocks + 1; + sense = _dev.OmniDriveReadNintendoDvd(out buffer, out senseBuf, lba, @@ -859,7 +872,10 @@ sealed partial class Reader out duration, false, true, - NintendoDerivedDiscKey); + NintendoDerivedDiscKey, + negative, + regularDataEndExclusive); + } else sense = _dev.OmniDriveReadRawDvd(out buffer, out senseBuf, diff --git a/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs b/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs index b28f984b1..8acf92ca1 100644 --- a/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs +++ b/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs @@ -44,6 +44,7 @@ namespace Aaru.Devices; public partial class Device { readonly NintendoSector _nintendoSectorDecoder = new NintendoSector(); + readonly Sector _dvdSectorDecoder = new Sector(); enum OmniDriveDiscType { CD = 0, @@ -154,10 +155,18 @@ public partial class Device /// derived. /// /// Disc key from LBA 0 (0–15); null until the host has read and derived it. + /// + /// True when caller is reading wrapped negative LBAs (lead-in); these sectors use standard DVD descramble. + /// + /// + /// Exclusive upper bound of regular user-data LBAs. Sectors at or above this are leadout and use standard DVD + /// descramble. + /// /// true if the command failed and contains the sense buffer. public bool OmniDriveReadNintendoDvd(out byte[] buffer, out ReadOnlySpan senseBuffer, uint lba, uint transferLength, uint timeout, out double duration, bool fua = false, bool descramble = true, - byte? derivedDiscKey = null) + byte? derivedDiscKey = null, bool negativeAddressing = false, + ulong regularDataEndExclusive = 0) { senseBuffer = SenseBuffer; Span cdb = CdbBuffer[..12]; @@ -172,44 +181,41 @@ public partial class Device if(!Sector.CheckIed(buffer, transferLength)) return true; - if(descramble) { + if(descramble) + { const int sectorBytes = 2064; byte[] outBuf = new byte[sectorBytes * transferLength]; + const uint maxRegularLba = 0x00FFFFFF; - if(lba < 16 && lba + transferLength > 16) { - for(uint i = 0; i < transferLength; i++) + for(uint i = 0; i < transferLength; i++) + { + byte[] slice = new byte[sectorBytes]; + Array.Copy(buffer, i * sectorBytes, slice, 0, sectorBytes); + + uint absLba = lba + i; + bool wrappedNegative = negativeAddressing || absLba > maxRegularLba; + bool leadout = !wrappedNegative && regularDataEndExclusive > 0 && absLba >= regularDataEndExclusive; + + ErrorNumber errno; + byte[] descrambled; + + if(wrappedNegative || leadout) + errno = _dvdSectorDecoder.Scramble(slice, out descrambled); + else { - var slice = new byte[sectorBytes]; - Array.Copy(buffer, i * sectorBytes, slice, 0, sectorBytes); - uint absLba = lba + i; - byte key = absLba < 16 ? (byte)0 : (derivedDiscKey ?? (byte)0); - - ErrorNumber errno = _nintendoSectorDecoder.Scramble(slice, key, out byte[] descrambled); - - if(errno != ErrorNumber.NoError || descrambled == null) - { - LastError = (int)errno; - Error = true; - - return true; - } - - Array.Copy(descrambled, 0, outBuf, i * sectorBytes, sectorBytes); + byte key = absLba < 16 ? (byte)0 : (derivedDiscKey ?? (byte)0); + errno = _nintendoSectorDecoder.Scramble(slice, key, out descrambled); } - } else { - ErrorNumber errno = _nintendoSectorDecoder.Scramble( - buffer, - transferLength, - lba < 16 || lba > 0xffffff ? (byte)0 : (derivedDiscKey ?? (byte)0), - out outBuf); - if(errno != ErrorNumber.NoError || outBuf == null) + if(errno != ErrorNumber.NoError || descrambled == null) { LastError = (int)errno; Error = true; return true; } + + Array.Copy(descrambled, 0, outBuf, i * sectorBytes, sectorBytes); } buffer = outBuf; From 47eee4da80c5aefb2979ab4a7b1b01416f980f4d Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Fri, 3 Apr 2026 11:07:47 +0200 Subject: [PATCH 12/16] Dump BCA from GOD/WOD --- Aaru.Core/Devices/Dumping/MMC.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Aaru.Core/Devices/Dumping/MMC.cs b/Aaru.Core/Devices/Dumping/MMC.cs index 5770333f5..2d7a06c42 100644 --- a/Aaru.Core/Devices/Dumping/MMC.cs +++ b/Aaru.Core/Devices/Dumping/MMC.cs @@ -653,11 +653,13 @@ partial class Dump switch(dskType) { -#region DVD-ROM and HD DVD-ROM +#region DVD-ROM, HD DVD-ROM, GameCube Game Disc and Wii Optical Disc case MediaType.DVDDownload: case MediaType.DVDROM: case MediaType.HDDVDROM: + case MediaType.GOD: + case MediaType.WOD: AaruLogging.WriteLine(Localization.Core.Reading_Burst_Cutting_Area); sense = _dev.ReadDiscStructure(out cmdBuf, @@ -679,7 +681,7 @@ partial class Dump break; -#endregion DVD-ROM and HD DVD-ROM +#endregion DVD-ROM, HD DVD-ROM, GameCube Game Disc and Wii Optical Disc #region DVD-RAM and HD DVD-RAM From cb16dc06429b427fb99c6dd84c85311e621fe994 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Fri, 3 Apr 2026 11:43:18 +0200 Subject: [PATCH 13/16] Use ECMA scrambling for Nintendo lead-in/out on Redumper image --- Aaru.Images/Redumper/Ngcw.cs | 14 ++++++++++++-- Aaru.Images/Redumper/Read.cs | 5 +++++ Aaru.Images/Redumper/Redumper.cs | 1 + 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Aaru.Images/Redumper/Ngcw.cs b/Aaru.Images/Redumper/Ngcw.cs index 9def68195..b398aef1e 100644 --- a/Aaru.Images/Redumper/Ngcw.cs +++ b/Aaru.Images/Redumper/Ngcw.cs @@ -126,9 +126,19 @@ public sealed partial class Redumper { 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); + bool leadIn = lba < 0; + bool leadOut = _ngcwRegularDataSectors > 0 && lba >= (long)_ngcwRegularDataSectors; - ErrorNumber error = _nintendoDecoder.Scramble(one, key, out byte[] decoded); + ErrorNumber error; + byte[] decoded; + + if(leadIn || leadOut) + error = _decoding.Scramble(one, out decoded); + else + { + byte key = lba < NGCW_SECTORS_PER_GROUP ? (byte)0 : (_nintendoDerivedKey ?? (byte)0); + error = _nintendoDecoder.Scramble(one, key, out decoded); + } if(error != ErrorNumber.NoError) { diff --git a/Aaru.Images/Redumper/Read.cs b/Aaru.Images/Redumper/Read.cs index fa45c764c..d44736e5f 100644 --- a/Aaru.Images/Redumper/Read.cs +++ b/Aaru.Images/Redumper/Read.cs @@ -56,6 +56,7 @@ public sealed partial class Redumper public ErrorNumber Open(IFilter imageFilter) { string filename = imageFilter.Filename; + _ngcwRegularDataSectors = 0; if(string.IsNullOrEmpty(filename)) return ErrorNumber.InvalidArgument; @@ -132,6 +133,10 @@ public sealed partial class Redumper : MediaType.WOD, _ => MediaType.DVDROM }; + + if(decodedPfi.Value.DataAreaEndPSN >= decodedPfi.Value.DataAreaStartPSN) + _ngcwRegularDataSectors = + (ulong)(decodedPfi.Value.DataAreaEndPSN - decodedPfi.Value.DataAreaStartPSN) + 1; } } diff --git a/Aaru.Images/Redumper/Redumper.cs b/Aaru.Images/Redumper/Redumper.cs index 4347274e5..a03d76511 100644 --- a/Aaru.Images/Redumper/Redumper.cs +++ b/Aaru.Images/Redumper/Redumper.cs @@ -84,6 +84,7 @@ public sealed partial class Redumper : IOpticalMediaImage /// Derived Nintendo key from LBA 0 so sectors 16+ can be descrambled. byte? _nintendoDerivedKey; + ulong _ngcwRegularDataSectors; IFilter _imageFilter; ImageInfo _imageInfo; From 7166ade598599ca50f141b66836c8a5b8a5675bd Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Fri, 3 Apr 2026 16:33:11 +0200 Subject: [PATCH 14/16] outputFormat null-check --- Aaru.Core/Devices/Dumping/Sbc/Error.cs | 21 ++++++++++++++++++++- Aaru.Localization/Core.Designer.cs | 6 ++++++ Aaru.Localization/Core.resx | 3 +++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Aaru.Core/Devices/Dumping/Sbc/Error.cs b/Aaru.Core/Devices/Dumping/Sbc/Error.cs index 56018b509..e3126e549 100644 --- a/Aaru.Core/Devices/Dumping/Sbc/Error.cs +++ b/Aaru.Core/Devices/Dumping/Sbc/Error.cs @@ -306,6 +306,14 @@ partial class Dump _resume.BadBlocks.Remove(badSector); extents.Add(badSector); + if(outputFormat is null) + { + ErrorMessage?.Invoke(string.Format(Localization.Core.Cannot_write_retried_sector_0_no_writable_output_image, + badSector)); + + continue; + } + if(scsiReader.ReadBuffer3CReadRaw || scsiReader.OmniDriveReadRaw || scsiReader.HldtstReadRaw) { if(_ngcwEnabled) @@ -368,7 +376,18 @@ partial class Dump badSector, pass)); } - else if(runningPersistent) outputFormat.WriteSector(buffer, badSector, false, SectorStatus.Errored); + else if(runningPersistent) + { + if(outputFormat is null) + { + ErrorMessage?.Invoke(string.Format(Localization.Core.Cannot_write_retried_sector_0_no_writable_output_image, + badSector)); + + continue; + } + + outputFormat.WriteSector(buffer, badSector, false, SectorStatus.Errored); + } } if(pass < _retryPasses && !_aborted && _resume.BadBlocks.Count > 0) diff --git a/Aaru.Localization/Core.Designer.cs b/Aaru.Localization/Core.Designer.cs index 0d42d4f22..043a80aee 100644 --- a/Aaru.Localization/Core.Designer.cs +++ b/Aaru.Localization/Core.Designer.cs @@ -3113,6 +3113,12 @@ namespace Aaru.Localization { } } + public static string Cannot_write_retried_sector_0_no_writable_output_image { + get { + return ResourceManager.GetString("Cannot_write_retried_sector_0_no_writable_output_image", resourceCulture); + } + } + public static string Cannot_write_SCR_to_output_image { get { return ResourceManager.GetString("Cannot_write_SCR_to_output_image", resourceCulture); diff --git a/Aaru.Localization/Core.resx b/Aaru.Localization/Core.resx index 7500469b5..0061f6e68 100644 --- a/Aaru.Localization/Core.resx +++ b/Aaru.Localization/Core.resx @@ -1126,6 +1126,9 @@ [red]Image is not writable, aborting...[/] + + [red]Cannot write retried sector [lime]{0}[/]: output is not a writable image.[/] + [red]Could not detect capacity...[/] From 77ab25def42b0885e2e4905a2302151bce883859 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Fri, 3 Apr 2026 16:33:56 +0200 Subject: [PATCH 15/16] outputFormat is AaruFormat check --- Aaru.Core/Devices/Dumping/Sbc/Dump.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Aaru.Core/Devices/Dumping/Sbc/Dump.cs b/Aaru.Core/Devices/Dumping/Sbc/Dump.cs index 8aed913ed..9bc81a079 100644 --- a/Aaru.Core/Devices/Dumping/Sbc/Dump.cs +++ b/Aaru.Core/Devices/Dumping/Sbc/Dump.cs @@ -58,6 +58,7 @@ using DVDDecryption = Aaru.Decryption.DVD.Dump; using Track = Aaru.CommonTypes.Structs.Track; using TrackType = Aaru.CommonTypes.Enums.TrackType; using Version = Aaru.CommonTypes.Interop.Version; +using AaruFormat = Aaru.Images.AaruFormat; // ReSharper disable JoinDeclarationAndInitializer @@ -349,7 +350,7 @@ partial class Dump return; } - if(outputFormat.Format != "Aaru") + if(outputFormat is not AaruFormat) { StoppingErrorMessage?.Invoke(string.Format(Localization.Core.Output_format_does_not_support_0, MediaTagType.NgcwJunkMap)); From 7b7d3b1151563e25f8e6bd1423fc653e6de0a173 Mon Sep 17 00:00:00 2001 From: Rebecca Wallander Date: Fri, 3 Apr 2026 16:34:53 +0200 Subject: [PATCH 16/16] untangle if-statement --- Aaru.Devices/Device/ScsiCommands/OmniDrive.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs b/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs index 8acf92ca1..30d961709 100644 --- a/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs +++ b/Aaru.Devices/Device/ScsiCommands/OmniDrive.cs @@ -138,8 +138,7 @@ public partial class Device if (!Sector.CheckIed(buffer, transferLength)) return true; - if(descramble) - if (!Sector.CheckEdc(buffer, transferLength)) return true; + if(descramble && !Sector.CheckEdc(buffer, transferLength)) return true; Error = LastError != 0;