Merge pull request #920 from aaru-dps/fakeshemp/edc

Dump GameCube and Wii with OmniDrive
This commit is contained in:
2026-04-03 18:06:28 +01:00
committed by GitHub
42 changed files with 3134 additions and 343 deletions

View File

@@ -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;
@@ -114,6 +115,8 @@ public partial class Dump
Database.Models.Device _dbDev; // Device database entry
bool _dumpFirstTrackPregap;
bool _fixOffset;
HashSet<ulong> _missingTitleKeysLookup;
bool _missingTitleKeysDirty;
uint _maximumReadable; // Maximum number of sectors drive can read at once
IMediaGraph _mediaGraph;
Resume _resume;
@@ -169,6 +172,7 @@ public partial class Dump
/// <param name="dimensions">Dimensions of graph in pixels for a square</param>
/// <param name="paranoia">Check sectors integrity before writing to image</param>
/// <param name="cureParanoia">Try to fix sectors integrity</param>
/// <param name="bypassWiiDecryption">When dumping Wii (WOD), skip partition AES decryption and store encrypted data</param>
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<string, string> formatOptions, Metadata preSidecar,
@@ -177,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;
@@ -221,6 +225,7 @@ public partial class Dump
_dimensions = dimensions;
_paranoia = paranoia;
_cureParanoia = cureParanoia;
_bypassWiiDecryption = bypassWiiDecryption;
_dumpStopwatch = new Stopwatch();
_sidecarStopwatch = new Stopwatch();
_speedStopwatch = new Stopwatch();
@@ -294,6 +299,7 @@ public partial class Dump
if(_resume == null || !_doResume) return;
_resume.LastWriteDate = DateTime.UtcNow;
SyncMissingTitleKeysToResume();
_resume.BadBlocks.Sort();
if(_createGraph && _mediaGraph is not null)

View File

@@ -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;
}
}
@@ -656,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,
@@ -682,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

View File

@@ -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);

View File

@@ -1,4 +1,4 @@
// /***************************************************************************
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
@@ -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;
@@ -57,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
@@ -337,6 +339,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 is not AaruFormat)
{
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 +432,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 +811,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 &&
@@ -791,6 +822,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 +952,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 &&
@@ -935,6 +969,8 @@ partial class Dump
#endregion Error handling
if(ngcwMode) FinalizeNgcwContext(outputFormat);
if(opticalDisc)
{
foreach(KeyValuePair<MediaTagType, byte[]> tag in mediaTags)

View File

@@ -306,7 +306,24 @@ 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)
{
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
{
var cmi = new byte[1];
@@ -316,13 +333,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];
}
@@ -349,6 +366,7 @@ partial class Dump
_resume.BadBlocks.Remove(badSector);
outputFormat.WriteSectorLong(buffer, badSector, false, SectorStatus.Dumped);
}
}
else
outputFormat.WriteSector(buffer, badSector, false, SectorStatus.Dumped);
@@ -358,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)
@@ -406,7 +435,7 @@ partial class Dump
InitProgress?.Invoke();
repeatRetry:
ulong[] tmpArray = _resume.MissingTitleKeys.ToArray();
ulong[] tmpArray = MissingTitleKeysSnapshot(forward);
foreach(ulong missingKey in tmpArray)
{
@@ -450,7 +479,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 +488,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 +502,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;
}

View File

@@ -0,0 +1,571 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Ngcw.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// 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.Decryption.Ngcw;
using Aaru.Decoders.Nintendo;
using Aaru.Helpers;
using Aaru.Localization;
using NgcwPartitions = Aaru.Decryption.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<WiiPartition> _ngcwPartitions;
DataRegion[][] _ngcwPartDataMaps;
ulong[] _ngcwPartSysEnd;
DataRegion[] _ngcwGcDataMap;
ulong _ngcwGcSysEnd;
byte? _nintendoDerivedDiscKey;
bool InitializeNgcwContext(MediaType dskType, Reader scsiReader, IWritableImage outputFormat)
{
_ngcwEnabled = dskType is MediaType.GOD or MediaType.WOD;
_ngcwMediaType = dskType;
_ngcwJunkCollector = new JunkCollector();
if(!_ngcwEnabled) return true;
if(scsiReader.OmniDriveNintendoMode)
{
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(_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;
}
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);
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(_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];
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(_nintendoDerivedDiscKey.HasValue) return true;
if(!scsiReader.OmniDriveNintendoMode) 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;
}
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;
}
List<WiiPartition> ParseWiiPartitionsFromDevice(Reader scsiReader)
{
byte[] partitionTable = ReadDiscBytesFromDevice(scsiReader, 0x40000, 32);
if(partitionTable == null) return null;
List<WiiPartition> partitions = new List<WiiPartition>();
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;
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;
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;
}
}

View File

