roxify 1.6.6 → 1.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,11 +3,14 @@ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
3
3
  import { tmpdir } from 'os';
4
4
  import { join } from 'path';
5
5
  import { unpackBuffer } from '../pack.js';
6
+ import { isWav, wavToBytes } from './audio.js';
6
7
  import { CHUNK_TYPE, MAGIC, MARKER_END, MARKER_START, PIXEL_MAGIC, PIXEL_MAGIC_BLOCK, PNG_HEADER, } from './constants.js';
7
8
  import { DataFormatError, IncorrectPassphraseError, PassphraseRequiredError, } from './errors.js';
8
9
  import { colorsToBytes, deltaDecode, tryDecryptIfNeeded } from './helpers.js';
9
10
  import { native } from './native.js';
10
11
  import { cropAndReconstitute } from './reconstitution.js';
12
+ import { decodeRobustAudio, isRobustAudioWav } from './robust-audio.js';
13
+ import { decodeRobustImage, isRobustImage } from './robust-image.js';
11
14
  import { parallelZstdDecompress, tryZstdDecompress } from './zstd.js';
12
15
  function isColorMatch(r1, g1, b1, r2, g2, b2) {
13
16
  return Math.abs(r1 - r2) + Math.abs(g1 - g2) + Math.abs(b1 - b2) < 50;
@@ -153,6 +156,107 @@ export async function decodePngToBinary(input, opts = {}) {
153
156
  }
154
157
  if (opts.onProgress)
155
158
  opts.onProgress({ phase: 'processed' });
159
+ // ─── Robust audio detection (lossy-resilient WAV) ──────────────────────────
160
+ if (isWav(processedBuf) && isRobustAudioWav(processedBuf)) {
161
+ try {
162
+ const result = decodeRobustAudio(processedBuf);
163
+ if (opts.onProgress)
164
+ opts.onProgress({ phase: 'done' });
165
+ progressBar?.stop();
166
+ // Try unpack multi-file archive
167
+ try {
168
+ const unpack = unpackBuffer(result.data);
169
+ if (unpack && unpack.files && unpack.files.length > 0) {
170
+ return { files: unpack.files, correctedErrors: result.correctedErrors };
171
+ }
172
+ }
173
+ catch (e) { }
174
+ return { buf: result.data, correctedErrors: result.correctedErrors };
175
+ }
176
+ catch (e) {
177
+ // Fall through to legacy WAV decoding
178
+ }
179
+ }
180
+ // ─── WAV container detection ───────────────────────────────────────────────
181
+ if (isWav(processedBuf)) {
182
+ const pcmData = wavToBytes(processedBuf);
183
+ // The WAV payload starts with PIXEL_MAGIC ("PXL1")
184
+ if (pcmData.length >= 4 && pcmData.subarray(0, 4).equals(PIXEL_MAGIC)) {
185
+ let idx = 4; // skip PIXEL_MAGIC
186
+ const version = pcmData[idx++];
187
+ const nameLen = pcmData[idx++];
188
+ let name;
189
+ if (nameLen > 0) {
190
+ name = pcmData.subarray(idx, idx + nameLen).toString('utf8');
191
+ idx += nameLen;
192
+ }
193
+ const payloadLen = pcmData.readUInt32BE(idx);
194
+ idx += 4;
195
+ const rawPayload = pcmData.subarray(idx, idx + payloadLen);
196
+ idx += payloadLen;
197
+ // Check for rXFL file list after payload
198
+ let fileListJson;
199
+ if (idx + 8 < pcmData.length &&
200
+ pcmData.subarray(idx, idx + 4).toString('utf8') === 'rXFL') {
201
+ idx += 4;
202
+ const jsonLen = pcmData.readUInt32BE(idx);
203
+ idx += 4;
204
+ fileListJson = pcmData.subarray(idx, idx + jsonLen).toString('utf8');
205
+ }
206
+ let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
207
+ if (opts.onProgress)
208
+ opts.onProgress({ phase: 'decompress_start' });
209
+ try {
210
+ payload = await tryDecompress(payload, (info) => {
211
+ if (opts.onProgress)
212
+ opts.onProgress(info);
213
+ });
214
+ }
215
+ catch (e) {
216
+ const errMsg = e instanceof Error ? e.message : String(e);
217
+ if (opts.passphrase)
218
+ throw new IncorrectPassphraseError('Incorrect passphrase (WAV mode, zstd failed: ' + errMsg + ')');
219
+ throw new DataFormatError('WAV mode zstd decompression failed: ' + errMsg);
220
+ }
221
+ if (!payload.subarray(0, MAGIC.length).equals(MAGIC)) {
222
+ throw new DataFormatError('Invalid ROX format in WAV (missing ROX1 magic after decompression)');
223
+ }
224
+ payload = payload.subarray(MAGIC.length);
225
+ if (opts.onProgress)
226
+ opts.onProgress({ phase: 'done' });
227
+ progressBar?.stop();
228
+ // Try unpack multi-file archive
229
+ try {
230
+ const unpack = unpackBuffer(payload);
231
+ if (unpack && unpack.files && unpack.files.length > 0) {
232
+ return { files: unpack.files, meta: { name } };
233
+ }
234
+ }
235
+ catch (e) { }
236
+ return { buf: payload, meta: { name } };
237
+ }
238
+ }
239
+ // ─── Robust image detection (lossy-resilient PNG) ──────────────────────────
240
+ try {
241
+ if (isRobustImage(processedBuf)) {
242
+ const result = decodeRobustImage(processedBuf);
243
+ if (opts.onProgress)
244
+ opts.onProgress({ phase: 'done' });
245
+ progressBar?.stop();
246
+ try {
247
+ const unpack = unpackBuffer(result.data);
248
+ if (unpack && unpack.files && unpack.files.length > 0) {
249
+ return { files: unpack.files, correctedErrors: result.correctedErrors };
250
+ }
251
+ }
252
+ catch (e) { }
253
+ return { buf: result.data, correctedErrors: result.correctedErrors };
254
+ }
255
+ }
256
+ catch (e) {
257
+ // Fall through to standard decoding
258
+ }
259
+ // ─── MAGIC header (compact mode) ──────────────────────────────────────────
156
260
  if (processedBuf.subarray(0, MAGIC.length).equals(MAGIC)) {
157
261
  const d = processedBuf.subarray(MAGIC.length);
158
262
  const nameLen = d[0];
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Reed-Solomon Error Correction over GF(2^8).
3
+ *
4
+ * Uses the same Galois Field parameters as QR codes:
5
+ * - Primitive polynomial: x^8 + x^4 + x^3 + x^2 + 1 (0x11D)
6
+ * - Generator element: α = 2
7
+ * - First consecutive root (FCR): 0
8
+ *
9
+ * Max codeword length per block: 255 symbols.
10
+ * Correction capability: floor(nsym / 2) symbol errors per block.
11
+ *
12
+ * For arbitrary-length data, the module splits into RS blocks,
13
+ * applies interleaving to spread burst errors, and prepends a
14
+ * lightweight header for self-describing decoding.
15
+ */
16
+ declare const GF_EXP: Uint8Array<ArrayBuffer>;
17
+ declare const GF_LOG: Uint8Array<ArrayBuffer>;
18
+ declare function gfMul(a: number, b: number): number;
19
+ declare function gfDiv(a: number, b: number): number;
20
+ declare function gfPow(a: number, n: number): number;
21
+ /**
22
+ * Evaluate p(x) using Horner's method. p[0] is the leading coefficient.
23
+ */
24
+ declare function polyEval(p: number[], x: number): number;
25
+ /**
26
+ * Encode data with Reed-Solomon. Returns data followed by nsym parity bytes.
27
+ * @param data - Original data (max 255 - nsym bytes).
28
+ * @param nsym - Number of ECC parity symbols (correction = floor(nsym/2) errors).
29
+ */
30
+ export declare function rsEncode(data: Uint8Array, nsym: number): Uint8Array;
31
+ /**
32
+ * Compute syndromes S_j = msg(α^j) for j = 0..nsym-1.
33
+ */
34
+ declare function calcSyndromes(msg: Uint8Array, nsym: number): number[];
35
+ /**
36
+ * Decode a Reed-Solomon codeword, correcting up to floor(nsym/2) errors.
37
+ * @param msg - Received codeword (data + parity, length ≤ 255).
38
+ * @param nsym - Number of ECC parity symbols used during encoding.
39
+ * @returns Corrected data bytes and the number of corrected errors.
40
+ * @throws If the codeword has too many errors to correct.
41
+ */
42
+ export declare function rsDecode(msg: Uint8Array, nsym: number): {
43
+ data: Uint8Array;
44
+ corrected: number;
45
+ };
46
+ /** ECC correction levels (percentage of redundancy). */
47
+ export type EccLevel = 'low' | 'medium' | 'quartile' | 'high';
48
+ /**
49
+ * Encode arbitrary data with Reed-Solomon error correction + interleaving.
50
+ *
51
+ * Output format:
52
+ * [4B] "ECC1" magic
53
+ * [1B] version
54
+ * [1B] nsym (ECC symbols per block)
55
+ * [4B] original data length (big-endian)
56
+ * [2B] number of RS blocks (big-endian)
57
+ * [...] interleaved RS-encoded blocks
58
+ *
59
+ * @param data - Raw data to protect.
60
+ * @param level - Error correction level (default: 'medium').
61
+ * @returns Protected data with ECC.
62
+ */
63
+ export declare function eccEncode(data: Buffer, level?: EccLevel): Buffer;
64
+ /**
65
+ * Decode ECC-protected data, correcting errors.
66
+ *
67
+ * @param protected_ - Data produced by eccEncode.
68
+ * @returns Recovered original data and total number of corrected errors.
69
+ * @throws If data is too corrupted to recover.
70
+ */
71
+ export declare function eccDecode(protected_: Buffer): {
72
+ data: Buffer;
73
+ totalCorrected: number;
74
+ };
75
+ export { GF_EXP, GF_LOG, calcSyndromes, gfDiv, gfMul, gfPow, polyEval };
@@ -0,0 +1,446 @@
1
+ /**
2
+ * Reed-Solomon Error Correction over GF(2^8).
3
+ *
4
+ * Uses the same Galois Field parameters as QR codes:
5
+ * - Primitive polynomial: x^8 + x^4 + x^3 + x^2 + 1 (0x11D)
6
+ * - Generator element: α = 2
7
+ * - First consecutive root (FCR): 0
8
+ *
9
+ * Max codeword length per block: 255 symbols.
10
+ * Correction capability: floor(nsym / 2) symbol errors per block.
11
+ *
12
+ * For arbitrary-length data, the module splits into RS blocks,
13
+ * applies interleaving to spread burst errors, and prepends a
14
+ * lightweight header for self-describing decoding.
15
+ */
16
+ // ─── GF(256) Arithmetic ─────────────────────────────────────────────────────
17
+ const GF_EXP = new Uint8Array(512); // α^i (doubled for fast mod-free mul)
18
+ const GF_LOG = new Uint8Array(256); // log_α(x)
19
+ {
20
+ let x = 1;
21
+ for (let i = 0; i < 255; i++) {
22
+ GF_EXP[i] = x;
23
+ GF_LOG[x] = i;
24
+ x = ((x << 1) ^ (x & 0x80 ? 0x1d : 0)) & 0xff;
25
+ }
26
+ for (let i = 255; i < 512; i++) {
27
+ GF_EXP[i] = GF_EXP[i - 255];
28
+ }
29
+ }
30
+ function gfMul(a, b) {
31
+ if (a === 0 || b === 0)
32
+ return 0;
33
+ return GF_EXP[GF_LOG[a] + GF_LOG[b]];
34
+ }
35
+ function gfDiv(a, b) {
36
+ if (b === 0)
37
+ throw new Error('GF(256): division by zero');
38
+ if (a === 0)
39
+ return 0;
40
+ return GF_EXP[(GF_LOG[a] + 255 - GF_LOG[b]) % 255];
41
+ }
42
+ function gfPow(a, n) {
43
+ if (a === 0)
44
+ return n === 0 ? 1 : 0;
45
+ return GF_EXP[(GF_LOG[a] * n) % 255];
46
+ }
47
+ // ─── Polynomial Operations (descending degree: p[0] = leading coeff) ────────
48
+ /**
49
+ * Evaluate p(x) using Horner's method. p[0] is the leading coefficient.
50
+ */
51
+ function polyEval(p, x) {
52
+ let y = p[0];
53
+ for (let i = 1; i < p.length; i++) {
54
+ y = gfMul(y, x) ^ p[i];
55
+ }
56
+ return y;
57
+ }
58
+ /**
59
+ * Multiply two polynomials in GF(256).
60
+ */
61
+ function polyMul(p, q) {
62
+ const r = new Array(p.length + q.length - 1).fill(0);
63
+ for (let j = 0; j < q.length; j++) {
64
+ for (let i = 0; i < p.length; i++) {
65
+ r[i + j] ^= gfMul(p[i], q[j]);
66
+ }
67
+ }
68
+ return r;
69
+ }
70
+ // ─── RS Generator Polynomial ────────────────────────────────────────────────
71
+ const genCache = new Map();
72
+ /**
73
+ * g(x) = (x - α^0)(x - α^1)...(x - α^(nsym-1))
74
+ * In GF(2), subtraction = addition, so g(x) = ∏(x + α^i).
75
+ * Stored in descending order: g[0] = 1 (leading coeff).
76
+ */
77
+ function rsGenPoly(nsym) {
78
+ if (genCache.has(nsym))
79
+ return genCache.get(nsym);
80
+ let g = [1];
81
+ for (let i = 0; i < nsym; i++) {
82
+ g = polyMul(g, [1, GF_EXP[i]]);
83
+ }
84
+ genCache.set(nsym, g);
85
+ return g;
86
+ }
87
+ // ─── RS Encoding (Systematic) ───────────────────────────────────────────────
88
+ /**
89
+ * Encode data with Reed-Solomon. Returns data followed by nsym parity bytes.
90
+ * @param data - Original data (max 255 - nsym bytes).
91
+ * @param nsym - Number of ECC parity symbols (correction = floor(nsym/2) errors).
92
+ */
93
+ export function rsEncode(data, nsym) {
94
+ const k = data.length;
95
+ if (k + nsym > 255)
96
+ throw new Error(`RS block too large: ${k}+${nsym} > 255`);
97
+ const gen = rsGenPoly(nsym);
98
+ // Long division: data * x^nsym mod g(x)
99
+ const msg = new Array(k + nsym).fill(0);
100
+ for (let i = 0; i < k; i++)
101
+ msg[i] = data[i];
102
+ for (let i = 0; i < k; i++) {
103
+ const coef = msg[i];
104
+ if (coef !== 0) {
105
+ for (let j = 1; j < gen.length; j++) {
106
+ msg[i + j] ^= gfMul(gen[j], coef);
107
+ }
108
+ }
109
+ }
110
+ // Result: original data | parity
111
+ const result = new Uint8Array(k + nsym);
112
+ result.set(data);
113
+ for (let i = 0; i < nsym; i++)
114
+ result[k + i] = msg[k + i];
115
+ return result;
116
+ }
117
+ // ─── RS Decoding ────────────────────────────────────────────────────────────
118
+ /**
119
+ * Compute syndromes S_j = msg(α^j) for j = 0..nsym-1.
120
+ */
121
+ function calcSyndromes(msg, nsym) {
122
+ const synd = new Array(nsym);
123
+ const arr = Array.from(msg);
124
+ for (let j = 0; j < nsym; j++) {
125
+ synd[j] = polyEval(arr, GF_EXP[j]);
126
+ }
127
+ return synd;
128
+ }
129
+ /**
130
+ * Berlekamp-Massey algorithm.
131
+ * Returns error locator polynomial σ(x) in ascending order:
132
+ * σ[0] = 1 (constant), σ[1] = coefficient of x, etc.
133
+ */
134
+ function berlekampMassey(synd, nsym) {
135
+ let C = [1]; // error locator (ascending)
136
+ let B = [1]; // previous polynomial
137
+ let L = 0; // number of errors
138
+ let m = 1; // shift counter
139
+ let b = 1; // previous discrepancy
140
+ for (let n = 0; n < nsym; n++) {
141
+ // Compute discrepancy d
142
+ let d = synd[n];
143
+ for (let i = 1; i <= L; i++) {
144
+ if (i < C.length) {
145
+ d ^= gfMul(C[i], synd[n - i]);
146
+ }
147
+ }
148
+ if (d === 0) {
149
+ m++;
150
+ }
151
+ else if (2 * L <= n) {
152
+ // Degree increase
153
+ const T = [...C];
154
+ const factor = gfDiv(d, b);
155
+ // C(x) -= (d/b) * x^m * B(x)
156
+ const shifted = new Array(m).fill(0);
157
+ for (let i = 0; i < B.length; i++)
158
+ shifted.push(gfMul(B[i], factor));
159
+ while (C.length < shifted.length)
160
+ C.push(0);
161
+ for (let i = 0; i < shifted.length; i++)
162
+ C[i] ^= shifted[i];
163
+ L = n + 1 - L;
164
+ B = T;
165
+ b = d;
166
+ m = 1;
167
+ }
168
+ else {
169
+ // No degree increase
170
+ const factor = gfDiv(d, b);
171
+ const shifted = new Array(m).fill(0);
172
+ for (let i = 0; i < B.length; i++)
173
+ shifted.push(gfMul(B[i], factor));
174
+ while (C.length < shifted.length)
175
+ C.push(0);
176
+ for (let i = 0; i < shifted.length; i++)
177
+ C[i] ^= shifted[i];
178
+ m++;
179
+ }
180
+ }
181
+ return C; // ascending: C[0]=1, C[1]=σ_1, ..., C[L]=σ_L
182
+ }
183
+ /**
184
+ * Evaluate polynomial in ascending order (p[0] = constant, p[i] = coeff of x^i).
185
+ */
186
+ function polyEvalAsc(p, x) {
187
+ if (p.length === 0)
188
+ return 0;
189
+ let result = p[p.length - 1];
190
+ for (let i = p.length - 2; i >= 0; i--) {
191
+ result = gfMul(result, x) ^ p[i];
192
+ }
193
+ return result;
194
+ }
195
+ /**
196
+ * Chien search: find error positions from σ(x) (ascending order).
197
+ * For each codeword position j (array index), error at j ↔ σ(α^(-position)) = 0.
198
+ */
199
+ function chienSearch(sigma, n) {
200
+ const errPos = [];
201
+ const numErrors = sigma.length - 1;
202
+ for (let i = 0; i < 255; i++) {
203
+ const val = polyEvalAsc(sigma, GF_EXP[i]);
204
+ if (val === 0) {
205
+ // σ(α^i) = 0 → X^(-1) = α^i → X = α^(255-i)
206
+ // position j: α^(n-1-j) = α^(255-i) → n-1-j ≡ 255-i (mod 255)
207
+ // j = (n + i - 1) % 255 (when n ≤ 255)
208
+ const j = (n + i - 1) % 255;
209
+ if (j >= 0 && j < n) {
210
+ errPos.push(j);
211
+ }
212
+ }
213
+ }
214
+ if (errPos.length !== numErrors) {
215
+ throw new Error(`RS Chien search: found ${errPos.length} roots but expected ${numErrors}. Data may be too corrupted.`);
216
+ }
217
+ return errPos;
218
+ }
219
+ /**
220
+ * Compute error magnitudes by solving the Vandermonde system directly.
221
+ * S_j = Σ_k e_k * X_k^j for j = 0..nsym-1
222
+ * where X_k = α^(n-1-errPos[k]).
223
+ * Uses Gaussian elimination in GF(256). O(v^3) where v = number of errors.
224
+ */
225
+ function solveErrorValues(X, synd) {
226
+ const v = X.length;
227
+ if (v === 0)
228
+ return [];
229
+ // Build augmented matrix [Vandermonde | syndromes]
230
+ const A = [];
231
+ for (let i = 0; i < v; i++) {
232
+ const row = new Array(v + 1);
233
+ for (let j = 0; j < v; j++) {
234
+ row[j] = gfPow(X[j], i);
235
+ }
236
+ row[v] = synd[i];
237
+ A.push(row);
238
+ }
239
+ // Gaussian elimination
240
+ for (let col = 0; col < v; col++) {
241
+ // Find pivot
242
+ let pivotRow = -1;
243
+ for (let row = col; row < v; row++) {
244
+ if (A[row][col] !== 0) {
245
+ pivotRow = row;
246
+ break;
247
+ }
248
+ }
249
+ if (pivotRow === -1) {
250
+ throw new Error('RS: singular Vandermonde matrix');
251
+ }
252
+ // Swap rows
253
+ if (pivotRow !== col) {
254
+ [A[col], A[pivotRow]] = [A[pivotRow], A[col]];
255
+ }
256
+ // Scale pivot row
257
+ const pivotInv = gfDiv(1, A[col][col]);
258
+ for (let j = col; j <= v; j++) {
259
+ A[col][j] = gfMul(A[col][j], pivotInv);
260
+ }
261
+ // Eliminate column
262
+ for (let row = 0; row < v; row++) {
263
+ if (row !== col && A[row][col] !== 0) {
264
+ const factor = A[row][col];
265
+ for (let j = col; j <= v; j++) {
266
+ A[row][j] ^= gfMul(factor, A[col][j]);
267
+ }
268
+ }
269
+ }
270
+ }
271
+ return A.map((row) => row[v]);
272
+ }
273
+ /**
274
+ * Decode a Reed-Solomon codeword, correcting up to floor(nsym/2) errors.
275
+ * @param msg - Received codeword (data + parity, length ≤ 255).
276
+ * @param nsym - Number of ECC parity symbols used during encoding.
277
+ * @returns Corrected data bytes and the number of corrected errors.
278
+ * @throws If the codeword has too many errors to correct.
279
+ */
280
+ export function rsDecode(msg, nsym) {
281
+ const n = msg.length;
282
+ if (n > 255)
283
+ throw new Error(`RS block too large: ${n} > 255`);
284
+ // 1. Syndromes
285
+ const synd = calcSyndromes(msg, nsym);
286
+ // No errors?
287
+ if (synd.every((s) => s === 0)) {
288
+ return { data: new Uint8Array(msg.subarray(0, n - nsym)), corrected: 0 };
289
+ }
290
+ // 2. Error locator via Berlekamp-Massey
291
+ const sigma = berlekampMassey(synd, nsym);
292
+ const numErrors = sigma.length - 1;
293
+ if (numErrors === 0) {
294
+ throw new Error('RS: non-zero syndromes but BM found zero errors');
295
+ }
296
+ // 3. Error positions via Chien search
297
+ const errPos = chienSearch(sigma, n);
298
+ // 4. Error magnitudes via Vandermonde solve
299
+ const X = errPos.map((j) => GF_EXP[(n - 1 - j) % 255]);
300
+ const errValues = solveErrorValues(X, synd);
301
+ // 5. Correct errors
302
+ const corrected = new Uint8Array(msg);
303
+ for (let k = 0; k < errPos.length; k++) {
304
+ corrected[errPos[k]] ^= errValues[k];
305
+ }
306
+ // 6. Verify correction
307
+ const checkSynd = calcSyndromes(corrected, nsym);
308
+ if (!checkSynd.every((s) => s === 0)) {
309
+ throw new Error('RS: correction failed, residual syndromes non-zero');
310
+ }
311
+ return {
312
+ data: new Uint8Array(corrected.subarray(0, n - nsym)),
313
+ corrected: numErrors,
314
+ };
315
+ }
316
+ const ECC_NSYM = {
317
+ low: 20, // ~10% overhead, corrects ~4% errors
318
+ medium: 40, // ~19% overhead, corrects ~9% errors
319
+ quartile: 64, // ~33% overhead, corrects ~15% errors
320
+ high: 128, // ~100% overhead, corrects ~25% errors
321
+ };
322
+ const ECC_MAGIC = Buffer.from('ECC1');
323
+ const ECC_VERSION = 1;
324
+ /**
325
+ * Compute the number of data bytes per RS block for a given nsym.
326
+ */
327
+ function dataPerBlock(nsym) {
328
+ return 255 - nsym;
329
+ }
330
+ /**
331
+ * Interleave bytes from multiple blocks (column-major write).
332
+ * Spreads burst errors across RS blocks for better correction.
333
+ */
334
+ function interleave(blocks) {
335
+ if (blocks.length === 0)
336
+ return new Uint8Array(0);
337
+ const maxLen = Math.max(...blocks.map((b) => b.length));
338
+ const out = [];
339
+ for (let col = 0; col < maxLen; col++) {
340
+ for (let row = 0; row < blocks.length; row++) {
341
+ out.push(col < blocks[row].length ? blocks[row][col] : 0);
342
+ }
343
+ }
344
+ return new Uint8Array(out);
345
+ }
346
+ /**
347
+ * De-interleave bytes back into blocks.
348
+ */
349
+ function deinterleave(data, numBlocks, blockLen) {
350
+ const blocks = [];
351
+ for (let i = 0; i < numBlocks; i++) {
352
+ blocks.push(new Uint8Array(blockLen));
353
+ }
354
+ let idx = 0;
355
+ for (let col = 0; col < blockLen; col++) {
356
+ for (let row = 0; row < numBlocks; row++) {
357
+ if (idx < data.length) {
358
+ blocks[row][col] = data[idx++];
359
+ }
360
+ }
361
+ }
362
+ return blocks;
363
+ }
364
+ /**
365
+ * Encode arbitrary data with Reed-Solomon error correction + interleaving.
366
+ *
367
+ * Output format:
368
+ * [4B] "ECC1" magic
369
+ * [1B] version
370
+ * [1B] nsym (ECC symbols per block)
371
+ * [4B] original data length (big-endian)
372
+ * [2B] number of RS blocks (big-endian)
373
+ * [...] interleaved RS-encoded blocks
374
+ *
375
+ * @param data - Raw data to protect.
376
+ * @param level - Error correction level (default: 'medium').
377
+ * @returns Protected data with ECC.
378
+ */
379
+ export function eccEncode(data, level = 'medium') {
380
+ const nsym = ECC_NSYM[level];
381
+ const k = dataPerBlock(nsym);
382
+ const numBlocks = Math.ceil(data.length / k);
383
+ // RS-encode each block
384
+ const encodedBlocks = [];
385
+ for (let i = 0; i < numBlocks; i++) {
386
+ const start = i * k;
387
+ const end = Math.min(start + k, data.length);
388
+ const blockData = new Uint8Array(k); // zero-padded
389
+ blockData.set(data.subarray(start, end));
390
+ encodedBlocks.push(rsEncode(blockData, nsym));
391
+ }
392
+ // Interleave
393
+ const interleaved = interleave(encodedBlocks);
394
+ // Build header
395
+ const header = Buffer.alloc(12);
396
+ ECC_MAGIC.copy(header, 0);
397
+ header[4] = ECC_VERSION;
398
+ header[5] = nsym;
399
+ header.writeUInt32BE(data.length, 6);
400
+ header.writeUInt16BE(numBlocks, 10);
401
+ return Buffer.concat([header, Buffer.from(interleaved)]);
402
+ }
403
+ /**
404
+ * Decode ECC-protected data, correcting errors.
405
+ *
406
+ * @param protected_ - Data produced by eccEncode.
407
+ * @returns Recovered original data and total number of corrected errors.
408
+ * @throws If data is too corrupted to recover.
409
+ */
410
+ export function eccDecode(protected_) {
411
+ if (protected_.length < 12) {
412
+ throw new Error('ECC data too short for header');
413
+ }
414
+ // Parse header
415
+ if (!protected_.subarray(0, 4).equals(ECC_MAGIC)) {
416
+ throw new Error('Invalid ECC magic (expected "ECC1")');
417
+ }
418
+ const version = protected_[4];
419
+ if (version !== ECC_VERSION) {
420
+ throw new Error(`Unsupported ECC version: ${version}`);
421
+ }
422
+ const nsym = protected_[5];
423
+ const originalLen = protected_.readUInt32BE(6);
424
+ const numBlocks = protected_.readUInt16BE(10);
425
+ const blockLen = 255; // data + parity per block
426
+ const interleaved = protected_.subarray(12);
427
+ // De-interleave
428
+ const blocks = deinterleave(interleaved, numBlocks, blockLen);
429
+ // RS-decode each block
430
+ let totalCorrected = 0;
431
+ const decoded = [];
432
+ const k = dataPerBlock(nsym);
433
+ for (let i = 0; i < numBlocks; i++) {
434
+ const { data, corrected } = rsDecode(blocks[i], nsym);
435
+ totalCorrected += corrected;
436
+ decoded.push(Buffer.from(data));
437
+ }
438
+ // Concatenate and trim to original length
439
+ const full = Buffer.concat(decoded);
440
+ return {
441
+ data: full.subarray(0, originalLen),
442
+ totalCorrected,
443
+ };
444
+ }
445
+ // ─── Exports for testing ────────────────────────────────────────────────────
446
+ export { GF_EXP, GF_LOG, calcSyndromes, gfDiv, gfMul, gfPow, polyEval };