295 lines
11 KiB
Dart
295 lines
11 KiB
Dart
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<int> _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<int> _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<int> _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<int> 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<int> of nibbles (high nibble first).
|
||
static List<int> unpackNibbles(List<int> bytes) {
|
||
final out = <int>[];
|
||
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<int> _encodeNibbles(List<int> data) {
|
||
assert(data.length == _k, 'RS encode: input must be $_k nibbles');
|
||
|
||
// Work buffer: data shifted left by t positions.
|
||
final r = List<int>.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<int>.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<int>).
|
||
/// Returns true if no uncorrectable error was detected.
|
||
static bool _decodeNibbles(List<int> 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<int>.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<int>.filled(_t + 1, 0);
|
||
sigma[0] = 1;
|
||
var prev = List<int>.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<int>.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 = <int>[];
|
||
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<int>.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
|
||
}
|
||
}
|