@@ -209,10 +209,36 @@ partial class Dump
_writeStopwatch.Restart();
byte[] tmpBuf;
if(_ngcwEnabled)
{
if(!TransformNgcwLongSectors(scsiReader, buffer, i, blocksToRead, out SectorStatus[] statuses))
{
if(_stopOnError) return;
outputFormat.WriteSectorsLong(new byte[blockSize * _skip],
i,
false,
_skip,
Enumerable.Repeat(SectorStatus.NotDumped, (int)_skip).ToArray());
for(ulong b = i; b < i + _skip; b++) _resume.BadBlocks.Add(b);
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
{
outputFormat.WriteSectorsLong(buffer, i, false, blocksToRead, statuses);
}
}
else
{
var cmi = new byte[blocksToRead];
for(uint j = 0; j < blocksToRead; j++)
for (uint j = 0; j < blocksToRead; j++)
{
byte[] key = buffer.Skip((int)(2064 * j + 7)).Take(5).ToArray();
@@ -220,14 +246,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);
CSS.DecryptTitleKey(discKey, key, out byte[] tmpBuf);
outputFormat.WriteSectorTag(tmpBuf, i + j, false, SectorTagType.DvdTitleKeyDecrypted);
_resume.MissingTitleKeys?.Remove(i + j);
MarkTitleKeyDumped(i + j);
if(_storeEncrypted) continue;
@@ -258,6 +284,7 @@ partial class Dump
false,
blocksToRead,
Enumerable.Repeat(SectorStatus.Dumped, (int)blocksToRead).ToArray());
}
imageWriteDuration += _writeStopwatch.Elapsed.TotalSeconds;
extents.Add(i, blocksToRead, true);

View File

@@ -100,6 +100,15 @@ partial class Dump
extents.Add(badSector);
if(scsiReader.ReadBuffer3CReadRaw || scsiReader.OmniDriveReadRaw || scsiReader.HldtstReadRaw)
{
if(_ngcwEnabled)
{
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
{
var cmi = new byte[1];
@@ -109,13 +118,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];
}
@@ -141,6 +150,7 @@ partial class Dump
_resume.BadBlocks.Remove(badSector);
outputFormat.WriteSectorLong(buffer, badSector, false, SectorStatus.Dumped);
}
}
else
outputFormat.WriteSector(buffer, badSector, false, SectorStatus.Dumped);

View File

@@ -0,0 +1,100 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : TitleKeys.cs
// Author(s) : Rebecca Wallander <sakcheen@gmail.com>
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 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;
}
}

View File

@@ -77,6 +77,14 @@ sealed partial class Reader
internal uint PhysicalBlockSize { get; private set; }
internal uint LongBlockSize { get; private set; }
internal bool CanReadRaw { get; private set; }
/// <summary>When true with OmniDrive raw reads, use descramble=0 and software Nintendo descrambling (GameCube/Wii).</summary>
internal bool OmniDriveNintendoMode { get; set; }
/// <summary>
/// Disc-wide Nintendo XOR key (015) from LBA 0 CPR_MAI, set after the dump pipeline reads LBA 0. Used for
/// OmniDrive Nintendo reads at LBAs ≥ 16; null until derived.
/// </summary>
internal byte? NintendoDerivedDiscKey { get; set; }
internal bool CanSeek => _ataSeek || _seek6 || _seek10;
internal bool CanSeekLba => _ataSeekLba || _seek6 || _seek10;

View File

@@ -588,11 +588,22 @@ 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 _, true, true);
bool omniNintendoOk = !_dev.OmniDriveReadNintendoDvd(out _,
out senseBuf,
0,
1,
_timeout,
out _,
true,
true,
null,
false,
0);
OmniDriveReadRaw = omniStandardOk || omniNintendoOk;
}
if(HldtstReadRaw || _plextorReadRaw || ReadBuffer3CReadRaw || OmniDriveReadRaw)
@@ -848,6 +859,24 @@ sealed partial class Reader
else if(OmniDriveReadRaw)
{
uint lba = negative ? (uint)(-(long)block) : (uint)block;
if(OmniDriveNintendoMode)
{
ulong regularDataEndExclusive = Blocks + 1;
sense = _dev.OmniDriveReadNintendoDvd(out buffer,
out senseBuf,
lba,
count,
_timeout,
out duration,
false,
true,
NintendoDerivedDiscKey,
negative,
regularDataEndExclusive);
}
else
sense = _dev.OmniDriveReadRawDvd(out buffer,
out senseBuf,
lba,

View File

@@ -287,7 +287,7 @@ public partial class Convert
}
}
errno = ConvertNgcwSectors();
errno = ConvertNgcwSectors(useLong);
if(errno != ErrorNumber.NoError) return errno;
}
@@ -367,14 +367,14 @@ public partial class Convert
}
}
if(!isPs3Conversion && !isWiiuConversion && !isNgcwConversion && _negativeSectors > 0)
if(!isPs3Conversion && !isWiiuConversion && _negativeSectors > 0)
{
errno = ConvertNegativeSectors(useLong);
if(errno != ErrorNumber.NoError) return errno;
}
if(!isPs3Conversion && !isWiiuConversion && !isNgcwConversion && _overflowSectors > 0)
if(!isPs3Conversion && !isWiiuConversion && _overflowSectors > 0)
{
errno = ConvertOverflowSectors(useLong);

View File

@@ -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:

View File

@@ -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.
/// </summary>
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)
{
@@ -266,6 +271,216 @@ public partial class Convert
return ErrorNumber.NoError;
}
/// <summary>Fill a 32 KiB Wii encrypted group from 2048-byte logical ReadSector slices.</summary>
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);
}
}
/// <summary>
/// Read one encrypted Wii group from long sectors: copy main_data into <paramref name="encGrp" /> and
/// keep full long buffers for output (one ReadSectorLong per logical sector).
/// </summary>
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);
}
}
/// <summary>
/// Classify FST regions, run LFG junk detection on decrypted user data, and build the 32 KiB
/// plaintext group (hash + user, junk zeroed).
/// </summary>
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;
}
/// <summary>Wii sector conversion pipeline.</summary>
ErrorNumber ConvertWiiSectors(ulong discSize, ulong totalLogicalSectors, JunkCollector jc, ref ulong dataSectors,
ref ulong junkSectors)
@@ -294,7 +509,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)
{
@@ -302,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++)
@@ -747,4 +816,322 @@ 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;
}
/// <summary>Wii sector conversion pipeline for long sectors (main_data + framing).</summary>
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;
}
}

