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.
- package/README.md +223 -22
- package/dist/cli.js +24 -11
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/utils/audio.d.ts +23 -0
- package/dist/utils/audio.js +98 -0
- package/dist/utils/decoder.js +104 -0
- package/dist/utils/ecc.d.ts +75 -0
- package/dist/utils/ecc.js +446 -0
- package/dist/utils/encoder.js +151 -43
- package/dist/utils/robust-audio.d.ts +54 -0
- package/dist/utils/robust-audio.js +400 -0
- package/dist/utils/robust-image.d.ts +54 -0
- package/dist/utils/robust-image.js +515 -0
- package/dist/utils/types.d.ts +24 -0
- package/dist/utils/zstd.js +26 -12
- package/package.json +1 -1
- package/roxify_native-x86_64-unknown-linux-gnu.node +0 -0
- package/roxify_native.node +0 -0
package/dist/utils/decoder.js
CHANGED
|
@@ -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 };
|