Files
Aaru/Aaru.Core/Image/Convert/Ngcw/ConvertNgcw.cs
2026-04-03 08:38:41 +02:00

888 lines
34 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// /***************************************************************************
// Aaru Data Preservation Suite
// ----------------------------------------------------------------------------
//
// Filename : ConvertNgcw.cs
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// Component : Image conversion.
//
// --[ Description ] ----------------------------------------------------------
//
// GameCube / Wii conversion pipeline: inject media tags, convert sectors
// with junk detection and Wii decryption, enrich metadata.
// Port of tool/ngcw/convert.c.
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2019-2026 Natalia Portillo
// ****************************************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using Aaru.CommonTypes;
using Aaru.CommonTypes.Enums;
using Aaru.Decoders.Nintendo;
using Aaru.Decryption.Ngcw;
using NgcwPartitions = Aaru.Decryption.Ngcw.Partitions;
using Aaru.Helpers;
using Aaru.Localization;
namespace Aaru.Core.Image;
public partial class Convert
{
DataRegion[][] _ngcwPartDataMaps;
List<WiiPartition> _ngcwPartitions;
ulong[] _ngcwPartSysEnd;
WiiPartitionRegion[] _ngcwRegions;
/// <summary>
/// For WOD: parses the Wii partition table, decrypts title keys, builds the
/// partition key map, and writes it as a media tag.
/// For GOD: no-op (GameCube has no encryption).
/// Must be called before ConvertMediaTags.
/// </summary>
ErrorNumber InjectNgcwMediaTags()
{
if(_aborted) return ErrorNumber.NoError;
// GOD has no partition keys to inject
if(_mediaType == MediaType.GOD) return ErrorNumber.NoError;
InitProgress?.Invoke();
// Parse Wii partition table
PulseProgress?.Invoke(UI.Ngcw_parsing_partition_table);
_ngcwPartitions = NgcwPartitions.ParseWiiPartitions(_inputImage);
if(_ngcwPartitions == null)
{
EndProgress?.Invoke();
StoppingErrorMessage?.Invoke(UI.Ngcw_cannot_parse_partitions);
return ErrorNumber.InvalidArgument;
}
UpdateStatus?.Invoke(string.Format(UI.Ngcw_found_0_partitions, _ngcwPartitions.Count));
// Build partition region map
PulseProgress?.Invoke(UI.Ngcw_building_partition_key_map);
_ngcwRegions = NgcwPartitions.BuildRegionMap(_ngcwPartitions);
// Serialize and write partition key map
byte[] keyMapData = NgcwPartitions.SerializeKeyMap(_ngcwRegions);
_outputImage.WriteMediaTag(keyMapData, MediaTagType.WiiPartitionKeyMap);
UpdateStatus?.Invoke(UI.Ngcw_written_partition_key_map);
EndProgress?.Invoke();
return ErrorNumber.NoError;
}
/// <summary>
/// Converts GameCube/Wii sectors with junk detection (and Wii decryption).
/// For GC: reads blocks, detects junk via LFG, writes with Dumped/Generable status.
/// For Wii: decrypts partition groups, detects junk in user data, zeroes junk,
/// writes with Unencrypted status; plaintext areas use Dumped/Generable.
/// Does not copy sector tags, negative sectors, or overflow sectors.
/// </summary>
ErrorNumber ConvertNgcwSectors(bool useLong)
{
if(_aborted) return ErrorNumber.NoError;
InitProgress?.Invoke();
ulong totalLogicalSectors = _inputImage.Info.Sectors;
ulong discSize = totalLogicalSectors * Crypto.SECTOR_SIZE;
var jc = new JunkCollector();
ulong dataSectors = 0;
ulong junkSectors = 0;
if(_mediaType == MediaType.GOD)
{
ErrorNumber errno = useLong ?
ConvertGameCubeSectorsLong(discSize, totalLogicalSectors, jc, ref dataSectors, ref junkSectors) :
ConvertGameCubeSectors(discSize, totalLogicalSectors, jc, ref dataSectors, ref junkSectors);
if(errno != ErrorNumber.NoError)
{
EndProgress?.Invoke();
return errno;
}
}
else
{
ErrorNumber errno = useLong ?
ConvertWiiSectorsLong(discSize, totalLogicalSectors, jc, ref dataSectors, ref junkSectors) :
ConvertWiiSectors(discSize, totalLogicalSectors, jc, ref dataSectors, ref junkSectors);
if(errno != ErrorNumber.NoError)
{
EndProgress?.Invoke();
return errno;
}
}
// Store junk map
if(jc.Count > 0)
{
byte[] junkMapData = Junk.Serialize(jc.Entries);
_outputImage.WriteMediaTag(junkMapData, MediaTagType.NgcwJunkMap);
UpdateStatus?.Invoke(string.Format(UI.Ngcw_stored_junk_map_0_entries_1_bytes,
jc.Count,
junkMapData.Length));
}
UpdateStatus?.Invoke(string.Format(UI.Ngcw_converted_0_data_1_junk, dataSectors, junkSectors));
EndProgress?.Invoke();
return ErrorNumber.NoError;
}
/// <summary>GameCube sector conversion pipeline.</summary>
ErrorNumber ConvertGameCubeSectors(ulong discSize, ulong totalLogicalSectors, JunkCollector jc,
ref ulong dataSectors, ref ulong junkSectors)
{
const int blockSize = Crypto.GROUP_SIZE; // 0x8000
const int sectorsPerBlock = Crypto.LOGICAL_PER_GROUP;
const int sectorSize = Crypto.SECTOR_SIZE;
// Read disc header to get FST info
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 + sectorSize - 1) / sectorSize);
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);
}
// Process disc in 0x8000-byte blocks
var blockBuf = new byte[blockSize];
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 / sectorSize;
UpdateProgress?.Invoke(string.Format(UI.Converting_sectors_0_to_1,
baseSector,
baseSector + sectorsPerBlock),
(long)baseSector,
(long)totalLogicalSectors);
// Read block
for(var s = 0; s < sectorsPerBlock && s * sectorSize < blockBytes; s++)
{
ErrorNumber errno = _inputImage.ReadSector(baseSector + (ulong)s, false, out byte[] sectorData, out _);
if(errno != ErrorNumber.NoError || sectorData == null)
Array.Clear(blockBuf, s * sectorSize, sectorSize);
else
Array.Copy(sectorData, 0, blockBuf, s * sectorSize, sectorSize);
}
// Detect junk
Junk.DetectJunkInBlock(blockBuf,
blockBytes,
blockOff,
dataMap,
sysEnd,
0xFFFF,
jc,
ref dataSectors,
ref junkSectors,
sectorStatuses);
// Write sectors
int numSectors = blockBytes / sectorSize;
for(var si = 0; si < numSectors; si++)
{
ulong sector = baseSector + (ulong)si;
var sectorData = new byte[sectorSize];
Array.Copy(blockBuf, si * sectorSize, sectorData, 0, sectorSize);
bool ok = _outputImage.WriteSector(sectorData, 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.</summary>
ErrorNumber ConvertWiiSectors(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;
// Build FST data maps for each partition
BuildWiiPartitionFstMaps();
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);
// Check if inside a partition's data area
int inPart = NgcwPartitions.FindPartitionAtOffset(_ngcwPartitions, offset);
if(inPart >= 0)
{
// Inside partition — decrypt group, detect junk, write
ulong groupDiscOff = _ngcwPartitions[inPart].DataOffset +
(offset - _ngcwPartitions[inPart].DataOffset) / groupSize * groupSize;
// Read encrypted group
var encGrp = new byte[groupSize];
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++;
}
}
// Write all 16 sectors as SectorStatusUnencrypted
for(var s = 0; s < sectorsPerBlock; s++)
{
ulong sector = groupDiscOff / sectorSize + (ulong)s;
var sectorData = new byte[sectorSize];
Array.Copy(decryptedGroup, s * sectorSize, sectorData, 0, sectorSize);
bool ok = _outputImage.WriteSector(sectorData, 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
{
// Outside partition — read as plaintext, detect junk
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(var s = 0; s < sectorsPerBlock && s * sectorSize < blockBytes; s++)
{
ulong sec = alignedOff / sectorSize + (ulong)s;
ErrorNumber errno = _inputImage.ReadSector(sec, false, out byte[] sd, out _);
if(errno != ErrorNumber.NoError || sd == null)
Array.Clear(blockBuf, s * sectorSize, sectorSize);
else
Array.Copy(sd, 0, 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(var si = 0; si < numSectors; si++)
{
ulong sector = alignedOff / sectorSize + (ulong)si;
var sectorData = new byte[sectorSize];
Array.Copy(blockBuf, si * sectorSize, sectorData, 0, sectorSize);
bool ok = _outputImage.WriteSector(sectorData, 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;
}
/// <summary>Build FST data maps for each Wii partition.</summary>
void BuildWiiPartitionFstMaps()
{
if(_ngcwPartitions == null || _ngcwPartitions.Count == 0) return;
_ngcwPartDataMaps = new DataRegion[_ngcwPartitions.Count][];
_ngcwPartSysEnd = new ulong[_ngcwPartitions.Count];
for(var p = 0; p < _ngcwPartitions.Count; p++)
{
// Read and decrypt first group to get boot block
var encGrp0 = new byte[Crypto.GROUP_SIZE];
int sectorsToRead = Crypto.LOGICAL_PER_GROUP;
for(var s = 0; s < sectorsToRead; s++)
{
ulong sec = _ngcwPartitions[p].DataOffset / Crypto.SECTOR_SIZE + (ulong)s;
ErrorNumber errno = _inputImage.ReadSector(sec, false, out byte[] sd, out _);
if(errno != ErrorNumber.NoError || sd == null) continue;
Array.Copy(sd, 0, encGrp0, s * Crypto.SECTOR_SIZE, Crypto.SECTOR_SIZE);
}
var hb0 = new byte[Crypto.GROUP_HASH_SIZE];
var gd0 = new byte[Crypto.GROUP_DATA_SIZE];
Crypto.DecryptGroup(_ngcwPartitions[p].TitleKey, encGrp0, hb0, gd0);
uint fstOffsetP = BigEndianBitConverter.ToUInt32(gd0, 0x424) << 2;
uint fstSizeP = BigEndianBitConverter.ToUInt32(gd0, 0x428) << 2;
_ngcwPartSysEnd[p] = fstOffsetP + fstSizeP;
if(fstSizeP == 0 || fstSizeP >= 64 * 1024 * 1024) continue;
// Read FST from the partition (decrypting groups as needed)
var fstP = new byte[fstSizeP];
uint fstRead = 0;
var fstOk = true;
while(fstRead < fstSizeP)
{
ulong logicalOff = fstOffsetP + fstRead;
ulong grpIdx = logicalOff / Crypto.GROUP_DATA_SIZE;
var grpOff = (int)(logicalOff % Crypto.GROUP_DATA_SIZE);
ulong discOff = _ngcwPartitions[p].DataOffset + grpIdx * Crypto.GROUP_SIZE;
var encG = new byte[Crypto.GROUP_SIZE];
for(var s = 0; s < sectorsToRead; s++)
{
ulong sec = discOff / Crypto.SECTOR_SIZE + (ulong)s;
ErrorNumber errno = _inputImage.ReadSector(sec, false, out byte[] sd, out _);
if(errno != ErrorNumber.NoError || sd == null)
{
fstOk = false;
break;
}
Array.Copy(sd, 0, encG, s * Crypto.SECTOR_SIZE, Crypto.SECTOR_SIZE);
}
if(!fstOk) break;
var hb = new byte[Crypto.GROUP_HASH_SIZE];
var gd = new byte[Crypto.GROUP_DATA_SIZE];
Crypto.DecryptGroup(_ngcwPartitions[p].TitleKey, encG, hb, gd);
int avail = Crypto.GROUP_DATA_SIZE - grpOff;
var chunk = (int)(fstSizeP - fstRead < (uint)avail ? fstSizeP - fstRead : (uint)avail);
Array.Copy(gd, grpOff, fstP, (int)fstRead, chunk);
fstRead += (uint)chunk;
}
if(fstOk) _ngcwPartDataMaps[p] = DataMap.BuildFromFst(fstP, 0, 2);
}
}
/// <summary>
/// Extracts game metadata from the disc header and sets MediaTitle,
/// MediaPartNumber, and MediaSequence on the output image.
/// </summary>
void EnrichNgcwMetadata()
{
if(_aborted) return;
ErrorNumber errno = _inputImage.ReadSector(0, false, out byte[] header, out _);
if(errno != ErrorNumber.NoError || header == null) return;
if(header.Length < 0x60) return;
// Game title: 64 bytes at offset 0x20, null/space-trimmed
var titleLen = 64;
if(header.Length < 0x20 + titleLen) titleLen = header.Length - 0x20;
while(titleLen > 0 && (header[0x20 + titleLen - 1] == 0 || header[0x20 + titleLen - 1] == ' ')) titleLen--;
if(titleLen > 0)
{
string title = Encoding.ASCII.GetString(header, 0x20, titleLen);
_outputImage.SetImageInfo(new CommonTypes.Structs.ImageInfo
{
MediaTitle = title
});
UpdateStatus?.Invoke(string.Format(UI.Ngcw_title_0, title));
}
// Game ID (6 bytes at offset 0) → MediaPartNumber
var codeLen = 6;
while(codeLen > 0 && (header[codeLen - 1] == 0 || header[codeLen - 1] == ' ')) codeLen--;
if(codeLen > 0)
{
string gameId = Encoding.ASCII.GetString(header, 0, codeLen);
_outputImage.SetImageInfo(new CommonTypes.Structs.ImageInfo
{
MediaPartNumber = gameId
});
UpdateStatus?.Invoke(string.Format(UI.Ngcw_game_id_0, gameId));
}
// Disc number: byte at offset 6
byte discNumber = header[6];
if(discNumber > 0)
{
_outputImage.SetImageInfo(new CommonTypes.Structs.ImageInfo
{
MediaSequence = discNumber + 1,
LastMediaSequence = discNumber + 1
});
UpdateStatus?.Invoke(string.Format(UI.Ngcw_disc_number_0, discNumber + 1));
}
}
/// <summary>Read multiple consecutive sectors and return as a single byte array.</summary>
byte[] ReadNgcwSectors(ulong startSector, int count)
{
var result = new byte[count * Crypto.SECTOR_SIZE];
for(var i = 0; i < count; i++)
{
ErrorNumber errno = _inputImage.ReadSector(startSector + (ulong)i, false, out byte[] data, out _);
if(errno != ErrorNumber.NoError || data == null) return null;
Array.Copy(data, 0, result, i * Crypto.SECTOR_SIZE, Crypto.SECTOR_SIZE);
}
return result;
}
/// <summary>Read arbitrary bytes from a disc image at a given byte offset.</summary>
byte[] ReadNgcwBytes(ulong byteOffset, int length)
{
var result = new byte[length];
var read = 0;
while(read < length)
{
ulong sector = (byteOffset + (ulong)read) / Crypto.SECTOR_SIZE;
var sectorOff = (int)((byteOffset + (ulong)read) % Crypto.SECTOR_SIZE);
int chunk = Crypto.SECTOR_SIZE - sectorOff;
if(chunk > length - read) chunk = length - read;
ErrorNumber errno = _inputImage.ReadSector(sector, false, out byte[] sectorData, out _);
if(errno != ErrorNumber.NoError || sectorData == null) return null;
Array.Copy(sectorData, sectorOff, result, read, chunk);
read += chunk;
}
return result;
}
/// GameCube sector conversion pipeline for long sectors.
ErrorNumber ConvertGameCubeSectorsLong(ulong discSize, ulong totalLogicalSectors, JunkCollector jc,
ref ulong dataSectors, ref ulong junkSectors)
{
const int blockSize = Crypto.GROUP_SIZE; // 0x8000 logical (16 × 2048 user bytes)
const int sectorsPerBlock = Crypto.LOGICAL_PER_GROUP;
const int userSize = Crypto.SECTOR_SIZE;
// Junk / FST offsets are in logical (user) byte space. User 2048-byte slices match ReadSector (Nintendo main_data at Sector.NintendoMainDataOffset).
ErrorNumber probeErr = _inputImage.ReadSectorLong(0, false, out byte[] longProbe, out _);
if(probeErr != ErrorNumber.NoError || longProbe == null ||
longProbe.Length < Sector.NintendoMainDataOffset + userSize)
return ErrorNumber.InOutError;
int longSectorSize = longProbe.Length;
// Read disc header to get FST info (logical user bytes via ReadSector)
byte[] header = ReadNgcwSectors(0, 2); // first 0x1000 bytes (need 0x42C)
if(header == null || header.Length < 0x42C) return ErrorNumber.InOutError;
// Read extended header for FST pointers (at 0x424)
byte[] extHeader = ReadNgcwSectors(0, (0x440 + userSize - 1) / userSize);
var fstOffset = BigEndianBitConverter.ToUInt32(extHeader, 0x424);
var fstSize = BigEndianBitConverter.ToUInt32(extHeader, 0x428);
ulong sysEnd = fstOffset + fstSize;
// Build FST data map
DataRegion[] dataMap = null;
if(fstSize > 0 && fstSize < 64 * 1024 * 1024)
{
byte[] fst = ReadNgcwBytes(fstOffset, (int)fstSize);
if(fst != null) dataMap = DataMap.BuildFromFst(fst, 0, 0);
}
// User-only buffer for LFG / data-region junk detection (same as ConvertGameCubeSectors)
var userBlockBuf = new byte[blockSize];
var longSectorBufs = new byte[sectorsPerBlock][];
var sectorStatuses = new SectorStatus[sectorsPerBlock];
for(ulong blockOff = 0; blockOff < discSize; blockOff += blockSize)
{
if(_aborted) break;
int blockBytes = blockSize;
if(blockOff + (ulong)blockBytes > discSize) blockBytes = (int)(discSize - blockOff);
ulong baseSector = blockOff / userSize;
UpdateProgress?.Invoke(string.Format(UI.Converting_sectors_0_to_1,
baseSector,
baseSector + sectorsPerBlock),
(long)baseSector,
(long)totalLogicalSectors);
// Read long sectors; pack user main data into userBlockBuf; keep full long buffers for output
for(var s = 0; s < sectorsPerBlock && s * userSize < blockBytes; s++)
{
ErrorNumber errno =
_inputImage.ReadSectorLong(baseSector + (ulong)s, false, out byte[] sectorData, out _);
byte[] stored = new byte[longSectorSize];
longSectorBufs[s] = stored;
if(errno != ErrorNumber.NoError || sectorData == null)
{
Array.Clear(userBlockBuf, s * userSize, userSize);
continue;
}
int copyLong = sectorData.Length < longSectorSize ? sectorData.Length : longSectorSize;
Array.Copy(sectorData, 0, stored, 0, copyLong);
if(sectorData.Length >= Sector.NintendoMainDataOffset + userSize)
Array.Copy(sectorData, Sector.NintendoMainDataOffset, userBlockBuf, s * userSize, userSize);
else
Array.Clear(userBlockBuf, s * userSize, userSize);
}
Junk.DetectJunkInBlock(userBlockBuf,
blockBytes,
blockOff,
dataMap,
sysEnd,
0xFFFF,
jc,
ref dataSectors,
ref junkSectors,
sectorStatuses);
int numSectors = blockBytes / userSize;
for(var si = 0; si < numSectors; si++)
{
ulong sector = baseSector + (ulong)si;
bool ok = _outputImage.WriteSectorLong(longSectorBufs[si], sector, false, sectorStatuses[si]);
if(!ok)
{
if(_force)
{
ErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_continuing,
_outputImage.ErrorMessage,
sector));
}
else
{
StoppingErrorMessage?.Invoke(string.Format(UI.Error_0_writing_sector_1_not_continuing,
_outputImage.ErrorMessage,
sector));
return ErrorNumber.WriteError;
}
}
}
}
return ErrorNumber.NoError;
}
/// Wii sector conversion pipeline for long sectors.
ErrorNumber ConvertWiiSectorsLong(ulong discSize, ulong totalLogicalSectors, JunkCollector jc, ref ulong dataSectors, ref ulong junkSectors){
// TODO: Implement
return ErrorNumber.NoError;
}
}