View File

@@ -43,6 +43,54 @@ namespace Aaru.Decoders.DVD;
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public sealed class Sector
{
/// <summary>Offset of main user data in a standard ECMA-267 DVD sector (bytes 12-2059).</summary>
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;
}
/// <summary>Gets the Physical Sector Number (PSN) from the sector header (bytes 1-3, big-endian).</summary>
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,
@@ -109,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]];
}
/// <summary>
/// Store seed and its cipher in cache
/// </summary>
@@ -137,6 +228,94 @@ public sealed class Sector
return edc;
}
/// <summary>
/// Check if the EDC of a sector is correct
/// </summary>
/// <param name="sectorLong">Buffer of the sector</param>
/// <returns><c>True</c> if EDC is correct, <c>False</c> if not</returns>
public static bool CheckEdc(byte[] sectorLong) => ComputeEdc(0, sectorLong, 2060) == BigEndianBitConverter.ToUInt32(sectorLong, 2060);
/// <summary>
/// Check if the EDC of sectors is correct
/// </summary>
/// <param name="sectorLong">Buffer of the sector</param>
/// <param name="transferLength">The number of sectors to check</param>
/// <returns><c>True</c> if EDC is correct, <c>False</c> if not</returns>
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;
}
/// <summary>
/// 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 <c>gpsxre::dvd::DataFrame::ID::valid()</c> (redumper).
/// </summary>
/// <param name="sectorLong">Buffer of the sector (must be at least 4 bytes; first 4 bytes are the ID field).</param>
/// <returns>The IED value in the same byte order as stored at sector bytes 4-5 (little-endian uint16 layout).</returns>
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<byte> 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);
}
/// <summary>
/// Check if the IED of a sector is correct (bytes 0-3 vs bytes 4-5, same layout as redumper <c>DataFrame::ID</c>).
/// </summary>
/// <param name="sectorLong">Buffer of the sector</param>
/// <returns><c>True</c> if IED is correct, <c>False</c> if not</returns>
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;
}
/// <summary>
/// Check if the IED of sectors is correct
/// </summary>
/// <param name="sectorLong">Buffer of the sector</param>
/// <param name="transferLength">The number of sectors to check</param>
/// <returns><c>True</c> if IED is correct, <c>False</c> if not</returns>
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;
}
/// <summary>
/// Tests if a seed unscrambles a sector correctly
/// </summary>
@@ -152,7 +331,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);
}
/// <summary>
@@ -208,17 +387,31 @@ 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;
}
/// <summary>
/// Descrambles a DVD sector. Uses PSN from header (bytes 1-3) to select XOR table.
/// </summary>
/// <param name="sector">Scrambled 2064-byte sector</param>
/// <param name="scrambled">Descrambled sector output</param>
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);

View File

@@ -0,0 +1,114 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Sector.cs
// Author(s) : Rebecca Wallander <sakcheen@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// 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
{
/// <summary>
/// Start of the 2048-byte DVD XOR (scramble) region in a 2064-byte Nintendo sector — same as ECMA-267
/// <c>main_data</c> for a standard DVD sector. Nintendo still applies the table to these 2048 bytes.
/// </summary>
public const int NintendoScrambledDataOffset = 12;
/// <summary>
/// Start of the 2048-byte logical <c>main_data</c> exposed to the game / filesystem in Nintendo GameCube/Wii DVD
/// sectors. Unlike ECMA-267 (where <c>main_data</c> 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 <see cref="NintendoScrambledDataOffset" />; bytes 611 are not part of that scrambled block.
/// </summary>
public const int NintendoMainDataOffset = 6;
/// <summary>
/// Derives the Nintendo descramble key from the first 8 bytes at <see cref="NintendoMainDataOffset" /> in the
/// LBA 0 sector after descramble (same 8 bytes used for key derivation in the drive/firmware path).
/// </summary>
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);
}
/// <summary>
/// Descrambles a Nintendo DVD sector. Uses PSN from header (bytes 1-3) to select XOR table and Nintendo key.
/// </summary>
/// <param name="sector">Scrambled 2064-byte sector</param>
/// <param name="nintendoKey">Nintendo key (0-15)</param>
/// <param name="scrambled">Descrambled sector output</param>
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 = NintendoScrambledDataOffset;
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;
}
}

View File

