diff --git a/Aaru.Checksums/CDChecksums.cs b/Aaru.Checksums/CDChecksums.cs index 6b63e6abc..743f2c442 100644 --- a/Aaru.Checksums/CDChecksums.cs +++ b/Aaru.Checksums/CDChecksums.cs @@ -38,7 +38,7 @@ using Aaru.Logging; namespace Aaru.Checksums; -/// Result of a operation. +/// Result of a operation. public enum SectorFixResult { /// Input is invalid or the sector type has no repairable ECC. @@ -54,12 +54,14 @@ public enum SectorFixResult /// Implements ReedSolomon and CRC32 algorithms as used by CD-ROM public static class CdChecksums { - const string MODULE_NAME = "CD checksums"; - static byte[] _eccFTable; - static byte[] _eccBTable; - static uint[] _edcTable; - static Dictionary _eccPSyndromeTable; - static Dictionary _eccQSyndromeTable; + const string MODULE_NAME = "CD checksums"; + static byte[] _eccFTable; + static byte[] _eccBTable; + static uint[] _edcTable; + static byte[] _gfExp; // α^k for k=0..254, length 256 (_gfExp[255] wraps to _gfExp[0]) + static byte[] _gfLog; // log_α(v) for v=1..255; _gfLog[0] unused + static byte[] _eccPWeights; // H_P column weights: α^(25−pos) for pos=0..25 + static byte[] _eccQWeights; // H_Q column weights: α^(44−pos) for pos=0..44 /// Checks the EDC and ECC of a CD sector /// CD sector @@ -129,12 +131,7 @@ public static class CdChecksums static void EccInit() { - if(_eccFTable != null && - _eccBTable != null && - _edcTable != null && - _eccPSyndromeTable != null && - _eccQSyndromeTable != null) - return; + if(_eccFTable != null && _eccBTable != null && _edcTable != null && _gfExp != null) return; _eccFTable = new byte[256]; _eccBTable = new byte[256]; @@ -152,42 +149,27 @@ public static class CdChecksums _edcTable[i] = edc; } - _eccPSyndromeTable = BuildSyndromeTable(24); - _eccQSyndromeTable = BuildSyndromeTable(43); - } + // GF(2^8) log/exp tables using primitive element α=2 and polynomial x^8+x^4+x^3+x^2+1 + // as specified in ECMA-130 Annex A section A.3. + _gfExp = new byte[256]; + _gfLog = new byte[256]; + _gfExp[0] = 1; - static Dictionary BuildSyndromeTable(int minorCount) - { - Dictionary table = new(); - var row = new byte[minorCount]; + for(var i = 1; i < 255; i++) _gfExp[i] = _eccFTable[_gfExp[i - 1]]; - for(var position = 0; position < minorCount + 2; position++) - { - for(var error = 1; error < 256; error++) - { - Array.Clear(row, 0, row.Length); + _gfExp[255] = _gfExp[0]; - byte storedA = 0; - byte storedB = 0; + for(var i = 0; i < 255; i++) _gfLog[_gfExp[i]] = (byte)i; - if(position < minorCount) - row[position] = (byte)error; - else if(position == minorCount) - storedA = (byte)error; - else - storedB = (byte)error; + // H_P column weights: weights[pos] = α^(25−pos) for pos=0..25 (ECMA-130 Annex A, H_P matrix). + _eccPWeights = new byte[26]; - ComputeEcc(row, out byte calcA, out byte calcB); + for(var pos = 0; pos < 26; pos++) _eccPWeights[pos] = _gfExp[(255 + 25 - pos) % 255]; - var syndrome = (uint)((calcA ^ storedA) << 8 | calcB ^ storedB); + // H_Q column weights: weights[pos] = α^(44−pos) for pos=0..44 (ECMA-130 Annex A, H_Q matrix). + _eccQWeights = new byte[45]; - if(syndrome == 0 || table.ContainsKey(syndrome)) continue; - - table.Add(syndrome, new EccSyndromeMatch(position, (byte)error)); - } - } - - return table; + for(var pos = 0; pos < 45; pos++) _eccQWeights[pos] = _gfExp[(255 + 44 - pos) % 255]; } static void ComputeEcc(IReadOnlyList data, out byte eccA, out byte eccB) @@ -491,7 +473,59 @@ public static class CdChecksums /// if it had uncorrectable errors, and /// if the input is invalid or the sector type has no repairable ECC. /// - public static SectorFixResult FixSector(byte[] buffer) + public static SectorFixResult FixSector(byte[] buffer) => FixSector(buffer, (bool[])null); + + /// + /// Attempts to repair a raw 2352-byte data sector using stored ECC-P and ECC-Q values and MMC C2 pointers. + /// + /// Raw 2352-byte sector. + /// 294-byte C2 pointer map, or 296-byte C2 pointer map plus block error bytes. + /// + /// if the sector was already correct, + /// if it had errors that were successfully corrected, + /// if it had uncorrectable errors, and + /// if the input is invalid or the sector type has no repairable ECC. + /// + public static SectorFixResult FixSector(byte[] buffer, byte[] c2Pointers) + { + if(buffer is not { Length: 2352 }) return SectorFixResult.NotApplicable; + + if(c2Pointers == null) return FixSector(buffer, (bool[])null); + + if(c2Pointers.Length != 294 && c2Pointers.Length != 296) return SectorFixResult.NotApplicable; + + var original = new byte[buffer.Length]; + Array.Copy(buffer, original, buffer.Length); + + for(var bitOrder = 0; bitOrder < 2; bitOrder++) + { + Array.Copy(original, buffer, buffer.Length); + + bool[] erasureMap = CreateErasureMapFromC2Pointers(c2Pointers, bitOrder == 0); + SectorFixResult result = FixSector(buffer, erasureMap); + + if(result == SectorFixResult.CouldNotFix) continue; + + return result == SectorFixResult.Correct && SectorChanged(original, buffer) + ? SectorFixResult.Fixed + : result; + } + + Array.Copy(original, buffer, buffer.Length); + + return SectorFixResult.CouldNotFix; + } + + static bool SectorChanged(byte[] original, byte[] current) + { + for(var i = 0; i < original.Length; i++) + if(original[i] != current[i]) + return true; + + return false; + } + + static SectorFixResult FixSector(byte[] buffer, bool[] erasureMap) { if(buffer is not { Length: 2352 }) return SectorFixResult.NotApplicable; @@ -511,16 +545,38 @@ public static class CdChecksums buffer[0x00B] != 0x00) return SectorFixResult.NotApplicable; - return (buffer[0x00F] & 0x03) switch - { - 0x01 => FixMode1Sector(buffer), - 0x02 when (buffer[0x012] & 0x20) == 0x20 => SectorFixResult.NotApplicable, - 0x02 => FixMode2Form1Sector(buffer), - _ => SectorFixResult.NotApplicable - }; + var original = new byte[buffer.Length]; + Array.Copy(buffer, original, buffer.Length); + + SectorFixResult result = (buffer[0x00F] & 0x03) switch + { + 0x01 => FixMode1Sector(buffer, erasureMap), + 0x02 when (buffer[0x012] & 0x20) == 0x20 => SectorFixResult.NotApplicable, + 0x02 => FixMode2Form1Sector(buffer, erasureMap), + _ => SectorFixResult.NotApplicable + }; + + return result; } - static SectorFixResult FixMode1Sector(byte[] sector) + static bool[] CreateErasureMapFromC2Pointers(byte[] c2Pointers, bool msbFirst) + { + var erasureMap = new bool[2352]; + + for(var i = 0; i < 294; i++) + { + for(var bit = 0; bit < 8; bit++) + { + int mask = msbFirst ? 0x80 >> bit : 1 << bit; + + if((c2Pointers[i] & mask) != 0) erasureMap[i * 8 + bit] = true; + } + } + + return erasureMap; + } + + static SectorFixResult FixMode1Sector(byte[] sector, bool[] erasureMap) { bool? status = CheckCdSectorChannel(sector, out bool? correctEccP, out bool? correctEccQ, out bool? _); @@ -540,10 +596,10 @@ public static class CdChecksums int[] pMap = CreateOffsetMap(2064, 0x0C, 0x10, false); int[] qMap = CreateOffsetMap(2236, 0x0C, 0x10, false); - return FixSectorWithEcc(sector, pMap, qMap, 0x81C, 0x8C8, 0, 0x810, 0x810); + return FixSectorWithEcc(sector, pMap, qMap, 0x81C, 0x8C8, 0, 0x810, 0x810, erasureMap); } - static SectorFixResult FixMode2Form1Sector(byte[] sector) + static SectorFixResult FixMode2Form1Sector(byte[] sector, bool[] erasureMap) { bool? status = CheckCdSectorChannel(sector, out bool? correctEccP, out bool? correctEccQ, out bool? _); @@ -561,16 +617,154 @@ public static class CdChecksums int[] pMap = CreateOffsetMap(2064, 0, 0x10, true); int[] qMap = CreateOffsetMap(2236, 0, 0x10, true); - return FixSectorWithEcc(sector, pMap, qMap, 0x81C, 0x8C8, 0x10, 0x808, 0x818); + return FixSectorWithEcc(sector, pMap, qMap, 0x81C, 0x8C8, 0x10, 0x808, 0x818, erasureMap); } static SectorFixResult FixSectorWithEcc(byte[] sector, int[] pMap, int[] qMap, int eccPOffset, int eccQOffset, - int edcSourceOffset, int edcSize, int edcOffset) + int edcSourceOffset, int edcSize, int edcOffset, bool[] erasureMap) { - for(var pass = 0; pass < 16; pass++) + int[][] pRows = BuildRowOffsets(pMap, 86, 24, 2, 86); + int[][] qRows = BuildRowOffsets(qMap, 52, 43, 86, 88); + + // Reverse maps: sector byte offset → (row index, position in row) for each code. + // Used by TryFixEccValidated to require cross-code locator agreement before applying a correction. + var pByteToRow = new int[0x930]; + var pByteToPos = new int[0x930]; + var qByteToRow = new int[0x930]; + var qByteToPos = new int[0x930]; + + for(var i = 0; i < 0x930; i++) { - bool corrected = TryFixEcc(sector, pMap, eccPOffset, 86, 24, 2, 86, _eccPSyndromeTable); - corrected = TryFixEcc(sector, qMap, eccQOffset, 52, 43, 86, 88, _eccQSyndromeTable) || corrected; + pByteToRow[i] = -1; + qByteToRow[i] = -1; + } + + for(var maj = 0; maj < 86; maj++) + { + for(var min = 0; min < 24; min++) + if(pRows[maj][min] >= 0) + { + pByteToRow[pRows[maj][min]] = maj; + pByteToPos[pRows[maj][min]] = min; + } + } + + for(var maj = 0; maj < 52; maj++) + { + for(var min = 0; min < 43; min++) + if(qRows[maj][min] >= 0) + { + qByteToRow[qRows[maj][min]] = maj; + qByteToPos[qRows[maj][min]] = min; + } + } + + // Also map parity byte offsets into the reverse maps. + for(var maj = 0; maj < 86; maj++) + { + int pb1 = eccPOffset + maj, pb2 = eccPOffset + 86 + maj; + + if(pb1 < 0x930) + { + pByteToRow[pb1] = maj; + pByteToPos[pb1] = 24; + } + + if(pb2 < 0x930) + { + pByteToRow[pb2] = maj; + pByteToPos[pb2] = 25; + } + + if(qByteToRow[pb1] < 0) + { + /* P-parity byte not in Q-row data range — no entry */ + } + } + + for(var maj = 0; maj < 52; maj++) + { + int qb1 = eccQOffset + maj, qb2 = eccQOffset + 52 + maj; + + if(qb1 < 0x930) + { + qByteToRow[qb1] = maj; + qByteToPos[qb1] = 43; + } + + if(qb2 < 0x930) + { + qByteToRow[qb2] = maj; + qByteToPos[qb2] = 44; + } + } + + if(erasureMap != null) + { + SectorFixResult erasureResult = + FixSectorWithErasures(sector, pRows, qRows, eccPOffset, eccQOffset, erasureMap); + + if(erasureResult != SectorFixResult.CouldNotFix) return erasureResult; + } + + for(var pass = 0; pass < 64; pass++) + { + uint[] pSyndromes = ComputeRowSyndromes(sector, pRows, eccPOffset, 86, 24, _eccPWeights); + uint[] qSyndromes = ComputeRowSyndromes(sector, qRows, eccQOffset, 52, 43, _eccQWeights); + int failedRows = CountFailedRows(pSyndromes) + CountFailedRows(qSyndromes); + var previous = new byte[sector.Length]; + Array.Copy(sector, previous, sector.Length); + + // Fix P-rows (skip if Q's 1-error locator points elsewhere), then undo newly-broken Q-rows. + bool corrected = TryFixEccValidated(sector, + pRows, + eccPOffset, + 86, + 24, + _eccPWeights, + qRows, + eccQOffset, + 52, + 43, + _eccQWeights, + qByteToRow, + qByteToPos); + + RevertNewlyFailingRows(sector, qRows, eccQOffset, 52, 43, _eccQWeights, qSyndromes); + + // Recompute P syndromes after P-pass + Q-reverts — these reflect which P-rows are now correct. + uint[] pSyndromesAfterP = ComputeRowSyndromes(sector, pRows, eccPOffset, 86, 24, _eccPWeights); + + // Fix Q-rows (skip if P's 1-error locator points elsewhere), then undo newly-broken P-rows. + corrected = TryFixEccValidated(sector, + qRows, + eccQOffset, + 52, + 43, + _eccQWeights, + pRows, + eccPOffset, + 86, + 24, + _eccPWeights, + pByteToRow, + pByteToPos) || + corrected; + + RevertNewlyFailingRows(sector, pRows, eccPOffset, 86, 24, _eccPWeights, pSyndromesAfterP); + + if(corrected) + { + uint[] correctedPSyndromes = ComputeRowSyndromes(sector, pRows, eccPOffset, 86, 24, _eccPWeights); + uint[] correctedQSyndromes = ComputeRowSyndromes(sector, qRows, eccQOffset, 52, 43, _eccQWeights); + int correctedFailedRows = CountFailedRows(correctedPSyndromes) + CountFailedRows(correctedQSyndromes); + + if(correctedFailedRows >= failedRows) + { + Array.Copy(previous, sector, sector.Length); + corrected = false; + } + } bool? status = CheckCdSectorChannel(sector, out bool? correctEccP, out bool? correctEccQ, out bool? correctEdc); @@ -587,52 +781,626 @@ public static class CdChecksums if(!corrected || status == null) break; } + // Single-error correction stalled — try 2-error correction with cross-code validation. + for(var brutePass = 0; brutePass < 64; brutePass++) + { + bool bruteFixed = TryFix2ErrorRows(sector, + pRows, + 86, + 24, + qRows, + 52, + 43, + eccPOffset, + eccQOffset, + _eccPWeights, + _eccQWeights); + + bruteFixed |= TryFix2ErrorRows(sector, + qRows, + 52, + 43, + pRows, + 86, + 24, + eccQOffset, + eccPOffset, + _eccQWeights, + _eccPWeights); + + if(!bruteFixed) break; + + // Re-run single-error correction after each 2-error pass, with the same + // convergence guard as the outer loop so that wrong corrections (fake locators + // from still-multi-error rows) are reverted and do not accumulate. + for(var synPass = 0; synPass < 64; synPass++) + { + uint[] pSyn2Before = ComputeRowSyndromes(sector, pRows, eccPOffset, 86, 24, _eccPWeights); + uint[] qSyn2Before = ComputeRowSyndromes(sector, qRows, eccQOffset, 52, 43, _eccQWeights); + int failed2Before = CountFailedRows(pSyn2Before) + CountFailedRows(qSyn2Before); + var previous2 = new byte[sector.Length]; + Array.Copy(sector, previous2, sector.Length); + + bool corrected2 = TryFixEcc(sector, pRows, eccPOffset, 86, 24, _eccPWeights); + RevertNewlyFailingRows(sector, qRows, eccQOffset, 52, 43, _eccQWeights, qSyn2Before); + uint[] pSyn2AfterP = ComputeRowSyndromes(sector, pRows, eccPOffset, 86, 24, _eccPWeights); + corrected2 = TryFixEcc(sector, qRows, eccQOffset, 52, 43, _eccQWeights) || corrected2; + RevertNewlyFailingRows(sector, pRows, eccPOffset, 86, 24, _eccPWeights, pSyn2AfterP); + + if(corrected2) + { + uint[] pSyn2After = ComputeRowSyndromes(sector, pRows, eccPOffset, 86, 24, _eccPWeights); + uint[] qSyn2After = ComputeRowSyndromes(sector, qRows, eccQOffset, 52, 43, _eccQWeights); + int failed2After = CountFailedRows(pSyn2After) + CountFailedRows(qSyn2After); + + if(failed2After >= failed2Before) + { + Array.Copy(previous2, sector, sector.Length); + + break; + } + } + + CheckCdSectorChannel(sector, out bool? correctEccP2, out bool? correctEccQ2, out bool? correctEdc2); + + if(correctEccP2 == true && correctEccQ2 == true) + { + if(correctEdc2 != true) UpdateEdc(sector, edcSourceOffset, edcSize, edcOffset); + + return CheckCdSectorChannel(sector, out _, out _, out _) == true + ? SectorFixResult.Fixed + : SectorFixResult.CouldNotFix; + } + + if(!corrected2) break; + } + } + return SectorFixResult.CouldNotFix; } - static bool TryFixEcc(byte[] sector, int[] offsetMap, int eccOffset, int majorCount, int minorCount, int majorMult, - int minorInc, Dictionary syndromeTable) + /// + /// 2-error correction on stuck code rows using cross-code syndrome validation. + /// For each row with a non-zero ECMA syndrome (S₀,S₁), enumerates all C(n,2) position pairs (p1,p2) + /// and solves for (e1,e2) in O(1) via the ECMA-130 Annex A H matrix formula: + /// e1 = (S₁ ⊕ w2·S₀)/(w1 ⊕ w2), e2 = S₀ ⊕ e1 where w_i = fixWeights[p_i]. + /// All pairs are tried; the pair that minimises the cross-code failure count is accepted, + /// provided it does not increase that count (Q must not get worse when fixing a P-row and + /// vice-versa). Accepting the globally best pair prevents false-positive corrections that + /// arise when a correct fix reduces 2-error cross-code rows to 1-error (still failing, so + /// a strict-decrease test would wrongly reject it). + /// + static bool TryFix2ErrorRows(byte[] sector, int[][] fixRows, int fixMajorCount, int fixMinorCount, + int[][] checkRows, int checkMajorCount, int checkMinorCount, int fixEccOffset, + int checkEccOffset, byte[] fixWeights, byte[] checkWeights) { - var corrected = false; - int size = majorCount * minorCount; + int fixPositionCount = fixMinorCount + 2; + var anyFixed = false; + + for(var major = 0; major < fixMajorCount; major++) + { + (byte s0, byte s1) = ComputeEcmaRowSyndrome(sector, + fixRows[major], + fixEccOffset, + major, + fixMajorCount, + fixMinorCount, + fixWeights); + + if(s0 == 0 && s1 == 0) continue; + + int checkFailedBefore = + CountFailedRows(ComputeRowSyndromes(sector, + checkRows, + checkEccOffset, + checkMajorCount, + checkMinorCount, + checkWeights)); + + // Find the pair (p1, p2) that minimises cross-code failures after correction. + int bestCheckFailed = checkFailedBefore + 1; // sentinel: must be ≤ before to accept + int bestP1 = -1; + int bestP2 = -1; + byte bestE1 = 0; + byte bestE2 = 0; + + for(var p1 = 0; p1 < fixPositionCount; p1++) + { + for(int p2 = p1 + 1; p2 < fixPositionCount; p2++) + { + byte w1 = fixWeights[p1]; + byte w2 = fixWeights[p2]; + var denom = (byte)(w1 ^ w2); + + if(denom == 0) continue; + + byte e1 = GfDiv((byte)(s1 ^ GfMul(w2, s0)), denom); + var e2 = (byte)(s0 ^ e1); + + if(e1 == 0 && e2 == 0) continue; + + ApplyEccRowError(sector, fixRows[major], fixEccOffset, major, fixMajorCount, fixMinorCount, p1, e1); + ApplyEccRowError(sector, fixRows[major], fixEccOffset, major, fixMajorCount, fixMinorCount, p2, e2); + + int checkFailedAfter = + CountFailedRows(ComputeRowSyndromes(sector, + checkRows, + checkEccOffset, + checkMajorCount, + checkMinorCount, + checkWeights)); + + ApplyEccRowError(sector, fixRows[major], fixEccOffset, major, fixMajorCount, fixMinorCount, p1, e1); + ApplyEccRowError(sector, fixRows[major], fixEccOffset, major, fixMajorCount, fixMinorCount, p2, e2); + + if(checkFailedAfter < bestCheckFailed) + { + bestCheckFailed = checkFailedAfter; + bestP1 = p1; + bestP2 = p2; + bestE1 = e1; + bestE2 = e2; + } + } + } + + // Accept only if the best candidate strictly decreases cross-code failures. + if(bestP1 >= 0 && bestCheckFailed < checkFailedBefore) + { + ApplyEccRowError(sector, + fixRows[major], + fixEccOffset, + major, + fixMajorCount, + fixMinorCount, + bestP1, + bestE1); + + ApplyEccRowError(sector, + fixRows[major], + fixEccOffset, + major, + fixMajorCount, + fixMinorCount, + bestP2, + bestE2); + + anyFixed = true; + } + } + + return anyFixed; + } + + static int[][] BuildRowOffsets(int[] offsetMap, int majorCount, int minorCount, int majorMult, int minorInc) + { + var rows = new int[majorCount][]; + int size = majorCount * minorCount; for(var major = 0; major < majorCount; major++) { - int index = (major >> 1) * majorMult + (major & 1); - var row = new byte[minorCount]; - var rowOffsets = new int[minorCount]; + int index = (major >> 1) * majorMult + (major & 1); + rows[major] = new int[minorCount]; for(var minor = 0; minor < minorCount; minor++) { - int offset = offsetMap[index]; - rowOffsets[minor] = offset; - row[minor] = offset >= 0 ? sector[offset] : (byte)0; - index += minorInc; + rows[major][minor] = offsetMap[index]; + index += minorInc; if(index >= size) index -= size; } + } - ComputeEcc(row, out byte calcA, out byte calcB); + return rows; + } - byte storedA = sector[eccOffset + major]; - byte storedB = sector[eccOffset + majorCount + major]; - var syndrome = (uint)((calcA ^ storedA) << 8 | calcB ^ storedB); + static SectorFixResult FixSectorWithErasures(byte[] sector, int[][] pRows, int[][] qRows, int eccPOffset, + int eccQOffset, bool[] erasureMap) + { + for(var pass = 0; pass < 64; pass++) + { + bool corrected = TryFixEccErasures(sector, pRows, eccPOffset, 86, 24, erasureMap, _eccPWeights); - if(syndrome == 0 || !syndromeTable.TryGetValue(syndrome, out EccSyndromeMatch match)) continue; + corrected = TryFixEccErasures(sector, qRows, eccQOffset, 52, 43, erasureMap, _eccQWeights) || corrected; - if(match.Position < minorCount) + bool? status = + CheckCdSectorChannel(sector, out bool? correctEccP, out bool? correctEccQ, out bool? correctEdc); + + if(correctEccP == true && correctEccQ == true) { - int offset = rowOffsets[match.Position]; + if(correctEdc == true) return SectorFixResult.Fixed; - if(offset < 0) continue; - - sector[offset] ^= match.Error; + return SectorFixResult.CouldNotFix; } - else if(match.Position == minorCount) - sector[eccOffset + major] ^= match.Error; - else - sector[eccOffset + majorCount + major] ^= match.Error; + if(!corrected || status == null) break; + } + + return SectorFixResult.CouldNotFix; + } + + static bool TryFixEccErasures(byte[] sector, int[][] rows, int eccOffset, int majorCount, int minorCount, + bool[] erasureMap, byte[] weights) + { + var corrected = false; + + for(var major = 0; major < majorCount; major++) + { + List positions = GetKnownErasurePositions(rows[major], + eccOffset, + major, + majorCount, + minorCount, + erasureMap); + + if(positions.Count == 0 || positions.Count > 2) continue; + + (byte s0, byte s1) = + ComputeEcmaRowSyndrome(sector, rows[major], eccOffset, major, majorCount, minorCount, weights); + + if(s0 == 0 && s1 == 0) + { + ClearKnownErasurePositions(rows[major], + eccOffset, + major, + majorCount, + minorCount, + positions, + erasureMap); + + continue; + } + + if(positions.Count == 1) + { + if(!TrySolveOneErasureSyndrome(s0, s1, positions[0], weights, out byte error)) continue; + + ApplyEccRowError(sector, rows[major], eccOffset, major, majorCount, minorCount, positions[0], error); + + ClearKnownErasurePosition(rows[major], + eccOffset, + major, + majorCount, + minorCount, + positions[0], + erasureMap); + + corrected = true; + + continue; + } + + if(!TrySolveTwoErasureSyndrome(s0, + s1, + positions[0], + positions[1], + weights, + out byte error1, + out byte error2)) + continue; + + ApplyEccRowError(sector, rows[major], eccOffset, major, majorCount, minorCount, positions[0], error1); + ApplyEccRowError(sector, rows[major], eccOffset, major, majorCount, minorCount, positions[1], error2); + ClearKnownErasurePositions(rows[major], eccOffset, major, majorCount, minorCount, positions, erasureMap); + + corrected = true; + } + + return corrected; + } + + // GF(2^8) multiply using log/exp tables; returns 0 if either operand is 0. + static byte GfMul(byte a, byte b) => a == 0 || b == 0 ? (byte)0 : _gfExp[(_gfLog[a] + _gfLog[b]) % 255]; + + // GF(2^8) divide a/b; returns 0 if a==0. b must be non-zero. + static byte GfDiv(byte a, byte b) => a == 0 ? (byte)0 : _gfExp[(255 + _gfLog[a] - _gfLog[b]) % 255]; + + /// + /// Computes the two ECMA-130 Annex A syndromes (S₀, S₁) for one ECC row. + /// S₀ = XOR of all codeword bytes (H row 1 = all-ones). + /// S₁ = Σ weights[pos]·byte[pos] (H row 2 = α-weighted sum). + /// Both are zero for a correct codeword. A single error e at position pos satisfies + /// S₀=e and S₁=weights[pos]·e, giving pos=(n−1)−log_α(S₁/S₀). + /// + static (byte s0, byte s1) ComputeEcmaRowSyndrome(byte[] sector, int[] row, int eccOffset, int major, int majorCount, + int minorCount, byte[] weights) + { + byte s0 = 0, s1 = 0; + + for(var pos = 0; pos < minorCount; pos++) + { + int offset = row[pos]; + + if(offset < 0) continue; + byte v = sector[offset]; + s0 ^= v; + s1 ^= GfMul(weights[pos], v); + } + + byte pa = sector[eccOffset + major]; + s0 ^= pa; + s1 ^= GfMul(weights[minorCount], pa); // α^1 weight for first parity byte + + byte pb = sector[eccOffset + majorCount + major]; + s0 ^= pb; + s1 ^= pb; // α^0 = 1 weight for second parity byte + + return (s0, s1); + } + + static uint ComputeRowSyndrome(byte[] sector, int[] row, int eccOffset, int major, int majorCount, int minorCount, + byte[] weights) + { + (byte s0, byte s1) = ComputeEcmaRowSyndrome(sector, row, eccOffset, major, majorCount, minorCount, weights); + + return (uint)(s0 << 8 | s1); + } + + static List GetKnownErasurePositions(int[] row, int eccOffset, int major, int majorCount, int minorCount, + bool[] erasureMap) + { + List positions = []; + + for(var position = 0; position < minorCount + 2; position++) + { + int offset = GetEccRowPositionOffset(row, eccOffset, major, majorCount, minorCount, position); + + if(offset >= 0 && erasureMap[offset]) positions.Add(position); + } + + return positions; + } + + static void ClearKnownErasurePositions(int[] row, int eccOffset, int major, int majorCount, int minorCount, + List positions, bool[] erasureMap) + { + for(var i = 0; i < positions.Count; i++) + ClearKnownErasurePosition(row, eccOffset, major, majorCount, minorCount, positions[i], erasureMap); + } + + static void ClearKnownErasurePosition(int[] row, int eccOffset, int major, int majorCount, int minorCount, + int position, bool[] erasureMap) + { + int offset = GetEccRowPositionOffset(row, eccOffset, major, majorCount, minorCount, position); + + if(offset >= 0) erasureMap[offset] = false; + } + + static uint[] ComputeRowSyndromes(byte[] sector, int[][] rows, int eccOffset, int majorCount, int minorCount, + byte[] weights) + { + var syndromes = new uint[majorCount]; + + for(var major = 0; major < majorCount; major++) + syndromes[major] = + ComputeRowSyndrome(sector, rows[major], eccOffset, major, majorCount, minorCount, weights); + + return syndromes; + } + + static int CountFailedRows(uint[] syndromes) + { + var count = 0; + + for(var i = 0; i < syndromes.Length; i++) + { + if(syndromes[i] != 0) count++; + } + + return count; + } + + static int GetEccRowPositionOffset(int[] rowOffsets, int eccOffset, int major, int majorCount, int minorCount, + int position) + { + if(position < minorCount) return rowOffsets[position]; + + return position == minorCount ? eccOffset + major : eccOffset + majorCount + major; + } + + /// + /// Solve for the single erasure error value from ECMA syndromes. + /// From H·V=0 with one error e at position pos: S₀=e, S₁=weights[pos]·e. + /// Verify consistency and return error = S₀. + /// + static bool TrySolveOneErasureSyndrome(byte s0, byte s1, int position, byte[] weights, out byte error) + { + error = 0; + + if(s0 == 0) return false; + + // Verify syndrome is consistent with a single error at this position. + if(GfMul(weights[position], s0) != s1) return false; + + error = s0; + + return true; + } + + /// + /// Solve for two erasure error values from ECMA syndromes using the direct GF formula. + /// From H·V=0 with errors (e1,e2) at known positions (p1,p2): + /// e1 = (S₁ ⊕ w2·S₀) / (w1 ⊕ w2), e2 = S₀ ⊕ e1 where w_i = weights[p_i]. + /// + static bool TrySolveTwoErasureSyndrome(byte s0, byte s1, int position1, int position2, byte[] weights, + out byte error1, out byte error2) + { + error1 = 0; + error2 = 0; + + byte w1 = weights[position1]; + byte w2 = weights[position2]; + var denom = (byte)(w1 ^ w2); + + if(denom == 0) return false; + + error1 = GfDiv((byte)(s1 ^ GfMul(w2, s0)), denom); + error2 = (byte)(s0 ^ error1); + + return true; + } + + static void ApplyEccRowError(byte[] sector, int[] rowOffsets, int eccOffset, int major, int majorCount, + int minorCount, int position, byte error) + { + int offset = GetEccRowPositionOffset(rowOffsets, eccOffset, major, majorCount, minorCount, position); + + if(offset >= 0) sector[offset] ^= error; + } + + /// + /// For each row in that had a zero syndrome in + /// (was correct) but now has a non-zero syndrome (broke), revertes the single error that was introduced. + /// This catches wrong single-error corrections from that applied a fake locator + /// (derived from a multi-error row) at a byte that was previously clean, making the cross-code row fail. + /// The syndrome of a newly-failing row that was clean before is guaranteed to be a 1-error syndrome because + /// exactly one byte changed in that row (the fake correction), so the locator unambiguously identifies and + /// reverts it. + /// + static void RevertNewlyFailingRows(byte[] sector, int[][] rows, int eccOffset, int majorCount, int minorCount, + byte[] weights, uint[] syndromesBefore) + { + int n = minorCount + 2; + + for(var major = 0; major < majorCount; major++) + { + // Only care about rows that were passing (syndrome == 0) before the last fix pass. + if(syndromesBefore[major] != 0) continue; + + (byte s0, byte s1) = + ComputeEcmaRowSyndrome(sector, rows[major], eccOffset, major, majorCount, minorCount, weights); + + if(s0 == 0) continue; // still passing — no wrong correction here + + // Row went from correct to failing: a wrong correction introduced exactly 1 error. + // Use the ECMA single-error locator to find and revert it. + byte locator = GfDiv(s1, s0); + + if(locator == 0) continue; + int pos = n - 1 - _gfLog[locator]; + + if(pos < 0 || pos >= n) continue; + + ApplyEccRowError(sector, rows[major], eccOffset, major, majorCount, minorCount, pos, s0); + } + } + + /// + /// Single-error correction with cross-code locator agreement validation. + /// For each candidate correction: the fix-code locator gives position b and value e. + /// The correction is accepted only if the check-code row containing b also has its + /// 1-error locator pointing to b (i.e., both codes independently identify the same byte as + /// the single error). This eliminates corrections at previously-correct bytes that happen to give + /// a valid-looking fix-code locator from a multi-error row. + /// When the check-code row has 2+ errors the locator does not point to b, so the correction + /// is skipped; subsequent passes (after the check-code errors decrease) will accept it. + /// + static bool TryFixEccValidated(byte[] sector, int[][] rows, int eccOffset, int majorCount, int minorCount, + byte[] weights, int[][] checkRows, int checkEccOffset, int checkMajorCount, + int checkMinorCount, byte[] checkWeights, int[] byteToCheckRow, int[] byteToCheckPos) + { + var corrected = false; + int n = minorCount + 2; + int nCheck = checkMinorCount + 2; + + for(var major = 0; major < majorCount; major++) + { + (byte s0, byte s1) = + ComputeEcmaRowSyndrome(sector, rows[major], eccOffset, major, majorCount, minorCount, weights); + + if(s0 == 0) continue; + + byte locator = GfDiv(s1, s0); + + if(locator == 0) continue; + + int pos = n - 1 - _gfLog[locator]; + + if(pos < 0 || pos >= n) continue; + + // Get the byte offset this correction targets. + int byteOffset = GetEccRowPositionOffset(rows[major], eccOffset, major, majorCount, minorCount, pos); + + if(byteOffset < 0 || byteOffset >= byteToCheckRow.Length) + goto apply; // parity byte or unmapped: apply directly + + int checkRowIdx = byteToCheckRow[byteOffset]; + + if(checkRowIdx < 0) goto apply; // byte not covered by check code: apply directly + + int checkPos = byteToCheckPos[byteOffset]; + + // Require the check-code row's 1-error locator to point to the same byte. + (byte cs0, byte cs1) = ComputeEcmaRowSyndrome(sector, + checkRows[checkRowIdx], + checkEccOffset, + checkRowIdx, + checkMajorCount, + checkMinorCount, + checkWeights); + + if(cs0 == 0) goto skip; // check-code row is correct — fix-code "correction" would break it + + byte checkLocator = GfDiv(cs1, cs0); + + if(checkLocator == 0) goto skip; + + int checkLocPos = nCheck - 1 - _gfLog[checkLocator]; + + // Only skip if the check-code has a VALID single-error locator that points to a DIFFERENT byte. + // This catches the case: Q-row has exactly 1 error somewhere else, P-correction would introduce + // a second error at the candidate byte. If the locator is out-of-range (multi-error Q-row with + // no valid single-error locator), we allow the P-correction — we just can't confirm it. + if(checkLocPos >= 0 && checkLocPos < nCheck) + { + int checkByteOffset = GetEccRowPositionOffset(checkRows[checkRowIdx], + checkEccOffset, + checkRowIdx, + checkMajorCount, + checkMinorCount, + checkLocPos); + + if(checkByteOffset >= 0 && checkByteOffset != byteOffset) goto skip; + } + + apply: + ApplyEccRowError(sector, rows[major], eccOffset, major, majorCount, minorCount, pos, s0); + corrected = true; + + continue; + + skip: ; + } + + return corrected; + } + + /// + /// Single-error correction using the ECMA-130 Annex A parity check matrices. + /// For each row with syndrome (S₀,S₁): error value = S₀, + /// error position = (n−1) − log_α(S₁/S₀) where n = minorCount+2. + /// + static bool TryFixEcc(byte[] sector, int[][] rows, int eccOffset, int majorCount, int minorCount, byte[] weights) + { + var corrected = false; + int n = minorCount + 2; + + for(var major = 0; major < majorCount; major++) + { + (byte s0, byte s1) = + ComputeEcmaRowSyndrome(sector, rows[major], eccOffset, major, majorCount, minorCount, weights); + + if(s0 == 0) continue; + + byte locator = GfDiv(s1, s0); + + if(locator == 0) continue; // S₁=0 but S₀≠0: even-count error pattern, unlocatable + + int pos = n - 1 - _gfLog[locator]; + + if(pos < 0 || pos >= n) continue; + + ApplyEccRowError(sector, rows[major], eccOffset, major, majorCount, minorCount, pos, s0); corrected = true; } @@ -918,10 +1686,4 @@ public static class CdChecksums return false; } - - readonly struct EccSyndromeMatch(int position, byte error) - { - public readonly int Position = position; - public readonly byte Error = error; - } } \ No newline at end of file diff --git a/Aaru.Tests/Checksums/CdChecksumsFix.cs b/Aaru.Tests/Checksums/CdChecksumsFix.cs index afbf5ae7a..6fa6fb07b 100644 --- a/Aaru.Tests/Checksums/CdChecksumsFix.cs +++ b/Aaru.Tests/Checksums/CdChecksumsFix.cs @@ -27,6 +27,7 @@ // ****************************************************************************/ using System; +using System.IO; using Aaru.Checksums; using FluentAssertions; using NUnit.Framework; @@ -420,6 +421,46 @@ public class CdChecksumsFix CdChecksums.CheckCdSector(sector).Should().BeFalse(); } + [Test] + public void Mode1TwoBytesInSamePRowCanBeFixed() + { + RequirePreparedSector(Mode1Sector, nameof(Mode1Sector)); + + var sector = new byte[2352]; + Array.Copy(Mode1Sector, sector, sector.Length); + CorruptOffsets(sector, 0x62, 0xB8); + + CdChecksums.CheckCdSector(sector).Should().BeFalse(); + + SectorFixResult result = CdChecksums.FixSector(sector); + + result.Should().Be(SectorFixResult.Fixed); + CdChecksums.CheckCdSector(sector).Should().BeTrue(); + sector.Should().BeEquivalentTo(Mode1Sector); + } + + [Test] + public void FailingSectorCanBeFixedWithC2Pointers() + { + string failingPath = Path.Combine(Consts.TestFilesRoot, "Checksum test files", "failingsector.bin"); + string workingPath = Path.Combine(Consts.TestFilesRoot, "Checksum test files", "workingsector.bin"); + + if(!File.Exists(failingPath) || !File.Exists(workingPath)) + Assert.Ignore("External sector fixtures are not present."); + + byte[] sector = File.ReadAllBytes(failingPath); + byte[] working = File.ReadAllBytes(workingPath); + byte[] c2Pointers = BuildC2PointersFromDifferences(sector, working); + + CdChecksums.CheckCdSector(sector).Should().BeFalse(); + + SectorFixResult result = CdChecksums.FixSector(sector, c2Pointers); + + result.Should().Be(SectorFixResult.Fixed); + CdChecksums.CheckCdSector(sector).Should().BeTrue(); + sector.Should().BeEquivalentTo(working); + } + [Test] public void Mode2Form1HeavyCorruptionCanStillBeFixed() { @@ -460,6 +501,20 @@ public class CdChecksumsFix for(var i = 0; i < offsets.Length; i++) sector[offsets[i]] ^= (byte)(0x11 << (i & 3)); } + static byte[] BuildC2PointersFromDifferences(byte[] sector, byte[] fixedSector) + { + var c2Pointers = new byte[294]; + + for(var i = 0; i < sector.Length; i++) + { + if(sector[i] == fixedSector[i]) continue; + + c2Pointers[i / 8] |= (byte)(0x80 >> (i & 7)); + } + + return c2Pointers; + } + static void RequirePreparedSector(byte[] sector, string name) { if(CdChecksums.CheckCdSector(sector) == true) return;