roxify 1.6.5 → 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 +550 -212
- package/dist/cli.js +54 -23
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/pack.d.ts +1 -0
- package/dist/pack.js +52 -3
- package/dist/utils/audio.d.ts +23 -0
- package/dist/utils/audio.js +98 -0
- package/dist/utils/decoder.js +114 -21
- package/dist/utils/ecc.d.ts +75 -0
- package/dist/utils/ecc.js +446 -0
- package/dist/utils/encoder.js +155 -49
- package/dist/utils/native.js +43 -26
- 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 +26 -0
- package/dist/utils/zstd.d.ts +2 -2
- package/dist/utils/zstd.js +41 -18
- package/package.json +7 -6
- package/roxify_native-x86_64-unknown-linux-gnu.node +0 -0
- package/roxify_native.node +0 -0
- package/dist/_esmodule_test.d.ts +0 -1
- package/dist/_esmodule_test.js +0 -1
- package/dist/hybrid-compression.d.ts +0 -25
- package/dist/hybrid-compression.js +0 -90
- package/dist/minpng.d.ts +0 -20
- package/dist/minpng.js +0 -285
- package/dist/rox.exe +0 -0
- package/dist/roxify_native-x86_64-pc-windows-gnu.node +0 -0
- package/dist/roxify_native-x86_64-unknown-linux-gnu.node +0 -0
- package/dist/roxify_native.exe +0 -0
- package/dist/roxify_native.node +0 -0
- package/roxify_native-x86_64-pc-windows-msvc.node +0 -0
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { DataFormatError, decodePngToBinary, encodeBinaryToPng, hasPassphraseInP
|
|
|
6
6
|
import { packPathsGenerator, unpackBuffer } from './pack.js';
|
|
7
7
|
import * as cliProgress from './stub-progress.js';
|
|
8
8
|
import { encodeWithRustCLI, isRustBinaryAvailable, } from './utils/rust-cli-wrapper.js';
|
|
9
|
-
const VERSION = '1.6.
|
|
9
|
+
const VERSION = '1.6.1';
|
|
10
10
|
function getDirectorySize(dirPath) {
|
|
11
11
|
let totalSize = 0;
|
|
12
12
|
try {
|
|
@@ -52,22 +52,25 @@ async function readLargeFile(filePath) {
|
|
|
52
52
|
}
|
|
53
53
|
function showHelp() {
|
|
54
54
|
console.log(`
|
|
55
|
-
ROX CLI — Encode/decode binary in PNG
|
|
55
|
+
ROX CLI — Encode/decode binary in PNG or WAV
|
|
56
56
|
|
|
57
57
|
Usage:
|
|
58
58
|
npx rox <command> [options]
|
|
59
59
|
|
|
60
60
|
Commands:
|
|
61
|
-
encode <input>... [output] Encode one or more files/directories
|
|
62
|
-
decode <input> [output]
|
|
63
|
-
list <input>
|
|
64
|
-
havepassphrase <input>
|
|
61
|
+
encode <input>... [output] Encode one or more files/directories
|
|
62
|
+
decode <input> [output] Decode PNG/WAV to original file
|
|
63
|
+
list <input> List files in a Rox archive
|
|
64
|
+
havepassphrase <input> Check whether the archive requires a passphrase
|
|
65
65
|
|
|
66
66
|
Options:
|
|
67
|
+
--image Use PNG container (default)
|
|
68
|
+
--sound Use WAV audio container (smaller overhead, faster)
|
|
67
69
|
-p, --passphrase <pass> Use passphrase (AES-256-GCM)
|
|
68
70
|
-m, --mode <mode> Mode: screenshot (default)
|
|
69
71
|
-e, --encrypt <type> auto|aes|xor|none
|
|
70
72
|
--no-compress Disable compression
|
|
73
|
+
--dict <file> Use zstd dictionary when compressing
|
|
71
74
|
--force-ts Force TypeScript encoder (slower but supports encryption)
|
|
72
75
|
-o, --output <path> Output file path
|
|
73
76
|
-s, --sizes Show file sizes in 'list' output (default)
|
|
@@ -115,6 +118,14 @@ function parseArgs(args) {
|
|
|
115
118
|
parsed.forceTs = true;
|
|
116
119
|
i++;
|
|
117
120
|
}
|
|
121
|
+
else if (key === 'sound') {
|
|
122
|
+
parsed.container = 'sound';
|
|
123
|
+
i++;
|
|
124
|
+
}
|
|
125
|
+
else if (key === 'image') {
|
|
126
|
+
parsed.container = 'image';
|
|
127
|
+
i++;
|
|
128
|
+
}
|
|
118
129
|
else if (key === 'debug-dir') {
|
|
119
130
|
parsed.debugDir = args[i + 1];
|
|
120
131
|
i += 2;
|
|
@@ -123,6 +134,10 @@ function parseArgs(args) {
|
|
|
123
134
|
parsed.files = args[i + 1].split(',');
|
|
124
135
|
i += 2;
|
|
125
136
|
}
|
|
137
|
+
else if (key === 'dict') {
|
|
138
|
+
parsed.dict = args[i + 1];
|
|
139
|
+
i += 2;
|
|
140
|
+
}
|
|
126
141
|
else {
|
|
127
142
|
const value = args[i + 1];
|
|
128
143
|
parsed[key] = value;
|
|
@@ -175,15 +190,11 @@ function parseArgs(args) {
|
|
|
175
190
|
}
|
|
176
191
|
async function encodeCommand(args) {
|
|
177
192
|
const parsed = parseArgs(args);
|
|
178
|
-
const inputPaths = parsed.output
|
|
179
|
-
? parsed._
|
|
180
|
-
: parsed._.length > 1
|
|
181
|
-
? parsed._.slice(0, -1)
|
|
193
|
+
const inputPaths = parsed.output ? parsed._
|
|
194
|
+
: parsed._.length > 1 ? parsed._.slice(0, -1)
|
|
182
195
|
: parsed._;
|
|
183
|
-
const outputPath = parsed.output
|
|
184
|
-
?
|
|
185
|
-
: parsed._.length > 1
|
|
186
|
-
? parsed._[parsed._.length - 1]
|
|
196
|
+
const outputPath = parsed.output ? undefined
|
|
197
|
+
: parsed._.length > 1 ? parsed._[parsed._.length - 1]
|
|
187
198
|
: undefined;
|
|
188
199
|
const firstInput = inputPaths[0];
|
|
189
200
|
if (!firstInput) {
|
|
@@ -200,12 +211,14 @@ async function encodeCommand(args) {
|
|
|
200
211
|
safeCwd = '/';
|
|
201
212
|
}
|
|
202
213
|
const resolvedInputs = inputPaths.map((p) => resolve(safeCwd, p));
|
|
214
|
+
const containerMode = parsed.container || 'image'; // default: image (PNG)
|
|
215
|
+
const containerExt = containerMode === 'sound' ? '.wav' : '.png';
|
|
203
216
|
let outputName = inputPaths.length === 1 ? basename(firstInput) : 'archive';
|
|
204
217
|
if (inputPaths.length === 1 && !statSync(resolvedInputs[0]).isDirectory()) {
|
|
205
|
-
outputName = outputName.replace(/(\.[^.]+)?$/,
|
|
218
|
+
outputName = outputName.replace(/(\.[^.]+)?$/, containerExt);
|
|
206
219
|
}
|
|
207
220
|
else {
|
|
208
|
-
outputName +=
|
|
221
|
+
outputName += containerExt;
|
|
209
222
|
}
|
|
210
223
|
let resolvedOutput;
|
|
211
224
|
try {
|
|
@@ -240,7 +253,7 @@ async function encodeCommand(args) {
|
|
|
240
253
|
catch (e) {
|
|
241
254
|
anyInputDir = false;
|
|
242
255
|
}
|
|
243
|
-
if (isRustBinaryAvailable() && !parsed.forceTs &&
|
|
256
|
+
if (isRustBinaryAvailable() && !parsed.forceTs && containerMode !== 'sound') {
|
|
244
257
|
try {
|
|
245
258
|
console.log(`Encoding to ${resolvedOutput} (Using native Rust encoder)\n`);
|
|
246
259
|
const startTime = Date.now();
|
|
@@ -257,7 +270,7 @@ async function encodeCommand(args) {
|
|
|
257
270
|
}, 500);
|
|
258
271
|
const encryptType = parsed.encrypt === 'xor' ? 'xor' : 'aes';
|
|
259
272
|
const fileName = basename(inputPaths[0]);
|
|
260
|
-
await encodeWithRustCLI(inputPaths.length === 1 ? resolvedInputs[0] : resolvedInputs[0], resolvedOutput,
|
|
273
|
+
await encodeWithRustCLI(inputPaths.length === 1 ? resolvedInputs[0] : resolvedInputs[0], resolvedOutput, 19, parsed.passphrase, encryptType, fileName);
|
|
261
274
|
clearInterval(progressInterval);
|
|
262
275
|
const encodeTime = Date.now() - startTime;
|
|
263
276
|
encodeBar.update(100, {
|
|
@@ -290,6 +303,15 @@ async function encodeCommand(args) {
|
|
|
290
303
|
}
|
|
291
304
|
}
|
|
292
305
|
let options = {};
|
|
306
|
+
if (parsed.dict) {
|
|
307
|
+
try {
|
|
308
|
+
options.dict = readFileSync(parsed.dict);
|
|
309
|
+
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
console.error(`failed to read dictionary file: ${parsed.dict}`);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
293
315
|
try {
|
|
294
316
|
const encodeBar = new cliProgress.SingleBar({
|
|
295
317
|
format: ' {bar} {percentage}% | {step} | {elapsed}s',
|
|
@@ -326,8 +348,9 @@ async function encodeCommand(args) {
|
|
|
326
348
|
mode,
|
|
327
349
|
name: parsed.outputName || 'archive',
|
|
328
350
|
skipOptimization: false,
|
|
329
|
-
compressionLevel:
|
|
351
|
+
compressionLevel: 19,
|
|
330
352
|
outputFormat: 'auto',
|
|
353
|
+
container: containerMode,
|
|
331
354
|
});
|
|
332
355
|
if (parsed.verbose)
|
|
333
356
|
options.verbose = true;
|
|
@@ -337,7 +360,7 @@ async function encodeCommand(args) {
|
|
|
337
360
|
options.passphrase = parsed.passphrase;
|
|
338
361
|
options.encrypt = parsed.encrypt || 'aes';
|
|
339
362
|
}
|
|
340
|
-
console.log(`Encoding to ${resolvedOutput} (Mode: ${mode})\n`);
|
|
363
|
+
console.log(`Encoding to ${resolvedOutput} (Mode: ${mode}, Container: ${containerMode === 'sound' ? 'WAV' : 'PNG'})\n`);
|
|
341
364
|
let inputData;
|
|
342
365
|
let inputSizeVal = 0;
|
|
343
366
|
let displayName;
|
|
@@ -347,9 +370,8 @@ async function encodeCommand(args) {
|
|
|
347
370
|
totalBytes = total;
|
|
348
371
|
const packPct = Math.floor((readBytes / totalBytes) * 25);
|
|
349
372
|
targetPct = Math.max(targetPct, packPct);
|
|
350
|
-
currentEncodeStep =
|
|
351
|
-
? `Reading files: ${currentFile}`
|
|
352
|
-
: 'Reading files';
|
|
373
|
+
currentEncodeStep =
|
|
374
|
+
currentFile ? `Reading files: ${currentFile}` : 'Reading files';
|
|
353
375
|
};
|
|
354
376
|
if (inputPaths.length > 1) {
|
|
355
377
|
currentEncodeStep = 'Reading files';
|
|
@@ -511,6 +533,15 @@ async function decodeCommand(args) {
|
|
|
511
533
|
if (parsed.files) {
|
|
512
534
|
options.files = parsed.files;
|
|
513
535
|
}
|
|
536
|
+
if (parsed.dict) {
|
|
537
|
+
try {
|
|
538
|
+
options.dict = readFileSync(parsed.dict);
|
|
539
|
+
}
|
|
540
|
+
catch (e) {
|
|
541
|
+
console.error(`Failed to read dictionary file: ${parsed.dict}`);
|
|
542
|
+
process.exit(1);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
514
545
|
console.log(' ');
|
|
515
546
|
console.log(`Decoding...`);
|
|
516
547
|
console.log(' ');
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
export * from './utils/audio.js';
|
|
1
2
|
export * from './utils/constants.js';
|
|
2
3
|
export * from './utils/crc.js';
|
|
3
4
|
export * from './utils/decoder.js';
|
|
5
|
+
export * from './utils/ecc.js';
|
|
4
6
|
export * from './utils/encoder.js';
|
|
5
7
|
export * from './utils/errors.js';
|
|
6
8
|
export * from './utils/helpers.js';
|
|
@@ -8,7 +10,9 @@ export * from './utils/inspection.js';
|
|
|
8
10
|
export { native } from './utils/native.js';
|
|
9
11
|
export * from './utils/optimization.js';
|
|
10
12
|
export * from './utils/reconstitution.js';
|
|
11
|
-
export
|
|
13
|
+
export * from './utils/robust-audio.js';
|
|
14
|
+
export * from './utils/robust-image.js';
|
|
15
|
+
export { encodeWithRustCLI, isRustBinaryAvailable } from './utils/rust-cli-wrapper.js';
|
|
12
16
|
export * from './utils/types.js';
|
|
13
17
|
export * from './utils/zstd.js';
|
|
14
18
|
export { packPaths, packPathsToParts, unpackBuffer } from './pack.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
export * from './utils/audio.js';
|
|
1
2
|
export * from './utils/constants.js';
|
|
2
3
|
export * from './utils/crc.js';
|
|
3
4
|
export * from './utils/decoder.js';
|
|
5
|
+
export * from './utils/ecc.js';
|
|
4
6
|
export * from './utils/encoder.js';
|
|
5
7
|
export * from './utils/errors.js';
|
|
6
8
|
export * from './utils/helpers.js';
|
|
@@ -8,7 +10,9 @@ export * from './utils/inspection.js';
|
|
|
8
10
|
export { native } from './utils/native.js';
|
|
9
11
|
export * from './utils/optimization.js';
|
|
10
12
|
export * from './utils/reconstitution.js';
|
|
11
|
-
export
|
|
13
|
+
export * from './utils/robust-audio.js';
|
|
14
|
+
export * from './utils/robust-image.js';
|
|
15
|
+
export { encodeWithRustCLI, isRustBinaryAvailable } from './utils/rust-cli-wrapper.js';
|
|
12
16
|
export * from './utils/types.js';
|
|
13
17
|
export * from './utils/zstd.js';
|
|
14
18
|
export { packPaths, packPathsToParts, unpackBuffer } from './pack.js';
|
package/dist/pack.d.ts
CHANGED
package/dist/pack.js
CHANGED
|
@@ -58,6 +58,9 @@ export function packPaths(paths, baseDir, onProgress) {
|
|
|
58
58
|
export function unpackBuffer(buf, fileList) {
|
|
59
59
|
if (buf.length < 8)
|
|
60
60
|
return null;
|
|
61
|
+
if (isTar(buf)) {
|
|
62
|
+
return unpackTar(buf, fileList);
|
|
63
|
+
}
|
|
61
64
|
const magic = buf.readUInt32BE(0);
|
|
62
65
|
if (magic === 0x524f5849) {
|
|
63
66
|
const indexLen = buf.readUInt32BE(4);
|
|
@@ -65,9 +68,7 @@ export function unpackBuffer(buf, fileList) {
|
|
|
65
68
|
const index = JSON.parse(indexBuf.toString('utf8'));
|
|
66
69
|
const dataStart = 8 + indexLen;
|
|
67
70
|
const files = [];
|
|
68
|
-
const entriesToProcess = fileList
|
|
69
|
-
? index.filter((e) => fileList.includes(e.path))
|
|
70
|
-
: index;
|
|
71
|
+
const entriesToProcess = fileList ? index.filter((e) => fileList.includes(e.path)) : index;
|
|
71
72
|
for (const entry of entriesToProcess) {
|
|
72
73
|
const entryStart = dataStart + entry.offset;
|
|
73
74
|
let ptr = entryStart;
|
|
@@ -203,3 +204,51 @@ export async function packPathsGenerator(paths, baseDir, onProgress) {
|
|
|
203
204
|
}
|
|
204
205
|
return { index, stream: streamGenerator(), totalSize };
|
|
205
206
|
}
|
|
207
|
+
export function isTar(buf) {
|
|
208
|
+
if (buf.length < 263)
|
|
209
|
+
return false;
|
|
210
|
+
return buf.slice(257, 262).toString('ascii') === 'ustar';
|
|
211
|
+
}
|
|
212
|
+
function unpackTar(buf, fileList) {
|
|
213
|
+
const files = [];
|
|
214
|
+
let offset = 0;
|
|
215
|
+
while (offset + 512 <= buf.length) {
|
|
216
|
+
const header = buf.slice(offset, offset + 512);
|
|
217
|
+
let allZero = true;
|
|
218
|
+
for (let i = 0; i < 512; i++) {
|
|
219
|
+
if (header[i] !== 0) {
|
|
220
|
+
allZero = false;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (allZero)
|
|
225
|
+
break;
|
|
226
|
+
const nameRaw = header.slice(0, 100).toString('ascii');
|
|
227
|
+
const name = nameRaw.replace(/\0+$/, '');
|
|
228
|
+
const sizeOctal = header
|
|
229
|
+
.slice(124, 136)
|
|
230
|
+
.toString('ascii')
|
|
231
|
+
.replace(/\0+$/, '')
|
|
232
|
+
.trim();
|
|
233
|
+
const size = parseInt(sizeOctal, 8) || 0;
|
|
234
|
+
const typeFlag = header[156];
|
|
235
|
+
const prefix = header.slice(345, 500).toString('ascii').replace(/\0+$/, '');
|
|
236
|
+
const fullPath = prefix ? `${prefix}/${name}` : name;
|
|
237
|
+
const cleanPath = fullPath
|
|
238
|
+
.split('/')
|
|
239
|
+
.filter((c) => c && c !== '.' && c !== '..')
|
|
240
|
+
.join('/');
|
|
241
|
+
offset += 512;
|
|
242
|
+
if (typeFlag === 0 || typeFlag === 0x30) {
|
|
243
|
+
if (offset + size > buf.length)
|
|
244
|
+
break;
|
|
245
|
+
const content = buf.slice(offset, offset + size);
|
|
246
|
+
if (!fileList || fileList.includes(cleanPath)) {
|
|
247
|
+
files.push({ path: cleanPath, buf: Buffer.from(content) });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const blocks = Math.ceil(size / 512);
|
|
251
|
+
offset += blocks * 512;
|
|
252
|
+
}
|
|
253
|
+
return files.length > 0 ? { files } : null;
|
|
254
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAV container for binary data.
|
|
3
|
+
*
|
|
4
|
+
* Encodes raw bytes as 8-bit unsigned PCM mono samples (44100 Hz).
|
|
5
|
+
* Header is exactly 44 bytes. Total container overhead: 44 bytes (constant).
|
|
6
|
+
*
|
|
7
|
+
* Compared to PNG (stored deflate): PNG overhead grows with data size
|
|
8
|
+
* (zlib framing, filter bytes, chunk CRCs). WAV overhead is constant.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Pack raw bytes into a WAV file (8-bit PCM, mono, 44100 Hz).
|
|
12
|
+
* The bytes are stored directly as unsigned PCM samples.
|
|
13
|
+
*/
|
|
14
|
+
export declare function bytesToWav(data: Buffer): Buffer;
|
|
15
|
+
/**
|
|
16
|
+
* Extract raw bytes from a WAV file.
|
|
17
|
+
* Returns the PCM data (the original bytes).
|
|
18
|
+
*/
|
|
19
|
+
export declare function wavToBytes(wav: Buffer): Buffer;
|
|
20
|
+
/**
|
|
21
|
+
* Check if a buffer starts with a RIFF/WAVE header.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isWav(buf: Buffer): boolean;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAV container for binary data.
|
|
3
|
+
*
|
|
4
|
+
* Encodes raw bytes as 8-bit unsigned PCM mono samples (44100 Hz).
|
|
5
|
+
* Header is exactly 44 bytes. Total container overhead: 44 bytes (constant).
|
|
6
|
+
*
|
|
7
|
+
* Compared to PNG (stored deflate): PNG overhead grows with data size
|
|
8
|
+
* (zlib framing, filter bytes, chunk CRCs). WAV overhead is constant.
|
|
9
|
+
*/
|
|
10
|
+
const WAV_HEADER_SIZE = 44;
|
|
11
|
+
const SAMPLE_RATE = 44100;
|
|
12
|
+
const BITS_PER_SAMPLE = 8;
|
|
13
|
+
const NUM_CHANNELS = 1;
|
|
14
|
+
/**
|
|
15
|
+
* Pack raw bytes into a WAV file (8-bit PCM, mono, 44100 Hz).
|
|
16
|
+
* The bytes are stored directly as unsigned PCM samples.
|
|
17
|
+
*/
|
|
18
|
+
export function bytesToWav(data) {
|
|
19
|
+
const dataSize = data.length;
|
|
20
|
+
const fileSize = WAV_HEADER_SIZE - 8 + dataSize;
|
|
21
|
+
const byteRate = SAMPLE_RATE * NUM_CHANNELS * (BITS_PER_SAMPLE / 8);
|
|
22
|
+
const blockAlign = NUM_CHANNELS * (BITS_PER_SAMPLE / 8);
|
|
23
|
+
const wav = Buffer.alloc(WAV_HEADER_SIZE + dataSize);
|
|
24
|
+
let offset = 0;
|
|
25
|
+
// RIFF header
|
|
26
|
+
wav.write('RIFF', offset, 'ascii');
|
|
27
|
+
offset += 4;
|
|
28
|
+
wav.writeUInt32LE(fileSize, offset);
|
|
29
|
+
offset += 4;
|
|
30
|
+
wav.write('WAVE', offset, 'ascii');
|
|
31
|
+
offset += 4;
|
|
32
|
+
// fmt sub-chunk
|
|
33
|
+
wav.write('fmt ', offset, 'ascii');
|
|
34
|
+
offset += 4;
|
|
35
|
+
wav.writeUInt32LE(16, offset);
|
|
36
|
+
offset += 4; // sub-chunk size (PCM = 16)
|
|
37
|
+
wav.writeUInt16LE(1, offset);
|
|
38
|
+
offset += 2; // audio format (1 = PCM)
|
|
39
|
+
wav.writeUInt16LE(NUM_CHANNELS, offset);
|
|
40
|
+
offset += 2;
|
|
41
|
+
wav.writeUInt32LE(SAMPLE_RATE, offset);
|
|
42
|
+
offset += 4;
|
|
43
|
+
wav.writeUInt32LE(byteRate, offset);
|
|
44
|
+
offset += 4;
|
|
45
|
+
wav.writeUInt16LE(blockAlign, offset);
|
|
46
|
+
offset += 2;
|
|
47
|
+
wav.writeUInt16LE(BITS_PER_SAMPLE, offset);
|
|
48
|
+
offset += 2;
|
|
49
|
+
// data sub-chunk
|
|
50
|
+
wav.write('data', offset, 'ascii');
|
|
51
|
+
offset += 4;
|
|
52
|
+
wav.writeUInt32LE(dataSize, offset);
|
|
53
|
+
offset += 4;
|
|
54
|
+
data.copy(wav, offset);
|
|
55
|
+
return wav;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Extract raw bytes from a WAV file.
|
|
59
|
+
* Returns the PCM data (the original bytes).
|
|
60
|
+
*/
|
|
61
|
+
export function wavToBytes(wav) {
|
|
62
|
+
if (wav.length < WAV_HEADER_SIZE) {
|
|
63
|
+
throw new Error('WAV data too short');
|
|
64
|
+
}
|
|
65
|
+
if (wav.toString('ascii', 0, 4) !== 'RIFF') {
|
|
66
|
+
throw new Error('Not a RIFF file');
|
|
67
|
+
}
|
|
68
|
+
if (wav.toString('ascii', 8, 12) !== 'WAVE') {
|
|
69
|
+
throw new Error('Not a WAVE file');
|
|
70
|
+
}
|
|
71
|
+
// Find the "data" sub-chunk
|
|
72
|
+
let offset = 12;
|
|
73
|
+
while (offset + 8 <= wav.length) {
|
|
74
|
+
const chunkId = wav.toString('ascii', offset, offset + 4);
|
|
75
|
+
const chunkSize = wav.readUInt32LE(offset + 4);
|
|
76
|
+
if (chunkId === 'data') {
|
|
77
|
+
const dataStart = offset + 8;
|
|
78
|
+
const dataEnd = dataStart + chunkSize;
|
|
79
|
+
if (dataEnd > wav.length) {
|
|
80
|
+
return wav.subarray(dataStart);
|
|
81
|
+
}
|
|
82
|
+
return wav.subarray(dataStart, dataEnd);
|
|
83
|
+
}
|
|
84
|
+
offset += 8 + chunkSize;
|
|
85
|
+
if (chunkSize % 2 !== 0)
|
|
86
|
+
offset += 1; // RIFF word alignment
|
|
87
|
+
}
|
|
88
|
+
throw new Error('data chunk not found in WAV');
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Check if a buffer starts with a RIFF/WAVE header.
|
|
92
|
+
*/
|
|
93
|
+
export function isWav(buf) {
|
|
94
|
+
return (buf.length >= 12 &&
|
|
95
|
+
buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46 && // RIFF
|
|
96
|
+
buf[8] === 0x57 && buf[9] === 0x41 && buf[10] === 0x56 && buf[11] === 0x45 // WAVE
|
|
97
|
+
);
|
|
98
|
+
}
|
package/dist/utils/decoder.js
CHANGED
|
@@ -3,12 +3,18 @@ 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';
|
|
15
|
+
function isColorMatch(r1, g1, b1, r2, g2, b2) {
|
|
16
|
+
return Math.abs(r1 - r2) + Math.abs(g1 - g2) + Math.abs(b1 - b2) < 50;
|
|
17
|
+
}
|
|
12
18
|
async function tryDecompress(payload, onProgress) {
|
|
13
19
|
return await parallelZstdDecompress(payload, onProgress);
|
|
14
20
|
}
|
|
@@ -150,6 +156,107 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
150
156
|
}
|
|
151
157
|
if (opts.onProgress)
|
|
152
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) ──────────────────────────────────────────
|
|
153
260
|
if (processedBuf.subarray(0, MAGIC.length).equals(MAGIC)) {
|
|
154
261
|
const d = processedBuf.subarray(MAGIC.length);
|
|
155
262
|
const nameLen = d[0];
|
|
@@ -331,9 +438,7 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
331
438
|
if (firstPixels.length === MARKER_START.length) {
|
|
332
439
|
hasMarkerStart = true;
|
|
333
440
|
for (let i = 0; i < MARKER_START.length; i++) {
|
|
334
|
-
if (firstPixels[i].r
|
|
335
|
-
firstPixels[i].g !== MARKER_START[i].g ||
|
|
336
|
-
firstPixels[i].b !== MARKER_START[i].b) {
|
|
441
|
+
if (!isColorMatch(firstPixels[i].r, firstPixels[i].g, firstPixels[i].b, MARKER_START[i].r, MARKER_START[i].g, MARKER_START[i].b)) {
|
|
337
442
|
hasMarkerStart = false;
|
|
338
443
|
break;
|
|
339
444
|
}
|
|
@@ -422,9 +527,7 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
422
527
|
let match = true;
|
|
423
528
|
for (let mi = 0; mi < MARKER_START.length && match; mi++) {
|
|
424
529
|
const offset = (i + mi) * 3;
|
|
425
|
-
if (logicalData[offset]
|
|
426
|
-
logicalData[offset + 1] !== MARKER_START[mi].g ||
|
|
427
|
-
logicalData[offset + 2] !== MARKER_START[mi].b) {
|
|
530
|
+
if (!isColorMatch(logicalData[offset], logicalData[offset + 1], logicalData[offset + 2], MARKER_START[mi].r, MARKER_START[mi].g, MARKER_START[mi].b)) {
|
|
428
531
|
match = false;
|
|
429
532
|
}
|
|
430
533
|
}
|
|
@@ -445,9 +548,7 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
445
548
|
for (let mi = 0; mi < MARKER_START.length && match; mi++) {
|
|
446
549
|
const idx = (y * logicalWidth + (x + mi)) * 3;
|
|
447
550
|
if (idx + 2 >= logicalData.length ||
|
|
448
|
-
logicalData[idx]
|
|
449
|
-
logicalData[idx + 1] !== MARKER_START[mi].g ||
|
|
450
|
-
logicalData[idx + 2] !== MARKER_START[mi].b) {
|
|
551
|
+
!isColorMatch(logicalData[idx], logicalData[idx + 1], logicalData[idx + 2], MARKER_START[mi].r, MARKER_START[mi].g, MARKER_START[mi].b)) {
|
|
451
552
|
match = false;
|
|
452
553
|
}
|
|
453
554
|
}
|
|
@@ -540,9 +641,7 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
540
641
|
}
|
|
541
642
|
for (let i = 0; i < MARKER_START.length; i++) {
|
|
542
643
|
const offset = (startIdx + i) * 3;
|
|
543
|
-
if (logicalData[offset]
|
|
544
|
-
logicalData[offset + 1] !== MARKER_START[i].g ||
|
|
545
|
-
logicalData[offset + 2] !== MARKER_START[i].b) {
|
|
644
|
+
if (!isColorMatch(logicalData[offset], logicalData[offset + 1], logicalData[offset + 2], MARKER_START[i].r, MARKER_START[i].g, MARKER_START[i].b)) {
|
|
546
645
|
throw new Error('Marker START not found - image format not supported');
|
|
547
646
|
}
|
|
548
647
|
}
|
|
@@ -576,9 +675,7 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
576
675
|
break;
|
|
577
676
|
}
|
|
578
677
|
const offset = pixelIdx * 3;
|
|
579
|
-
if (logicalData[offset]
|
|
580
|
-
logicalData[offset + 1] !== MARKER_END[mi].g ||
|
|
581
|
-
logicalData[offset + 2] !== MARKER_END[mi].b) {
|
|
678
|
+
if (!isColorMatch(logicalData[offset], logicalData[offset + 1], logicalData[offset + 2], MARKER_END[mi].r, MARKER_END[mi].g, MARKER_END[mi].b)) {
|
|
582
679
|
matchEnd = false;
|
|
583
680
|
}
|
|
584
681
|
}
|
|
@@ -730,9 +827,7 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
730
827
|
for (let mi = 0; mi < MARKER_START.length && match; mi++) {
|
|
731
828
|
const idx = (y * logicalWidth2 + (x + mi)) * 3;
|
|
732
829
|
if (idx + 2 >= logicalData2.length ||
|
|
733
|
-
logicalData2[idx]
|
|
734
|
-
logicalData2[idx + 1] !== MARKER_START[mi].g ||
|
|
735
|
-
logicalData2[idx + 2] !== MARKER_START[mi].b) {
|
|
830
|
+
!isColorMatch(logicalData2[idx], logicalData2[idx + 1], logicalData2[idx + 2], MARKER_START[mi].r, MARKER_START[mi].g, MARKER_START[mi].b)) {
|
|
736
831
|
match = false;
|
|
737
832
|
}
|
|
738
833
|
}
|
|
@@ -809,9 +904,7 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
809
904
|
break;
|
|
810
905
|
}
|
|
811
906
|
const offset = pixelIdx * 3;
|
|
812
|
-
if (logicalData2[offset]
|
|
813
|
-
logicalData2[offset + 1] !== MARKER_END[mi].g ||
|
|
814
|
-
logicalData2[offset + 2] !== MARKER_END[mi].b) {
|
|
907
|
+
if (!isColorMatch(logicalData2[offset], logicalData2[offset + 1], logicalData2[offset + 2], MARKER_END[mi].r, MARKER_END[mi].g, MARKER_END[mi].b)) {
|
|
815
908
|
matchEnd2 = false;
|
|
816
909
|
}
|
|
817
910
|
}
|