@@ -8,7 +8,7 @@
<Title>Aaru.Decryption</Title>
<Description>Decryption algorithms used by the Aaru Data Preservation Suite.</Description>
<PackageProjectUrl>https://github.com/aaru-dps/</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageLicenseExpression>(MIT AND GPL-3.0-only)</PackageLicenseExpression>
<RepositoryUrl>https://github.com/aaru-dps/Aaru.Decryption</RepositoryUrl>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<NeutralLanguage>en-US</NeutralLanguage>
@@ -32,9 +32,10 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Aaru.CommonTypes\Aaru.CommonTypes.csproj"/>
<ProjectReference Include="..\Aaru.Decoders\Aaru.Decoders.csproj"/>
<ProjectReference Include="..\Aaru.Devices\Aaru.Devices.csproj"/>
<ProjectReference Include="..\Aaru.Images\Aaru.Images.csproj"/>
<ProjectReference Include="..\Aaru.Helpers\Aaru.Helpers.csproj"/>
</ItemGroup>
<ItemGroup>

View File

@@ -5,7 +5,7 @@
// Filename : Crypto.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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;
/// <summary>Wii disc encryption helpers.</summary>
static class Crypto
public static class Crypto
{
/// <summary>Wii physical group size (32 KiB).</summary>
public const int GROUP_SIZE = 0x8000;

View File

@@ -5,7 +5,7 @@
// Filename : DataMap.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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;
/// <summary>A contiguous region of file data on disc.</summary>
readonly struct DataRegion : IComparable<DataRegion>
public readonly struct DataRegion : IComparable<DataRegion>
{
public readonly ulong Offset;
public readonly ulong Length;
@@ -55,7 +55,7 @@ readonly struct DataRegion : IComparable<DataRegion>
/// 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.
/// </summary>
static class DataMap
public static class DataMap
{
/// <summary>
/// Build a data region map from an FST (File System Table).

View File

@@ -5,7 +5,7 @@
// Filename : Junk.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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;
/// <summary>In-memory junk map entry.</summary>
struct JunkEntry
public struct JunkEntry
{
/// <summary>Disc byte offset where junk starts.</summary>
public ulong Offset;
@@ -55,7 +55,7 @@ struct JunkEntry
/// <summary>
/// Collects junk entries during conversion, merging contiguous entries with the same seed.
/// </summary>
sealed class JunkCollector
public sealed class JunkCollector
{
public int Count => Entries.Count;
@@ -110,7 +110,7 @@ sealed class JunkCollector
/// <summary>
/// Junk map serialization/deserialization and block-level junk detection.
/// </summary>
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)

View File

@@ -5,7 +5,7 @@
// Filename : Lfg.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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;
/// <summary>Lagged Fibonacci Generator for Nintendo GameCube/Wii junk fill.</summary>
static class Lfg
public static class Lfg
{
/// <summary>LFG buffer size (number of uint32 words in state).</summary>
const int K = 521;

View File

@@ -5,7 +5,7 @@
// Filename : Partitions.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// 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;
/// <summary>In-memory representation of a Wii partition.</summary>
struct WiiPartition
public struct WiiPartition
{
/// <summary>Partition offset on disc.</summary>
public ulong Offset;
@@ -58,7 +58,7 @@ struct WiiPartition
}
/// <summary>Wii partition region for the partition key map.</summary>
struct WiiPartitionRegion
public struct WiiPartitionRegion
{
/// <summary>First physical sector (0x8000-byte units).</summary>
public uint StartSector;
@@ -73,7 +73,7 @@ struct WiiPartitionRegion
/// <summary>
/// Wii partition table parsing and key map serialization.
/// </summary>
static class Partitions
public static class Partitions
{
/// <summary>
/// Parse the Wii partition table from a source image, extracting all partitions

View File

@@ -33,13 +33,57 @@
// ****************************************************************************/
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();
readonly Sector _dvdSectorDecoder = new Sector();
enum OmniDriveDiscType
{
CD = 0,
DVD = 1,
BD = 2
}
/// <summary>
/// Encodes byte 1 of the OmniDrive READ CDB to match redumper's <c>CDB12_ReadOmniDrive</c>
/// (<c>scsi/mmc.ixx</c>): <c>disc_type</c> :2 (LSB), <c>raw_addressing</c> :1, <c>fua</c> :1, <c>descramble</c> :1, reserved :3.
/// </summary>
/// <param name="discType">0 = CD, 1 = DVD, 2 = BD (redumper <c>OmniDrive_DiscType</c>).</param>
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<byte> 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
}
/// <summary>
/// Checks if the drive has OmniDrive firmware by inspecting INQUIRY Reserved5 (bytes 74+) for "OmniDrive",
/// matching redumper's is_omnidrive_firmware behaviour.
@@ -49,20 +93,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;
@@ -76,34 +120,110 @@ public partial class Device
/// <param name="transferLength">Number of 2064-byte sectors to read.</param>
/// <param name="timeout">Timeout in seconds.</param>
/// <param name="duration">Duration in milliseconds it took for the device to execute the command.</param>
/// <param name="fua">Set to <c>true</c> if the command should use FUA.</param>
/// <param name="descramble">Set to <c>true</c> if the data should be descrambled by the device.</param>
public bool OmniDriveReadRawDvd(out byte[] buffer, out ReadOnlySpan<byte> senseBuffer, uint lba, uint transferLength,
uint timeout, out double duration)
uint timeout, out double duration, bool fua = false, bool descramble = true)
{
senseBuffer = SenseBuffer;
Span<byte> 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);
if (!Sector.CheckIed(buffer, transferLength)) return true;
if(descramble && !Sector.CheckEdc(buffer, transferLength)) return true;
Error = LastError != 0;
AaruLogging.Debug(SCSI_MODULE_NAME, "OmniDrive READ RAW DVD took {0} ms", duration);
return sense;
}
/// <summary>
/// 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 015 use key 0; LBAs ≥ 16 use <paramref name="derivedDiscKey" /> (from LBA 0 CPR_MAI), or 0 if not yet
/// derived.
/// </summary>
/// <param name="derivedDiscKey">Disc key from LBA 0 (015); <c>null</c> until the host has read and derived it.</param>
/// <param name="negativeAddressing">
/// True when caller is reading wrapped negative LBAs (lead-in); these sectors use standard DVD descramble.
/// </param>
/// <param name="regularDataEndExclusive">
/// Exclusive upper bound of regular user-data LBAs. Sectors at or above this are leadout and use standard DVD
/// descramble.
/// </param>
/// <returns><c>true</c> if the command failed and <paramref name="senseBuffer" /> contains the sense buffer.</returns>
public bool OmniDriveReadNintendoDvd(out byte[] buffer, out ReadOnlySpan<byte> senseBuffer, uint lba, uint transferLength,
uint timeout, out double duration, bool fua = false, bool descramble = true,
byte? derivedDiscKey = null, bool negativeAddressing = false,
ulong regularDataEndExclusive = 0)
{
senseBuffer = SenseBuffer;
Span<byte> cdb = CdbBuffer[..12];
buffer = new byte[2064 * transferLength];
FillOmniDriveReadDvdCdb(cdb,
lba,
transferLength,
EncodeOmniDriveReadCdb1(OmniDriveDiscType.DVD, false, fua, false));
LastError = SendScsiCommand(cdb, ref buffer, timeout, ScsiDirection.In, out duration, out bool sense);
if(!Sector.CheckIed(buffer, transferLength)) return true;
if(descramble)
{
const int sectorBytes = 2064;
byte[] outBuf = new byte[sectorBytes * transferLength];
const uint maxRegularLba = 0x00FFFFFF;
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
{
byte key = absLba < 16 ? (byte)0 : (derivedDiscKey ?? (byte)0);
errno = _nintendoSectorDecoder.Scramble(slice, key, out descrambled);
}
if(errno != ErrorNumber.NoError || descrambled == null)
{
LastError = (int)errno;
Error = true;
return true;
}
Array.Copy(descrambled, 0, outBuf, i * sectorBytes, sectorBytes);
}
buffer = outBuf;
}
Error = LastError != 0;
AaruLogging.Debug(SCSI_MODULE_NAME, "OmniDrive READ NINTENDO DVD took {0} ms", duration);
return sense;
}
}

View File

@@ -753,7 +753,8 @@ public sealed partial class MediaDumpViewModel : ViewModelBase
true,
1080,
Paranoia,
CureParanoia);
CureParanoia,
false);
new Thread(DoWork).Start();
}

