import 'dart:typed_data'; /// Reed-Solomon codec RS(15,11) over GF(16). /// /// GF(16) = GF(2^4) with primitive polynomial p(x) = x^4 + x + 1 (0x13). /// Primitive element α has order 15 (α^15 = 1). /// /// Parameters: /// n = 15 (codeword length in nibbles) /// k = 11 (data symbols per codeword) /// t = 2 (corrects up to 2 nibble-errors per codeword) /// /// Wire mapping: /// 11 data nibbles + 4 parity nibbles = 15 nibbles = 7.5 bytes. /// To keep byte alignment the codec works on full-byte streams: /// encode(Uint8List data) where data.length must be a multiple of 5.5 B /// Callers pack/unpack nibbles via [packNibbles] / [unpackNibbles]. /// /// Usage (encode 11-byte block → 15-byte codeword): /// final cw = ReedSolomon.encodeBlock(data11Bytes); // Uint8List(15) /// final ok = ReedSolomon.decodeBlock(cw); // corrects in-place abstract final class ReedSolomon { // ── GF(16) tables ───────────────────────────────────────────────── // // Computed offline for p(x) = x^4 + x + 1 = 0x13. // α^0 = 1 α^1 = 2 α^2 = 4 α^3 = 8 // α^4 = 3 α^5 = 6 α^6 = 12 α^7 = 11 // α^8 = 5 α^9 = 10 α^10 = 7 α^11 = 14 // α^12 = 15 α^13 = 13 α^14 = 9 α^15 = 1 (wrap) static const List _exp = [ 1, 2, 4, 8, 3, 6, 12, 11, 5, 10, 7, 14, 15, 13, 9, // duplicate for modular index without % 15 1, 2, 4, 8, 3, 6, 12, 11, 5, 10, 7, 14, 15, 13, 9, ]; static const List _log = [ // index 0 → undefined (−∞); we store 0 but never use it for non-zero 0, // log[0] = -∞ (sentinel) 0, // log[1] = 0 (α^0) 1, // log[2] = 1 4, // log[3] = 4 2, // log[4] = 2 8, // log[5] = 8 5, // log[6] = 5 10, // log[7] = 10 3, // log[8] = 3 14, // log[9] = 14 9, // log[10] = 9 7, // log[11] = 7 6, // log[12] = 6 13, // log[13] = 13 11, // log[14] = 11 12, // log[15] = 12 ]; // Generator polynomial g(x) = (x-α)(x-α^2)(x-α^3)(x-α^4) // = x^4 + 13x^3 + 12x^2 + 8x + 7 (coefficients in GF(16)) // g[0] is the constant term, g[4] is the leading coefficient (=1). static const List _gen = [7, 8, 12, 13, 1]; static const int _n = 15; static const int _k = 11; static const int _t = 2; // ── GF(16) arithmetic ───────────────────────────────────────────── // Addition = XOR in GF(2^m). static int _add(int a, int b) => a ^ b; // Multiplication using log/exp lookup tables. static int _mul(int a, int b) { if (a == 0 || b == 0) return 0; return _exp[(_log[a] + _log[b]) % 15]; } // Division a / b. static int _div(int a, int b) { if (a == 0) return 0; assert(b != 0, 'GF division by zero'); return _exp[(_log[a] - _log[b] + 15) % 15]; } // Power α^e. static int _pow(int e) => _exp[e % 15]; // ── Nibble pack / unpack ────────────────────────────────────────── /// Pack n pairs of nibbles into bytes. nibbles.length must be even. static Uint8List packNibbles(List nibbles) { assert(nibbles.length.isEven, 'nibbles must have even length'); final out = Uint8List(nibbles.length >> 1); for (int i = 0; i < out.length; i++) { out[i] = ((nibbles[i * 2] & 0xF) << 4) | (nibbles[i * 2 + 1] & 0xF); } return out; } /// Unpack bytes into a List of nibbles (high nibble first). static List unpackNibbles(List bytes) { final out = []; for (final b in bytes) { out.add((b >> 4) & 0xF); out.add(b & 0xF); } return out; } // ── Encoder ─────────────────────────────────────────────────────── /// Encode [data] (11-nibble list) into a 15-nibble RS codeword. /// /// The encoding is systematic: the first 11 nibbles of the codeword /// are the data, the last 4 are parity computed via polynomial division. static List _encodeNibbles(List data) { assert(data.length == _k, 'RS encode: input must be $_k nibbles'); // Work buffer: data shifted left by t positions. final r = List.filled(_k + _t, 0); for (int i = 0; i < _k; i++) { r[i] = data[i]; } // Polynomial long division: r(x) = x^(n-k)·m(x) mod g(x) for (int i = 0; i < _k; i++) { final coeff = r[i]; if (coeff != 0) { for (int j = 1; j <= _t * 2; j++) { r[i + j] = _add(r[i + j], _mul(_gen[_t * 2 - j], coeff)); } } } // Build codeword: data || parity (last 4 elements of r) final cw = List.from(data); for (int i = _k; i < _n; i++) { cw.add(r[i]); } return cw; } // ── Public API: byte-level encode (11 bytes → 15 bytes) ─────────── /// Encode an 11-byte data block into a 15-byte RS codeword. /// /// The 11 bytes are treated as 22 GF(16) nibbles split into two /// 11-nibble RS codewords. Output is 30 nibbles = 15 bytes. /// /// [data] must have exactly 11 bytes. Returns Uint8List(15). static Uint8List encodeBlock(Uint8List data) { assert(data.length == 11, 'encodeBlock: need 11 bytes, got ${data.length}'); final nibs = unpackNibbles(data); // 22 nibbles // Codeword 0: nibbles [0..10] final cw0 = _encodeNibbles(nibs.sublist(0, 11)); // Codeword 1: nibbles [11..21] final cw1 = _encodeNibbles(nibs.sublist(11, 22)); // Merge 30 nibbles back to 15 bytes return packNibbles([...cw0, ...cw1]); } // ── Decoder (Berlekamp-Massey + Chien + Forney) ─────────────────── /// Decode and correct a 15-nibble codeword (in-place list). /// Returns true if no uncorrectable error was detected. static bool _decodeNibbles(List cw) { assert(cw.length == _n, 'RS decode: codeword must be $_n nibbles'); // ── Step 1: compute syndromes S_i = c(α^i) for i=1..2t ───────── final s = List.filled(2 * _t, 0); for (int i = 0; i < 2 * _t; i++) { int ev = 0; for (int j = 0; j < _n; j++) { ev = _add(_mul(ev, _pow(i + 1)), cw[j]); } s[i] = ev; } // All-zero syndromes → no errors. if (s.every((v) => v == 0)) return true; // ── Step 2: Berlekamp-Massey error-locator polynomial Λ(x) ────── var sigma = List.filled(_t + 1, 0); sigma[0] = 1; var prev = List.filled(_t + 1, 0); prev[0] = 1; int lfsr = 0, d = 1; for (int n = 0; n < 2 * _t; n++) { // Discrepancy Δ = S[n] + Λ_1·S[n-1] + … + Λ_L·S[n-L] int delta = s[n]; for (int i = 1; i <= lfsr; i++) { if (n - i >= 0) delta = _add(delta, _mul(sigma[i], s[n - i])); } if (delta == 0) { d++; } else { final temp = List.from(sigma); final scale = _div(delta, _exp[0]); // delta / 1 = delta // sigma(x) -= (delta / Δ_prev) · x^d · prev(x) for (int i = d; i <= _t + d && i <= _t; i++) { if (i - d < prev.length) { sigma[i] = _add(sigma[i], _mul(scale, prev[i - d])); } } if (2 * lfsr <= n) { lfsr = n + 1 - lfsr; prev = temp; d = 1; } else { d++; } } } // ── Step 3: Chien search — find roots of Λ(x) ────────────────── final errorPositions = []; for (int i = 0; i < _n; i++) { // Evaluate Λ(α^(-i)) = Λ(α^(15-i)) int ev = 0; for (int j = sigma.length - 1; j >= 0; j--) { ev = _add(_mul(ev, _pow((15 - i) % 15)), sigma[j]); } if (ev == 0) errorPositions.add(i); } if (errorPositions.length > _t) return false; // uncorrectable // ── Step 4: Forney algorithm — compute error magnitudes ───────── for (final pos in errorPositions) { // Evaluate error magnitude using Forney formula. // For t≤2 we use the direct closed-form when |errors|≤2. // Omega(x) = S(x)·Λ(x) mod x^(2t) final omega = List.filled(2 * _t, 0); for (int i = 0; i < 2 * _t; i++) { for (int j = 0; j <= i && j < sigma.length; j++) { if (i - j < s.length) { omega[i] = _add(omega[i], _mul(sigma[j], s[i - j])); } } } // Λ'(x) formal derivative (odd powers survive in GF(2)) // For binary GF, the derivative has only odd-indexed terms. int sigmaDerivAtXi = 0; for (int j = 1; j < sigma.length; j += 2) { sigmaDerivAtXi = _add(sigmaDerivAtXi, _mul(sigma[j], _pow(j * (15 - pos) % 15))); } if (sigmaDerivAtXi == 0) return false; // Evaluate Omega at α^(-pos) int omegaVal = 0; for (int i = omega.length - 1; i >= 0; i--) { omegaVal = _add(_mul(omegaVal, _pow((15 - pos) % 15)), omega[i]); } // Error value = -X^(-1) · Omega(X^(-1)) / Λ'(X^(-1)) // In GF(2^m): -a = a, so e = _mul(inv(α^pos), omegaVal) / sigmaDerivAtXi final xi = _pow(pos); // α^pos final e = _div(_mul(_div(1, xi), omegaVal), sigmaDerivAtXi); cw[pos] = _add(cw[pos], e); } return true; } /// Decode and error-correct a 15-byte RS block in-place. /// /// Returns true if the block is valid (or was corrected successfully). /// Returns false if errors exceeded the correction capacity. static bool decodeBlock(Uint8List block) { assert(block.length == 15, 'decodeBlock: need 15 bytes, got ${block.length}'); final nibs = unpackNibbles(block); // 30 nibbles final cw0 = nibs.sublist(0, 15); final cw1 = nibs.sublist(15, 30); final ok0 = _decodeNibbles(cw0); final ok1 = _decodeNibbles(cw1); if (!ok0 || !ok1) return false; // Write corrected nibbles back to byte array. final merged = [...cw0, ...cw1]; final packed = packNibbles(merged); for (int i = 0; i < 15; i++) { block[i] = packed[i]; } return true; } /// Extract just the 11 data bytes from a (possibly corrected) 15-byte block. static Uint8List extractData(Uint8List block) { assert(block.length == 15); final nibs = unpackNibbles(block); // Data nibbles: [0..10] from cw0 and [15..25] from cw1 → merge final dataNibs = [...nibs.sublist(0, 11), ...nibs.sublist(15, 26)]; return packNibbles(dataNibs); // 22 nibbles → 11 bytes } }