mirror of
https://github.com/aaru-dps/Aaru.git
synced 2026-04-05 21:44:05 +00:00
Merge pull request #920 from aaru-dps/fakeshemp/edc
Dump GameCube and Wii with OmniDrive
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
571
Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs
Normal file
571
Aaru.Core/Devices/Dumping/Sbc/Ngcw.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
100
Aaru.Core/Devices/Dumping/TitleKeys.cs
Normal file
100
Aaru.Core/Devices/Dumping/TitleKeys.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 (0–15) from LBA 0 CPR_MAI, set after the dump pipeline reads LBA 0. Used for
|
||||
/// OmniDrive Nintendo reads at LBAs ≥ 16; null until derived.
|
||||
/// </summary>
|
||||
internal byte? NintendoDerivedDiscKey { get; set; }
|
||||
internal bool CanSeek => _ataSeek || _seek6 || _seek10;
|
||||
internal bool CanSeekLba => _ataSeekLba || _seek6 || _seek10;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
114
Aaru.Decoders/Nintendo/Sector.cs
Normal file
114
Aaru.Decoders/Nintendo/Sector.cs
Normal 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 6–11 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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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).
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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 0–15 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 (0–15); <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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -753,7 +753,8 @@ public sealed partial class MediaDumpViewModel : ViewModelBase
|
||||
true,
|
||||
1080,
|
||||
Paranoia,
|
||||
CureParanoia);
|
||||
CureParanoia,
|
||||
false);
|
||||
|
||||
new Thread(DoWork).Start();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
14
Aaru.Images/Localization/Localization.Designer.cs
generated
14
Aaru.Images/Localization/Localization.Designer.cs
generated
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
71
Aaru.Images/Redumper/Identify.cs
Normal file
71
Aaru.Images/Redumper/Identify.cs
Normal 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
|
||||
}
|
||||
161
Aaru.Images/Redumper/Ngcw.cs
Normal file
161
Aaru.Images/Redumper/Ngcw.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
120
Aaru.Images/Redumper/Properties.cs
Normal file
120
Aaru.Images/Redumper/Properties.cs
Normal 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
|
||||
}
|
||||
474
Aaru.Images/Redumper/Read.cs
Normal file
474
Aaru.Images/Redumper/Read.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
119
Aaru.Images/Redumper/Redumper.cs
Normal file
119
Aaru.Images/Redumper/Redumper.cs
Normal 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 Reed–Solomon parity). The .state file has one byte per frame
|
||||
/// indicating dump status. The first frame in the file corresponds to physical
|
||||
/// sector number LBA_START (-0x30000 / -196608).
|
||||
/// </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
|
||||
};
|
||||
}
|
||||
96
Aaru.Images/Redumper/Verify.cs
Normal file
96
Aaru.Images/Redumper/Verify.cs
Normal 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
|
||||
}
|
||||
6
Aaru.Localization/Core.Designer.cs
generated
6
Aaru.Localization/Core.Designer.cs
generated
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
20
Aaru.Localization/UI.Designer.cs
generated
20
Aaru.Localization/UI.Designer.cs
generated
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user