View File

@@ -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,

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
@@ -50,6 +50,7 @@
<ProjectReference Include="..\Aaru.Helpers\Aaru.Helpers.csproj"/>
<ProjectReference Include="..\Aaru.Logging\Aaru.Logging.csproj"/>
<ProjectReference Include="..\Aaru.Decoders\Aaru.Decoders.csproj"/>
<ProjectReference Include="..\Aaru.Decryption\Aaru.Decryption.csproj"/>
<ProjectReference Include="..\Aaru.Filters\Aaru.Filters.csproj"/>
<ProjectReference Include="..\Aaru.Settings\Aaru.Settings.csproj"/>
</ItemGroup>

View File

@@ -1,4 +1,4 @@
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// <auto-generated>
// 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);
}
}
}
}

View File

@@ -2970,4 +2970,10 @@
<data name="WinOnCD_disc_image" xml:space="preserve">
<value>Imagen de disco de WinOnCD</value>
</data>
<data name="Redumper_Name" xml:space="preserve">
<value>Volcado DVD crudo de Redumper</value>
</data>
<data name="Redumper_disc_image" xml:space="preserve">
<value>Imagen de disco DVD crudo de Redumper</value>
</data>
</root>

View File

@@ -2980,4 +2980,10 @@
<data name="WinOnCD_disc_image" xml:space="preserve">
<value>WinOnCD disc image</value>
</data>
<data name="Redumper_Name" xml:space="preserve">
<value>Redumper raw DVD dump</value>
</data>
<data name="Redumper_disc_image" xml:space="preserve">
<value>Redumper raw DVD disc image</value>
</data>
</root>

View File

@@ -0,0 +1,71 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Identify.cs
// Author(s) : Rebecca Wallander <sakcheen@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Rebecca Wallander
// ****************************************************************************/
using System.IO;
using Aaru.CommonTypes.Interfaces;
namespace Aaru.Images;
public sealed partial class Redumper
{
#region IOpticalMediaImage Members
/// <inheritdoc />
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
}

View File

@@ -0,0 +1,161 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Ngcw.cs
// Author(s) : Rebecca Wallander <sakcheen@gmail.com>
//
// 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;
/// <summary>
/// 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.
/// </summary>
void TryInitializeNgcwAfterOpen()
{
_nintendoDerivedKey = null;
if(!IsNintendoMediaType(_imageInfo.MediaType)) return;
EnsureNintendoDerivedKeyFromLba0();
}
/// <summary>
/// Derives the Nintendo key from LBA 0 so sectors 16+ can be descrambled.
/// </summary>
/// <returns><c>True</c> if the Nintendo key was derived successfully, <c>False</c> if not</returns>
bool EnsureNintendoDerivedKeyFromLba0()
{
ErrorNumber errno = ReadSectorLongForNgcw(0, false, out byte[] long0, out _);
return errno == ErrorNumber.NoError && long0 != null && long0.Length >= NGCW_LONG_SECTOR_SIZE;
}
/// <summary>
/// Reads a sector long for Nintendo descrambling.
/// </summary>
/// <param name="sectorAddress">The sector address to read</param>
/// <param name="negative">Whether the sector address is negative</param>
/// <param name="buffer">The buffer to read the sector into</param>
/// <param name="sectorStatus">The status of the sector</param>
/// <returns>The error number</returns>
/// <remarks>
/// 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.
/// </remarks>
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);
bool leadIn = lba < 0;
bool leadOut = _ngcwRegularDataSectors > 0 && lba >= (long)_ngcwRegularDataSectors;
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)
{
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;
}
}

View File

@@ -0,0 +1,120 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Properties.cs
// Author(s) : Rebecca Wallander <sakcheen@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// 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
/// <inheritdoc />
// ReSharper disable once ConvertToAutoProperty
public ImageInfo Info => _imageInfo;
/// <inheritdoc />
public string Name => Localization.Redumper_Name;
/// <inheritdoc />
public Guid Id => new("F2D3E4A5-B6C7-4D8E-9F0A-1B2C3D4E5F60");
/// <inheritdoc />
public string Author => Authors.RebeccaWallander;
/// <inheritdoc />
public string Format => Localization.Redumper_disc_image;
/// <inheritdoc />
public List<Partition> Partitions { get; private set; }
/// <inheritdoc />
public List<Track> Tracks { get; private set; }
/// <inheritdoc />
public List<Session> Sessions { get; private set; }
/// <inheritdoc />
public List<DumpHardware> DumpHardware => null;
/// <inheritdoc />
public Metadata AaruMetadata => null;
/// <inheritdoc />
public IEnumerable<MediaTagType> SupportedMediaTags =>
[
MediaTagType.DVD_PFI,
MediaTagType.DVD_PFI_2ndLayer,
MediaTagType.DVD_DMI,
MediaTagType.DVD_BCA
];
/// <inheritdoc />
public IEnumerable<SectorTagType> SupportedSectorTags =>
[
SectorTagType.DvdSectorInformation,
SectorTagType.DvdSectorNumber,
SectorTagType.DvdSectorIed,
SectorTagType.DvdSectorCmi,
SectorTagType.DvdSectorTitleKey,
SectorTagType.DvdSectorEdc
];
/// <inheritdoc />
public IEnumerable<MediaType> 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,
];
/// <inheritdoc />
public IEnumerable<(string name, Type type, string description, object @default)> SupportedOptions => [];
/// <inheritdoc />
public IEnumerable<string> KnownExtensions => [".state"];
/// <inheritdoc />
public bool IsWriting => false;
/// <inheritdoc />
public string ErrorMessage { get; private set; }
#endregion
}

View File

@@ -0,0 +1,474 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Read.cs
// Author(s) : Rebecca Wallander <sakcheen@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// 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
/// <inheritdoc />
public ErrorNumber Open(IFilter imageFilter)
{
string filename = imageFilter.Filename;
_ngcwRegularDataSectors = 0;
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<MediaTagType, byte[]>();
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
};
if(decodedPfi.Value.DataAreaEndPSN >= decodedPfi.Value.DataAreaStartPSN)
_ngcwRegularDataSectors =
(ulong)(decodedPfi.Value.DataAreaEndPSN - decodedPfi.Value.DataAreaStartPSN) + 1;
}
}
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public ErrorNumber ReadSectorLong(ulong sectorAddress, bool negative, out byte[] buffer,
out SectorStatus sectorStatus) =>
ReadSectorLongForNgcw(sectorAddress, negative, out buffer, out sectorStatus);
/// <inheritdoc />
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;
}
/// <inheritdoc />
public ErrorNumber ReadSectorTag(ulong sectorAddress, bool negative, SectorTagType tag, out byte[] buffer)
{
buffer = null;
return ReadSectorsTag(sectorAddress, negative, 1, tag, out buffer);
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public ErrorNumber ReadSector(ulong sectorAddress, uint track, out byte[] buffer, out SectorStatus sectorStatus) =>
ReadSector(sectorAddress, false, out buffer, out sectorStatus);
/// <inheritdoc />
public ErrorNumber ReadSectorLong(ulong sectorAddress, uint track, out byte[] buffer,
out SectorStatus sectorStatus) =>
ReadSectorLong(sectorAddress, false, out buffer, out sectorStatus);
/// <inheritdoc />
public ErrorNumber ReadSectors(ulong sectorAddress, uint length, uint track, out byte[] buffer,
out SectorStatus[] sectorStatus) =>
ReadSectors(sectorAddress, false, length, out buffer, out sectorStatus);
/// <inheritdoc />
public ErrorNumber ReadSectorsLong(ulong sectorAddress, uint length, uint track, out byte[] buffer,
out SectorStatus[] sectorStatus) =>
ReadSectorsLong(sectorAddress, false, length, out buffer, out sectorStatus);
/// <inheritdoc />
public ErrorNumber ReadSectorTag(ulong sectorAddress, uint track, SectorTagType tag, out byte[] buffer) =>
ReadSectorTag(sectorAddress, false, tag, out buffer);
/// <inheritdoc />
public ErrorNumber ReadSectorsTag(ulong sectorAddress, uint length, uint track, SectorTagType tag,
out byte[] buffer) =>
ReadSectorsTag(sectorAddress, false, length, tag, out buffer);
/// <inheritdoc />
public List<Track> GetSessionTracks(Session session) => Tracks;
/// <inheritdoc />
public List<Track> GetSessionTracks(ushort session) => Tracks;
#endregion
/// <summary>
/// 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).
/// </summary>
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;
}
/// <summary>Maps a Redumper state byte to an Aaru SectorStatus.</summary>
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)
};
/// <summary>Loads Redumper sidecar files (.physical, .manufacturer, .bca) as media tags.</summary>
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);
}
}
}
/// <summary>
/// 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.
/// </summary>
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);
}
}

View File

@@ -0,0 +1,119 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Redumper.cs
// Author(s) : Rebecca Wallander <sakcheen@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2011-2026 Rebecca Wallander
// ****************************************************************************/
using System.Collections.Generic;
using Aaru.CommonTypes.Enums;
using Aaru.CommonTypes.Interfaces;
using Aaru.CommonTypes.Structs;
namespace Aaru.Images;
/// <inheritdoc cref="Aaru.CommonTypes.Interfaces.IOpticalMediaImage" />
/// <summary>
/// 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 ReedSolomon 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).
/// </summary>
public sealed partial class Redumper : IOpticalMediaImage
{
const string MODULE_NAME = "Redumper plugin";
/// <summary>Size of a single DVD RecordingFrame: 12 rows of (172 main + 10 PI) + 182 PO.</summary>
const int RECORDING_FRAME_SIZE = 2366;
/// <summary>Size of a DVD sector without parity (ID + CPR_MAI + user data + EDC).</summary>
const int DVD_SECTOR_SIZE = 2064;
/// <summary>DVD user data size.</summary>
const int DVD_USER_DATA_SIZE = 2048;
/// <summary>Number of main-data bytes per row in a RecordingFrame.</summary>
const int ROW_MAIN_DATA_SIZE = 172;
/// <summary>Number of inner-parity bytes per row.</summary>
const int ROW_PARITY_INNER_SIZE = 10;
/// <summary>Number of rows in a RecordingFrame.</summary>
const int RECORDING_FRAME_ROWS = 12;
/// <summary>Size of the outer parity block.</summary>
const int PARITY_OUTER_SIZE = 182;
/// <summary>
/// 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).
/// </summary>
const int LBA_START = -0x30000;
/// <summary>SCSI READ DVD STRUCTURE parameter list header size (4 bytes).</summary>
const int SCSI_HEADER_SIZE = 4;
readonly Decoders.DVD.Sector _decoding = new();
readonly Decoders.Nintendo.Sector _nintendoDecoder = new();
/// <summary>Derived Nintendo key from LBA 0 so sectors 16+ can be descrambled.</summary>
byte? _nintendoDerivedKey;
ulong _ngcwRegularDataSectors;
IFilter _imageFilter;
ImageInfo _imageInfo;
Dictionary<MediaTagType, byte[]> _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
};
}

View File

@@ -0,0 +1,96 @@
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : Verify.cs
// Author(s) : Rebecca Wallander <sakcheen@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// 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
/// <inheritdoc />
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;
ErrorNumber errno = ReadSectorLong(sectorAddress, false, out byte[] dvdSector, out _);
if(errno != ErrorNumber.NoError || dvdSector is null) return null;
if(!Sector.CheckIed(dvdSector)) return false;
return Sector.CheckEdc(dvdSector, 1);
}
/// <inheritdoc />
public bool? VerifySectors(ulong sectorAddress, uint length, out List<ulong> failingLbas,
out List<ulong> 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;
}
/// <inheritdoc />
public bool? VerifySectors(ulong sectorAddress, uint length, uint track, out List<ulong> failingLbas,
out List<ulong> unknownLbas) =>
VerifySectors(sectorAddress, length, out failingLbas, out unknownLbas);
#endregion
}

View File

@@ -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);

View File

@@ -925,6 +925,18 @@
<data name="Dumping_Nintendo_GameCube_or_Wii_discs_is_not_yet_implemented" xml:space="preserve">
<value>[red]El volcado de discos de Nintendo GameCube o Wii no está implementado todavía.[/]</value>
</data>
<data name="Nintendo_OmniDrive_dump_requires_Aaru_format" xml:space="preserve">
<value>[red]El volcado de discos de Nintendo GameCube o Wii con OmniDrive requiere formato de salida Aaru.[/]</value>
</data>
<data name="Ngcw_dump_parsing_partition_table" xml:space="preserve">
<value>[slateblue1]Analizando tabla de particiones de Wii...[/]</value>
</data>
<data name="Ngcw_dump_wrote_partition_key_map" xml:space="preserve">
<value>[slateblue1]Se guardó el mapa de claves de partición de Wii.[/]</value>
</data>
<data name="Ngcw_dump_wrote_junk_map_0_entries" xml:space="preserve">
<value>[slateblue1]Se guardó el mapa de basura de Nintendo con [lime]{0}[/] entradas.[/]</value>
</data>
<data name="Dumping_progress_log" xml:space="preserve">
<value>################# Registro de progreso del volcado #################</value>
</data>

View File

@@ -1126,6 +1126,9 @@
<data name="Image_is_not_writable_aborting" xml:space="preserve">
<value>[red]Image is not writable, aborting...[/]</value>
</data>
<data name="Cannot_write_retried_sector_0_no_writable_output_image" xml:space="preserve">
<value>[red]Cannot write retried sector [lime]{0}[/]: output is not a writable image.[/]</value>
</data>
<data name="Could_not_detect_capacity" xml:space="preserve">
<value>[red]Could not detect capacity...[/]</value>
</data>
@@ -1395,6 +1398,18 @@
<data name="Dumping_Nintendo_GameCube_or_Wii_discs_is_not_yet_implemented" xml:space="preserve">
<value>[red]Dumping Nintendo GameCube or Wii discs is not yet implemented.[/]</value>
</data>
<data name="Nintendo_OmniDrive_dump_requires_Aaru_format" xml:space="preserve">
<value>[red]Dumping Nintendo GameCube or Wii discs with OmniDrive requires Aaru output format.[/]</value>
</data>
<data name="Ngcw_dump_parsing_partition_table" xml:space="preserve">
<value>[slateblue1]Parsing Wii partition table...[/]</value>
</data>
<data name="Ngcw_dump_wrote_partition_key_map" xml:space="preserve">
<value>[slateblue1]Stored Wii partition key map.[/]</value>
</data>
<data name="Ngcw_dump_wrote_junk_map_0_entries" xml:space="preserve">
<value>[slateblue1]Stored Nintendo junk map with [lime]{0}[/] entries.[/]</value>
</data>
<data name="Reading_Disc_Manufacturing_Information" xml:space="preserve">
<value>[slateblue1]Reading [italic]Disc Manufacturing Information[/][/]</value>
</data>

View File

@@ -1,4 +1,4 @@
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
@@ -1983,6 +1983,24 @@ 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 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);

View File

@@ -663,6 +663,15 @@
</data>
<data name="Ngcw_disc_number_0" xml:space="preserve">
<value>[slateblue1]Número de disco: [green]{0}[/].[/]</value>
</data>
<data name="Ngcw_nintendo_software_descramble" xml:space="preserve">
<value>[slateblue1]Descifrado por software de Nintendo activado (OmniDrive en bruto, descramble=0).[/]</value>
</data>
<data name="Ngcw_nintendo_derived_key_0" xml:space="preserve">
<value>[slateblue1]Clave de disco Nintendo derivada: [green]{0}[/].[/]</value>
</data>
<data name="Ngcw_wii_dump_bypass_decryption" xml:space="preserve">
<value>[slateblue1]Omisión del descifrado de particiones Wii: guardando datos de partición cifrados tal como se leen.[/]</value>
</data>
<data name="PS3_disc_key_resolved_from_0" xml:space="preserve">
<value>[slateblue1]Clave de disco PS3 resuelta desde [aqua]{0}[/].[/]</value>

View File

@@ -971,7 +971,7 @@ In you are unsure, please press N to not continue.</value>
<value>Skip Wii U disc encryption processing during conversion.</value>
</data>
<data name="Bypass_Wii_decryption_help" xml:space="preserve">
<value>Skip Wii disc encryption processing during conversion.</value>
<value>Skip Wii disc encryption processing during conversion or dump.</value>
</data>
<data name="Ngcw_parsing_partition_table" xml:space="preserve">
<value>[slateblue1]Parsing Wii partition table...[/]</value>
@@ -1002,6 +1002,15 @@ In you are unsure, please press N to not continue.</value>
</data>
<data name="Ngcw_disc_number_0" xml:space="preserve">
<value>[slateblue1]Disc number: [green]{0}[/].[/]</value>
</data>
<data name="Ngcw_nintendo_software_descramble" xml:space="preserve">
<value>[slateblue1]Nintendo software descrambling enabled (OmniDrive raw, descramble=0).[/]</value>
</data>
<data name="Ngcw_nintendo_derived_key_0" xml:space="preserve">
<value>[slateblue1]Derived Nintendo disc key: [green]{0}[/].[/]</value>
</data>
<data name="Ngcw_wii_dump_bypass_decryption" xml:space="preserve">
<value>[slateblue1]Wii partition decryption bypass: storing encrypted partition data as read.[/]</value>
</data>
<data name="PS3_disc_key_resolved_from_0" xml:space="preserve">
<value>[slateblue1]PS3 disc key resolved from [aqua]{0}[/].[/]</value>

View File

@@ -116,6 +116,7 @@ sealed class DumpMediaCommand : Command<DumpMediaCommand.Settings>
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<DumpMediaCommand.Settings>
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<DumpMediaCommand.Settings>
[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)]