roxify 1.1.5 → 1.1.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 +30 -6
- package/dist/cli.js +105 -32
- package/dist/index.d.ts +4 -0
- package/dist/index.js +386 -54
- package/dist/pack.d.ts +10 -0
- package/dist/pack.js +59 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ if (result.files) {
|
|
|
84
84
|
|
|
85
85
|
## Example: Progress Logging
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
````js
|
|
88
88
|
import { encodeBinaryToPng, decodePngToBinary } from 'roxify';
|
|
89
89
|
|
|
90
90
|
const data = Buffer.from('Large data to encode...');
|
|
@@ -100,13 +100,37 @@ const png = await encodeBinaryToPng(data, {
|
|
|
100
100
|
},
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
|
|
104
|
+
Node.js (detailed chunk progress example)
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
import { encodeBinaryToPng } from 'roxify';
|
|
108
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
109
|
+
|
|
110
|
+
const buf = readFileSync('test-data/testVideo.mp4');
|
|
111
|
+
const png = await encodeBinaryToPng(buf, {
|
|
112
|
+
showProgress: false,
|
|
113
|
+
onProgress(info) {
|
|
114
|
+
if (info.phase === 'compress_progress' && info.loaded && info.total) {
|
|
115
|
+
const percent = Math.round((info.loaded / info.total) * 100);
|
|
116
|
+
console.log(`[progress] ${info.phase} ${percent}% (${info.loaded}/${info.total} chunks)`);
|
|
117
|
+
} else {
|
|
118
|
+
console.log(`[progress] ${info.phase} ${info.loaded || ''}/${info.total || ''}`);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
writeFileSync('out.png', png);
|
|
124
|
+
````
|
|
125
|
+
|
|
103
126
|
// Decode with progress logging
|
|
104
127
|
const { buf } = await decodePngToBinary(png, {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
128
|
+
onProgress: (info) => {
|
|
129
|
+
console.log(`Decoding phase: ${info.phase}`);
|
|
130
|
+
},
|
|
108
131
|
});
|
|
109
|
-
|
|
132
|
+
|
|
133
|
+
````
|
|
110
134
|
|
|
111
135
|
**API:**
|
|
112
136
|
|
|
@@ -160,7 +184,7 @@ const result = await decodePngToBinary(png, { files: ['myfolder/file.txt'] });
|
|
|
160
184
|
if (result.files) {
|
|
161
185
|
fs.writeFileSync('extracted.txt', result.files[0].buf);
|
|
162
186
|
}
|
|
163
|
-
|
|
187
|
+
````
|
|
164
188
|
|
|
165
189
|
## Requirements
|
|
166
190
|
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
|
4
4
|
import { basename, dirname, join, resolve } from 'path';
|
|
5
5
|
import { DataFormatError, decodePngToBinary, encodeBinaryToPng, IncorrectPassphraseError, listFilesInPng, PassphraseRequiredError, } from './index.js';
|
|
6
6
|
import { packPaths, unpackBuffer } from './pack.js';
|
|
7
|
-
const VERSION = '1.1.
|
|
7
|
+
const VERSION = '1.1.7';
|
|
8
8
|
function showHelp() {
|
|
9
9
|
console.log(`
|
|
10
10
|
ROX CLI — Encode/decode binary in PNG
|
|
@@ -127,16 +127,20 @@ async function encodeCommand(args) {
|
|
|
127
127
|
: undefined;
|
|
128
128
|
const firstInput = inputPaths[0];
|
|
129
129
|
if (!firstInput) {
|
|
130
|
+
console.log(' ');
|
|
130
131
|
console.error('Error: Input file required');
|
|
131
132
|
console.log('Usage: npx rox encode <input> [output] [options]');
|
|
132
133
|
process.exit(1);
|
|
133
134
|
}
|
|
134
135
|
const resolvedInputs = inputPaths.map((p) => resolve(p));
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
(
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
let outputName = inputPaths.length === 1 ? basename(firstInput) : 'archive';
|
|
137
|
+
if (inputPaths.length === 1 && !statSync(resolvedInputs[0]).isDirectory()) {
|
|
138
|
+
outputName = outputName.replace(/(\.[^.]+)?$/, '.png');
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
outputName += '.png';
|
|
142
|
+
}
|
|
143
|
+
const resolvedOutput = parsed.output || outputPath || outputName;
|
|
140
144
|
let options = {};
|
|
141
145
|
try {
|
|
142
146
|
let inputBuffer;
|
|
@@ -173,13 +177,10 @@ async function encodeCommand(args) {
|
|
|
173
177
|
}
|
|
174
178
|
else {
|
|
175
179
|
const resolvedInput = resolvedInputs[0];
|
|
176
|
-
console.log(' ');
|
|
177
|
-
console.log(`Reading: ${resolvedInput}`);
|
|
178
|
-
console.log(' ');
|
|
179
180
|
const st = statSync(resolvedInput);
|
|
180
181
|
if (st.isDirectory()) {
|
|
181
182
|
console.log(`Packing directory...`);
|
|
182
|
-
const packResult = packPaths([resolvedInput]);
|
|
183
|
+
const packResult = packPaths([resolvedInput], resolvedInput);
|
|
183
184
|
inputBuffer = packResult.buf;
|
|
184
185
|
console.log(`Packed ${packResult.list.length} files -> ${(inputBuffer.length /
|
|
185
186
|
1024 /
|
|
@@ -218,30 +219,59 @@ async function encodeCommand(args) {
|
|
|
218
219
|
elapsed: '0',
|
|
219
220
|
});
|
|
220
221
|
const startEncode = Date.now();
|
|
222
|
+
let currentEncodePct = 0;
|
|
223
|
+
let currentEncodeStep = 'Starting';
|
|
224
|
+
const encodeHeartbeat = setInterval(() => {
|
|
225
|
+
encodeBar.update(Math.floor(currentEncodePct), {
|
|
226
|
+
step: currentEncodeStep,
|
|
227
|
+
elapsed: String(Math.floor((Date.now() - startEncode) / 1000)),
|
|
228
|
+
});
|
|
229
|
+
}, 1000);
|
|
221
230
|
options.onProgress = (info) => {
|
|
222
231
|
let pct = 0;
|
|
223
|
-
|
|
224
|
-
|
|
232
|
+
let stepLabel = 'Processing';
|
|
233
|
+
if (info.phase === 'compress_start') {
|
|
234
|
+
pct = 5;
|
|
235
|
+
stepLabel = 'Compressing';
|
|
236
|
+
}
|
|
237
|
+
else if (info.phase === 'compress_progress') {
|
|
238
|
+
pct = 5 + Math.floor((info.loaded / info.total) * 45);
|
|
239
|
+
stepLabel = 'Compressing';
|
|
225
240
|
}
|
|
226
241
|
else if (info.phase === 'compress_done') {
|
|
227
242
|
pct = 50;
|
|
243
|
+
stepLabel = 'Compressed';
|
|
244
|
+
}
|
|
245
|
+
else if (info.phase === 'encrypt_start') {
|
|
246
|
+
pct = 60;
|
|
247
|
+
stepLabel = 'Encrypting';
|
|
228
248
|
}
|
|
229
249
|
else if (info.phase === 'encrypt_done') {
|
|
250
|
+
pct = 75;
|
|
251
|
+
stepLabel = 'Encrypted';
|
|
252
|
+
}
|
|
253
|
+
else if (info.phase === 'meta_prep_done') {
|
|
230
254
|
pct = 80;
|
|
255
|
+
stepLabel = 'Preparing';
|
|
231
256
|
}
|
|
232
257
|
else if (info.phase === 'png_gen') {
|
|
233
258
|
pct = 90;
|
|
259
|
+
stepLabel = 'Generating PNG';
|
|
234
260
|
}
|
|
235
261
|
else if (info.phase === 'done') {
|
|
236
262
|
pct = 100;
|
|
263
|
+
stepLabel = 'Done';
|
|
237
264
|
}
|
|
265
|
+
currentEncodePct = pct;
|
|
266
|
+
currentEncodeStep = stepLabel;
|
|
238
267
|
encodeBar.update(Math.floor(pct), {
|
|
239
|
-
step:
|
|
268
|
+
step: stepLabel,
|
|
240
269
|
elapsed: String(Math.floor((Date.now() - startEncode) / 1000)),
|
|
241
270
|
});
|
|
242
271
|
};
|
|
243
272
|
const output = await encodeBinaryToPng(inputBuffer, options);
|
|
244
273
|
const encodeTime = Date.now() - startEncode;
|
|
274
|
+
clearInterval(encodeHeartbeat);
|
|
245
275
|
encodeBar.update(100, {
|
|
246
276
|
step: 'done',
|
|
247
277
|
elapsed: String(Math.floor(encodeTime / 1000)),
|
|
@@ -259,6 +289,7 @@ async function encodeCommand(args) {
|
|
|
259
289
|
console.log(' ');
|
|
260
290
|
}
|
|
261
291
|
catch (err) {
|
|
292
|
+
console.log(' ');
|
|
262
293
|
console.error('Error: Failed to encode file. Use --verbose for details.');
|
|
263
294
|
if (parsed.verbose)
|
|
264
295
|
console.error('Details:', err.stack || err.message);
|
|
@@ -269,15 +300,14 @@ async function decodeCommand(args) {
|
|
|
269
300
|
const parsed = parseArgs(args);
|
|
270
301
|
const [inputPath, outputPath] = parsed._;
|
|
271
302
|
if (!inputPath) {
|
|
303
|
+
console.log(' ');
|
|
272
304
|
console.error('Error: Input PNG file required');
|
|
273
305
|
console.log('Usage: npx rox decode <input> [output] [options]');
|
|
274
306
|
process.exit(1);
|
|
275
307
|
}
|
|
276
308
|
const resolvedInput = resolve(inputPath);
|
|
309
|
+
const resolvedOutput = parsed.output || outputPath || 'decoded.bin';
|
|
277
310
|
try {
|
|
278
|
-
const inputBuffer = readFileSync(resolvedInput);
|
|
279
|
-
console.log(' ');
|
|
280
|
-
console.log(`Reading: ${resolvedInput}`);
|
|
281
311
|
const options = {};
|
|
282
312
|
if (parsed.passphrase) {
|
|
283
313
|
options.passphrase = parsed.passphrase;
|
|
@@ -289,7 +319,6 @@ async function decodeCommand(args) {
|
|
|
289
319
|
options.files = parsed.files;
|
|
290
320
|
}
|
|
291
321
|
console.log(' ');
|
|
292
|
-
console.log(' ');
|
|
293
322
|
console.log(`Decoding...`);
|
|
294
323
|
console.log(' ');
|
|
295
324
|
const decodeBar = new cliProgress.SingleBar({
|
|
@@ -300,24 +329,49 @@ async function decodeCommand(args) {
|
|
|
300
329
|
elapsed: '0',
|
|
301
330
|
});
|
|
302
331
|
const startDecode = Date.now();
|
|
332
|
+
let currentPct = 50;
|
|
333
|
+
let currentStep = 'Decoding';
|
|
334
|
+
const heartbeat = setInterval(() => {
|
|
335
|
+
decodeBar.update(Math.floor(currentPct), {
|
|
336
|
+
step: currentStep,
|
|
337
|
+
elapsed: String(Math.floor((Date.now() - startDecode) / 1000)),
|
|
338
|
+
});
|
|
339
|
+
}, 1000);
|
|
303
340
|
options.onProgress = (info) => {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
341
|
+
if (info.phase === 'decompress_start') {
|
|
342
|
+
currentPct = 50;
|
|
343
|
+
currentStep = 'Decompressing';
|
|
344
|
+
}
|
|
345
|
+
else if (info.phase === 'decompress_progress' &&
|
|
346
|
+
info.loaded &&
|
|
347
|
+
info.total) {
|
|
348
|
+
currentPct = 50 + Math.floor((info.loaded / info.total) * 40);
|
|
349
|
+
currentStep = `Decompressing (${info.loaded}/${info.total})`;
|
|
350
|
+
}
|
|
351
|
+
else if (info.phase === 'decompress_done') {
|
|
352
|
+
currentPct = 90;
|
|
353
|
+
currentStep = 'Decompressed';
|
|
354
|
+
}
|
|
355
|
+
else if (info.phase === 'done') {
|
|
356
|
+
currentPct = 100;
|
|
357
|
+
currentStep = 'Done';
|
|
358
|
+
}
|
|
359
|
+
decodeBar.update(Math.floor(currentPct), {
|
|
360
|
+
step: currentStep,
|
|
309
361
|
elapsed: String(Math.floor((Date.now() - startDecode) / 1000)),
|
|
310
362
|
});
|
|
311
363
|
};
|
|
364
|
+
const inputBuffer = readFileSync(resolvedInput);
|
|
312
365
|
const result = await decodePngToBinary(inputBuffer, options);
|
|
313
366
|
const decodeTime = Date.now() - startDecode;
|
|
367
|
+
clearInterval(heartbeat);
|
|
314
368
|
decodeBar.update(100, {
|
|
315
369
|
step: 'done',
|
|
316
370
|
elapsed: String(Math.floor(decodeTime / 1000)),
|
|
317
371
|
});
|
|
318
372
|
decodeBar.stop();
|
|
319
373
|
if (result.files) {
|
|
320
|
-
const baseDir = parsed.output || outputPath || '
|
|
374
|
+
const baseDir = parsed.output || outputPath || '.';
|
|
321
375
|
for (const file of result.files) {
|
|
322
376
|
const fullPath = join(baseDir, file.path);
|
|
323
377
|
const dir = dirname(fullPath);
|
|
@@ -325,42 +379,54 @@ async function decodeCommand(args) {
|
|
|
325
379
|
writeFileSync(fullPath, file.buf);
|
|
326
380
|
}
|
|
327
381
|
console.log(`\nSuccess!`);
|
|
328
|
-
console.log(`
|
|
382
|
+
console.log(`Unpacked ${result.files.length} files to directory : ${resolve(baseDir)}`);
|
|
329
383
|
console.log(`Time: ${decodeTime}ms`);
|
|
330
384
|
}
|
|
331
385
|
else if (result.buf) {
|
|
332
386
|
const unpacked = unpackBuffer(result.buf);
|
|
333
387
|
if (unpacked) {
|
|
334
|
-
const baseDir = parsed.output || outputPath ||
|
|
388
|
+
const baseDir = parsed.output || outputPath || '.';
|
|
335
389
|
for (const file of unpacked.files) {
|
|
336
390
|
const fullPath = join(baseDir, file.path);
|
|
337
391
|
const dir = dirname(fullPath);
|
|
338
392
|
mkdirSync(dir, { recursive: true });
|
|
339
393
|
writeFileSync(fullPath, file.buf);
|
|
340
394
|
}
|
|
341
|
-
console.log(
|
|
395
|
+
console.log(`\nSuccess!`);
|
|
396
|
+
console.log(`Time: ${decodeTime}ms`);
|
|
397
|
+
console.log(`Unpacked ${unpacked.files.length} files to current directory`);
|
|
342
398
|
}
|
|
343
399
|
else {
|
|
344
|
-
|
|
345
|
-
|
|
400
|
+
let finalOutput = resolvedOutput;
|
|
401
|
+
if (!parsed.output && !outputPath && result.meta?.name) {
|
|
402
|
+
finalOutput = result.meta.name;
|
|
403
|
+
}
|
|
404
|
+
writeFileSync(finalOutput, result.buf);
|
|
346
405
|
console.log(`\nSuccess!`);
|
|
347
406
|
if (result.meta?.name) {
|
|
348
407
|
console.log(` Original name: ${result.meta.name}`);
|
|
349
408
|
}
|
|
350
|
-
|
|
409
|
+
const outputSize = (result.buf.length / 1024 / 1024).toFixed(2);
|
|
410
|
+
console.log(` Output size: ${outputSize} MB`);
|
|
351
411
|
console.log(` Time: ${decodeTime}ms`);
|
|
352
|
-
console.log(` Saved: ${
|
|
412
|
+
console.log(` Saved: ${finalOutput}`);
|
|
353
413
|
}
|
|
354
414
|
}
|
|
415
|
+
else {
|
|
416
|
+
console.log(`\nSuccess!`);
|
|
417
|
+
console.log(`Time: ${decodeTime}ms`);
|
|
418
|
+
}
|
|
355
419
|
console.log(' ');
|
|
356
420
|
}
|
|
357
421
|
catch (err) {
|
|
358
422
|
if (err instanceof PassphraseRequiredError ||
|
|
359
423
|
(err.message && err.message.includes('passphrase') && !parsed.passphrase)) {
|
|
424
|
+
console.log(' ');
|
|
360
425
|
console.error('File appears to be encrypted. Provide a passphrase with -p');
|
|
361
426
|
}
|
|
362
427
|
else if (err instanceof IncorrectPassphraseError ||
|
|
363
428
|
(err.message && err.message.includes('Incorrect passphrase'))) {
|
|
429
|
+
console.log(' ');
|
|
364
430
|
console.error('Incorrect passphrase');
|
|
365
431
|
}
|
|
366
432
|
else if (err instanceof DataFormatError ||
|
|
@@ -370,13 +436,16 @@ async function decodeCommand(args) {
|
|
|
370
436
|
err.message.includes('Pixel payload truncated') ||
|
|
371
437
|
err.message.includes('Marker START not found') ||
|
|
372
438
|
err.message.includes('Brotli decompression failed')))) {
|
|
439
|
+
console.log(' ');
|
|
373
440
|
console.error('Data corrupted or unsupported format. Use --verbose for details.');
|
|
374
441
|
}
|
|
375
442
|
else {
|
|
443
|
+
console.log(' ');
|
|
376
444
|
console.error('Failed to decode file. Use --verbose for details.');
|
|
377
445
|
}
|
|
378
|
-
if (parsed.verbose)
|
|
446
|
+
if (parsed.verbose) {
|
|
379
447
|
console.error('Details:', err.stack || err.message);
|
|
448
|
+
}
|
|
380
449
|
process.exit(1);
|
|
381
450
|
}
|
|
382
451
|
}
|
|
@@ -384,6 +453,7 @@ async function listCommand(args) {
|
|
|
384
453
|
const parsed = parseArgs(args);
|
|
385
454
|
const [inputPath] = parsed._;
|
|
386
455
|
if (!inputPath) {
|
|
456
|
+
console.log(' ');
|
|
387
457
|
console.error('Error: Input PNG file required');
|
|
388
458
|
console.log('Usage: npx rox list <input>');
|
|
389
459
|
process.exit(1);
|
|
@@ -403,9 +473,11 @@ async function listCommand(args) {
|
|
|
403
473
|
}
|
|
404
474
|
}
|
|
405
475
|
catch (err) {
|
|
476
|
+
console.log(' ');
|
|
406
477
|
console.error('Failed to list files. Use --verbose for details.');
|
|
407
|
-
if (parsed.verbose)
|
|
478
|
+
if (parsed.verbose) {
|
|
408
479
|
console.error('Details:', err.stack || err.message);
|
|
480
|
+
}
|
|
409
481
|
process.exit(1);
|
|
410
482
|
}
|
|
411
483
|
}
|
|
@@ -438,6 +510,7 @@ async function main() {
|
|
|
438
510
|
}
|
|
439
511
|
}
|
|
440
512
|
main().catch((err) => {
|
|
513
|
+
console.log(' ');
|
|
441
514
|
console.error('Fatal error:', err);
|
|
442
515
|
process.exit(1);
|
|
443
516
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -127,6 +127,10 @@ export interface DecodeOptions {
|
|
|
127
127
|
* Directory to save debug images (doubled.png, reconstructed.png).
|
|
128
128
|
*/
|
|
129
129
|
debugDir?: string;
|
|
130
|
+
/**
|
|
131
|
+
* Path to write decoded output directly to disk (streamed) to avoid high memory usage.
|
|
132
|
+
*/
|
|
133
|
+
outPath?: string;
|
|
130
134
|
/**
|
|
131
135
|
* List of files to extract selectively from archives.
|
|
132
136
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { compress as zstdCompress, decompress as zstdDecompress, } from '@mongodb-js/zstd';
|
|
2
2
|
import cliProgress from 'cli-progress';
|
|
3
3
|
import { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes, } from 'crypto';
|
|
4
|
+
import { createReadStream, createWriteStream, readFileSync, unlinkSync, } from 'fs';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
4
6
|
import { join } from 'path';
|
|
5
7
|
import encode from 'png-chunks-encode';
|
|
6
8
|
import extract from 'png-chunks-extract';
|
|
@@ -42,6 +44,15 @@ const MARKER_COLORS = [
|
|
|
42
44
|
];
|
|
43
45
|
const MARKER_START = MARKER_COLORS;
|
|
44
46
|
const MARKER_END = [...MARKER_COLORS].reverse();
|
|
47
|
+
const CHUNK_SIZE = 8 * 1024;
|
|
48
|
+
async function writeInChunks(ws, buf, chunkSize = CHUNK_SIZE) {
|
|
49
|
+
for (let i = 0; i < buf.length; i += chunkSize) {
|
|
50
|
+
const part = buf.slice(i, i + chunkSize);
|
|
51
|
+
if (!ws.write(part))
|
|
52
|
+
await new Promise((resolve) => ws.once('drain', resolve));
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
45
56
|
const COMPRESSION_MARKERS = {
|
|
46
57
|
zstd: [{ r: 0, g: 255, b: 0 }],
|
|
47
58
|
};
|
|
@@ -54,6 +65,144 @@ function colorsToBytes(colors) {
|
|
|
54
65
|
}
|
|
55
66
|
return buf;
|
|
56
67
|
}
|
|
68
|
+
function deltaEncode(data) {
|
|
69
|
+
if (data.length === 0)
|
|
70
|
+
return data;
|
|
71
|
+
const out = Buffer.alloc(data.length);
|
|
72
|
+
out[0] = data[0];
|
|
73
|
+
for (let i = 1; i < data.length; i++) {
|
|
74
|
+
out[i] = (data[i] - data[i - 1] + 256) & 0xff;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
function deltaDecode(data) {
|
|
79
|
+
if (data.length === 0)
|
|
80
|
+
return data;
|
|
81
|
+
const out = Buffer.alloc(data.length);
|
|
82
|
+
out[0] = data[0];
|
|
83
|
+
for (let i = 1; i < data.length; i++) {
|
|
84
|
+
out[i] = (out[i - 1] + data[i]) & 0xff;
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
async function parallelZstdCompress(payload, level = 22, onProgress) {
|
|
89
|
+
const chunkSize = 1024 * 1024 * 1024;
|
|
90
|
+
if (payload.length <= chunkSize) {
|
|
91
|
+
return Buffer.from(await zstdCompress(payload, level));
|
|
92
|
+
}
|
|
93
|
+
const chunks = [];
|
|
94
|
+
const promises = [];
|
|
95
|
+
const totalChunks = Math.ceil(payload.length / chunkSize);
|
|
96
|
+
let completedChunks = 0;
|
|
97
|
+
for (let i = 0; i < payload.length; i += chunkSize) {
|
|
98
|
+
const chunk = payload.slice(i, Math.min(i + chunkSize, payload.length));
|
|
99
|
+
promises.push(zstdCompress(chunk, level).then((compressed) => {
|
|
100
|
+
completedChunks++;
|
|
101
|
+
if (onProgress) {
|
|
102
|
+
onProgress(completedChunks, totalChunks);
|
|
103
|
+
}
|
|
104
|
+
return Buffer.from(compressed);
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
const compressedChunks = await Promise.all(promises);
|
|
108
|
+
const chunkSizes = Buffer.alloc(compressedChunks.length * 4);
|
|
109
|
+
for (let i = 0; i < compressedChunks.length; i++) {
|
|
110
|
+
chunkSizes.writeUInt32BE(compressedChunks[i].length, i * 4);
|
|
111
|
+
}
|
|
112
|
+
const header = Buffer.alloc(8);
|
|
113
|
+
header.writeUInt32BE(0x5a535444, 0);
|
|
114
|
+
header.writeUInt32BE(compressedChunks.length, 4);
|
|
115
|
+
return Buffer.concat([header, chunkSizes, ...compressedChunks]);
|
|
116
|
+
}
|
|
117
|
+
async function parallelZstdDecompress(payload, onProgress, onChunk, outPath) {
|
|
118
|
+
if (payload.length < 8) {
|
|
119
|
+
onProgress?.({ phase: 'decompress_start', total: 1 });
|
|
120
|
+
const d = Buffer.from(await zstdDecompress(payload));
|
|
121
|
+
if (onChunk)
|
|
122
|
+
await onChunk(d, 0, 1);
|
|
123
|
+
onProgress?.({ phase: 'decompress_progress', loaded: 1, total: 1 });
|
|
124
|
+
onProgress?.({ phase: 'decompress_done', loaded: 1, total: 1 });
|
|
125
|
+
if (outPath) {
|
|
126
|
+
const ws = createWriteStream(outPath);
|
|
127
|
+
await writeInChunks(ws, d);
|
|
128
|
+
ws.end();
|
|
129
|
+
}
|
|
130
|
+
return onChunk ? Buffer.alloc(0) : d;
|
|
131
|
+
}
|
|
132
|
+
const magic = payload.readUInt32BE(0);
|
|
133
|
+
if (magic !== 0x5a535444) {
|
|
134
|
+
onProgress?.({ phase: 'decompress_start', total: 1 });
|
|
135
|
+
const d = Buffer.from(await zstdDecompress(payload));
|
|
136
|
+
onProgress?.({ phase: 'decompress_progress', loaded: 1, total: 1 });
|
|
137
|
+
onProgress?.({ phase: 'decompress_done', loaded: 1, total: 1 });
|
|
138
|
+
if (outPath) {
|
|
139
|
+
const ws = createWriteStream(outPath);
|
|
140
|
+
await writeInChunks(ws, d);
|
|
141
|
+
ws.end();
|
|
142
|
+
}
|
|
143
|
+
return d;
|
|
144
|
+
}
|
|
145
|
+
const numChunks = payload.readUInt32BE(4);
|
|
146
|
+
const chunkSizes = [];
|
|
147
|
+
let offset = 8;
|
|
148
|
+
for (let i = 0; i < numChunks; i++) {
|
|
149
|
+
chunkSizes.push(payload.readUInt32BE(offset));
|
|
150
|
+
offset += 4;
|
|
151
|
+
}
|
|
152
|
+
onProgress?.({ phase: 'decompress_start', total: numChunks });
|
|
153
|
+
const tempFiles = [];
|
|
154
|
+
for (let i = 0; i < numChunks; i++) {
|
|
155
|
+
const size = chunkSizes[i];
|
|
156
|
+
const chunk = payload.slice(offset, offset + size);
|
|
157
|
+
offset += size;
|
|
158
|
+
const tempFile = join(tmpdir(), `rox_chunk_${Date.now()}_${i}.tmp`);
|
|
159
|
+
const wsChunk = createWriteStream(tempFile);
|
|
160
|
+
const dec = Buffer.from(await zstdDecompress(chunk));
|
|
161
|
+
if (onChunk) {
|
|
162
|
+
await onChunk(dec, i + 1, numChunks);
|
|
163
|
+
unlinkSync(tempFile);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
await writeInChunks(wsChunk, dec);
|
|
167
|
+
await new Promise((res) => wsChunk.end(() => res()));
|
|
168
|
+
tempFiles.push(tempFile);
|
|
169
|
+
}
|
|
170
|
+
onProgress?.({
|
|
171
|
+
phase: 'decompress_progress',
|
|
172
|
+
loaded: i + 1,
|
|
173
|
+
total: numChunks,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
onProgress?.({
|
|
177
|
+
phase: 'decompress_done',
|
|
178
|
+
loaded: numChunks,
|
|
179
|
+
total: numChunks,
|
|
180
|
+
});
|
|
181
|
+
if (onChunk) {
|
|
182
|
+
return Buffer.alloc(0);
|
|
183
|
+
}
|
|
184
|
+
if (outPath || tempFiles.length > 0) {
|
|
185
|
+
const finalPath = outPath || join(tmpdir(), `rox_final_${Date.now()}.tmp`);
|
|
186
|
+
const ws = createWriteStream(finalPath);
|
|
187
|
+
for (const tempFile of tempFiles) {
|
|
188
|
+
const rs = createReadStream(tempFile);
|
|
189
|
+
await new Promise((resolve, reject) => {
|
|
190
|
+
rs.on('data', (chunk) => ws.write(chunk));
|
|
191
|
+
rs.on('end', resolve);
|
|
192
|
+
rs.on('error', reject);
|
|
193
|
+
});
|
|
194
|
+
unlinkSync(tempFile);
|
|
195
|
+
}
|
|
196
|
+
await new Promise((res) => ws.end(() => res()));
|
|
197
|
+
if (!outPath) {
|
|
198
|
+
const finalBuf = readFileSync(finalPath);
|
|
199
|
+
unlinkSync(finalPath);
|
|
200
|
+
return finalBuf;
|
|
201
|
+
}
|
|
202
|
+
return Buffer.alloc(0);
|
|
203
|
+
}
|
|
204
|
+
return Buffer.alloc(0);
|
|
205
|
+
}
|
|
57
206
|
function applyXor(buf, passphrase) {
|
|
58
207
|
const key = Buffer.from(passphrase, 'utf8');
|
|
59
208
|
const out = Buffer.alloc(buf.length);
|
|
@@ -65,14 +214,8 @@ function applyXor(buf, passphrase) {
|
|
|
65
214
|
function tryBrotliDecompress(payload) {
|
|
66
215
|
return Buffer.from(zlib.brotliDecompressSync(payload));
|
|
67
216
|
}
|
|
68
|
-
async function tryZstdDecompress(payload) {
|
|
69
|
-
|
|
70
|
-
const result = await zstdDecompress(payload);
|
|
71
|
-
return Buffer.from(result);
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
return payload;
|
|
75
|
-
}
|
|
217
|
+
async function tryZstdDecompress(payload, onProgress, onChunk, outPath) {
|
|
218
|
+
return await parallelZstdDecompress(payload, onProgress, onChunk, outPath);
|
|
76
219
|
}
|
|
77
220
|
function tryDecryptIfNeeded(buf, passphrase) {
|
|
78
221
|
if (!buf || buf.length === 0)
|
|
@@ -434,20 +577,17 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
434
577
|
const compression = opts.compression || 'zstd';
|
|
435
578
|
if (opts.onProgress)
|
|
436
579
|
opts.onProgress({ phase: 'compress_start', total: payload.length });
|
|
437
|
-
const
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const compressedChunk = Buffer.from(await zstdCompress(chunk, 22));
|
|
442
|
-
compressedChunks.push(compressedChunk);
|
|
443
|
-
if (opts.onProgress)
|
|
580
|
+
const useDelta = mode !== 'screenshot';
|
|
581
|
+
const deltaEncoded = useDelta ? deltaEncode(payload) : payload;
|
|
582
|
+
payload = await parallelZstdCompress(deltaEncoded, 1, (loaded, total) => {
|
|
583
|
+
if (opts.onProgress) {
|
|
444
584
|
opts.onProgress({
|
|
445
585
|
phase: 'compress_progress',
|
|
446
|
-
loaded
|
|
447
|
-
total
|
|
586
|
+
loaded,
|
|
587
|
+
total,
|
|
448
588
|
});
|
|
449
|
-
|
|
450
|
-
|
|
589
|
+
}
|
|
590
|
+
});
|
|
451
591
|
if (opts.onProgress)
|
|
452
592
|
opts.onProgress({ phase: 'compress_done', loaded: payload.length });
|
|
453
593
|
if (opts.passphrase && !opts.encrypt) {
|
|
@@ -480,6 +620,8 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
480
620
|
const enc = Buffer.concat([cipher.update(payload), cipher.final()]);
|
|
481
621
|
const tag = cipher.getAuthTag();
|
|
482
622
|
payload = Buffer.concat([Buffer.from([ENC_AES]), salt, iv, tag, enc]);
|
|
623
|
+
if (opts.onProgress)
|
|
624
|
+
opts.onProgress({ phase: 'encrypt_done' });
|
|
483
625
|
}
|
|
484
626
|
else if (encChoice === 'xor') {
|
|
485
627
|
const xored = applyXor(payload, opts.passphrase);
|
|
@@ -520,7 +662,7 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
520
662
|
const nameLen = nameBuf.length;
|
|
521
663
|
const payloadLenBuf = Buffer.alloc(4);
|
|
522
664
|
payloadLenBuf.writeUInt32BE(payload.length, 0);
|
|
523
|
-
const version =
|
|
665
|
+
const version = 1;
|
|
524
666
|
const metaPixel = Buffer.concat([
|
|
525
667
|
Buffer.from([version]),
|
|
526
668
|
Buffer.from([nameLen]),
|
|
@@ -542,15 +684,21 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
542
684
|
]);
|
|
543
685
|
const bytesPerPixel = 3;
|
|
544
686
|
const dataPixels = Math.ceil(dataWithMarkers.length / 3);
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
687
|
+
const totalPixels = dataPixels + MARKER_END.length;
|
|
688
|
+
const maxWidth = 16384;
|
|
689
|
+
let side = Math.ceil(Math.sqrt(totalPixels));
|
|
690
|
+
if (side < MARKER_END.length)
|
|
691
|
+
side = MARKER_END.length;
|
|
692
|
+
let logicalWidth;
|
|
693
|
+
let logicalHeight;
|
|
694
|
+
if (side <= maxWidth) {
|
|
695
|
+
logicalWidth = side;
|
|
696
|
+
logicalHeight = side;
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
logicalWidth = Math.min(maxWidth, totalPixels);
|
|
700
|
+
logicalHeight = Math.ceil(totalPixels / logicalWidth);
|
|
701
|
+
}
|
|
554
702
|
const scale = 1;
|
|
555
703
|
const width = logicalWidth * scale;
|
|
556
704
|
const height = logicalHeight * scale;
|
|
@@ -566,8 +714,7 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
566
714
|
g = MARKER_END[markerIdx].g;
|
|
567
715
|
b = MARKER_END[markerIdx].b;
|
|
568
716
|
}
|
|
569
|
-
else if (
|
|
570
|
-
(ly === dataRows && linearIdx < dataPixels)) {
|
|
717
|
+
else if (linearIdx < dataPixels) {
|
|
571
718
|
const srcIdx = linearIdx * 3;
|
|
572
719
|
r = srcIdx < dataWithMarkers.length ? dataWithMarkers[srcIdx] : 0;
|
|
573
720
|
g =
|
|
@@ -593,7 +740,9 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
593
740
|
}
|
|
594
741
|
if (opts.onProgress)
|
|
595
742
|
opts.onProgress({ phase: 'png_gen' });
|
|
596
|
-
let bufScr = await sharp(raw, {
|
|
743
|
+
let bufScr = await sharp(raw, {
|
|
744
|
+
raw: { width, height, channels: 3 },
|
|
745
|
+
})
|
|
597
746
|
.png({
|
|
598
747
|
compressionLevel: 9,
|
|
599
748
|
palette: false,
|
|
@@ -633,9 +782,10 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
633
782
|
const full = Buffer.concat([PIXEL_MAGIC, metaPixel]);
|
|
634
783
|
const bytesPerPixel = 3;
|
|
635
784
|
const nPixels = Math.ceil((full.length + 8) / 3);
|
|
636
|
-
const
|
|
637
|
-
const
|
|
638
|
-
const
|
|
785
|
+
const desiredSide = Math.ceil(Math.sqrt(nPixels));
|
|
786
|
+
const sideClamped = Math.max(1, Math.min(desiredSide, 65535));
|
|
787
|
+
const width = sideClamped;
|
|
788
|
+
const height = sideClamped === desiredSide ? sideClamped : Math.ceil(nPixels / width);
|
|
639
789
|
const dimHeader = Buffer.alloc(8);
|
|
640
790
|
dimHeader.writeUInt32BE(width, 0);
|
|
641
791
|
dimHeader.writeUInt32BE(height, 4);
|
|
@@ -771,15 +921,28 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
771
921
|
try {
|
|
772
922
|
const info = await sharp(pngBuf).metadata();
|
|
773
923
|
if (info.width && info.height) {
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
924
|
+
const MAX_RAW_BYTES = 150 * 1024 * 1024;
|
|
925
|
+
const rawBytesEstimate = info.width * info.height * 4;
|
|
926
|
+
if (rawBytesEstimate > MAX_RAW_BYTES) {
|
|
927
|
+
throw new DataFormatError(`Image too large to decode in-process (${Math.round(rawBytesEstimate / 1024 / 1024)} MB). Increase Node heap or use a smaller image/compact mode.`);
|
|
928
|
+
}
|
|
929
|
+
const MAX_DOUBLE_BYTES = 200 * 1024 * 1024;
|
|
930
|
+
const doubledPixels = info.width * 2 * (info.height * 2);
|
|
931
|
+
const doubledBytesEstimate = doubledPixels * 4;
|
|
932
|
+
if (false) {
|
|
933
|
+
const doubledBuffer = await sharp(pngBuf)
|
|
934
|
+
.resize({
|
|
935
|
+
width: info.width * 2,
|
|
936
|
+
height: info.height * 2,
|
|
937
|
+
kernel: 'nearest',
|
|
938
|
+
})
|
|
939
|
+
.png()
|
|
940
|
+
.toBuffer();
|
|
941
|
+
processedBuf = await cropAndReconstitute(doubledBuffer, opts.debugDir);
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
processedBuf = pngBuf;
|
|
945
|
+
}
|
|
783
946
|
}
|
|
784
947
|
}
|
|
785
948
|
catch (e) { }
|
|
@@ -796,16 +959,54 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
796
959
|
}
|
|
797
960
|
const rawPayload = d.slice(idx);
|
|
798
961
|
let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
|
|
962
|
+
if (opts.outPath) {
|
|
963
|
+
const ws = createWriteStream(opts.outPath, { highWaterMark: 64 * 1024 });
|
|
964
|
+
let headerBuf = Buffer.alloc(0);
|
|
965
|
+
let headerSkipped = false;
|
|
966
|
+
await tryZstdDecompress(payload, (info) => {
|
|
967
|
+
if (opts.onProgress)
|
|
968
|
+
opts.onProgress(info);
|
|
969
|
+
}, async (decChunk) => {
|
|
970
|
+
if (!headerSkipped) {
|
|
971
|
+
if (decChunk.length < MAGIC.length) {
|
|
972
|
+
headerBuf = Buffer.concat([headerBuf, decChunk]);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const mag = decChunk.slice(0, MAGIC.length);
|
|
976
|
+
if (!mag.equals(MAGIC)) {
|
|
977
|
+
ws.close();
|
|
978
|
+
throw new Error('Invalid ROX format (ROX direct: missing ROX1 magic after decompression)');
|
|
979
|
+
}
|
|
980
|
+
const toWriteBuf = decChunk.slice(MAGIC.length);
|
|
981
|
+
if (toWriteBuf.length > 0) {
|
|
982
|
+
await writeInChunks(ws, toWriteBuf, 16 * 1024);
|
|
983
|
+
}
|
|
984
|
+
headerBuf = Buffer.alloc(0);
|
|
985
|
+
headerSkipped = true;
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
await writeInChunks(ws, decChunk, 64 * 1024);
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
await new Promise((res) => ws.end(() => res()));
|
|
992
|
+
if (opts.onProgress)
|
|
993
|
+
opts.onProgress({ phase: 'done' });
|
|
994
|
+
progressBar?.stop();
|
|
995
|
+
return { meta: { name } };
|
|
996
|
+
}
|
|
799
997
|
if (opts.onProgress)
|
|
800
|
-
opts.onProgress({ phase: '
|
|
998
|
+
opts.onProgress({ phase: 'decompress_start' });
|
|
801
999
|
try {
|
|
802
|
-
payload = await tryZstdDecompress(payload)
|
|
1000
|
+
payload = await tryZstdDecompress(payload, (info) => {
|
|
1001
|
+
if (opts.onProgress)
|
|
1002
|
+
opts.onProgress(info);
|
|
1003
|
+
});
|
|
803
1004
|
}
|
|
804
1005
|
catch (e) {
|
|
805
1006
|
const errMsg = e instanceof Error ? e.message : String(e);
|
|
806
1007
|
if (opts.passphrase)
|
|
807
|
-
throw new
|
|
808
|
-
throw new
|
|
1008
|
+
throw new IncorrectPassphraseError('Incorrect passphrase (compact mode, zstd failed: ' + errMsg + ')');
|
|
1009
|
+
throw new DataFormatError('Compact mode zstd decompression failed: ' + errMsg);
|
|
809
1010
|
}
|
|
810
1011
|
if (!payload.slice(0, MAGIC.length).equals(MAGIC)) {
|
|
811
1012
|
throw new Error('Invalid ROX format (ROX direct: missing ROX1 magic after decompression)');
|
|
@@ -855,10 +1056,48 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
855
1056
|
if (rawPayload.length === 0)
|
|
856
1057
|
throw new DataFormatError('Compact mode payload empty');
|
|
857
1058
|
let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
|
|
1059
|
+
if (opts.outPath) {
|
|
1060
|
+
const ws = createWriteStream(opts.outPath, { highWaterMark: 64 * 1024 });
|
|
1061
|
+
let headerBuf = Buffer.alloc(0);
|
|
1062
|
+
let headerSkipped = false;
|
|
1063
|
+
await tryZstdDecompress(payload, (info) => {
|
|
1064
|
+
if (opts.onProgress)
|
|
1065
|
+
opts.onProgress(info);
|
|
1066
|
+
}, async (decChunk) => {
|
|
1067
|
+
if (!headerSkipped) {
|
|
1068
|
+
if (decChunk.length < MAGIC.length) {
|
|
1069
|
+
headerBuf = Buffer.concat([headerBuf, decChunk]);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
const mag = decChunk.slice(0, MAGIC.length);
|
|
1073
|
+
if (!mag.equals(MAGIC)) {
|
|
1074
|
+
ws.close();
|
|
1075
|
+
throw new DataFormatError('Invalid ROX format (compact mode: missing ROX1 magic after decompression)');
|
|
1076
|
+
}
|
|
1077
|
+
const toWriteBuf = decChunk.slice(MAGIC.length);
|
|
1078
|
+
if (toWriteBuf.length > 0) {
|
|
1079
|
+
await writeInChunks(ws, toWriteBuf);
|
|
1080
|
+
}
|
|
1081
|
+
headerBuf = Buffer.alloc(0);
|
|
1082
|
+
headerSkipped = true;
|
|
1083
|
+
}
|
|
1084
|
+
else {
|
|
1085
|
+
await writeInChunks(ws, decChunk, 64 * 1024);
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
await new Promise((res) => ws.end(() => res()));
|
|
1089
|
+
if (opts.onProgress)
|
|
1090
|
+
opts.onProgress({ phase: 'done' });
|
|
1091
|
+
progressBar?.stop();
|
|
1092
|
+
return { meta: { name } };
|
|
1093
|
+
}
|
|
858
1094
|
if (opts.onProgress)
|
|
859
|
-
opts.onProgress({ phase: '
|
|
1095
|
+
opts.onProgress({ phase: 'decompress_start' });
|
|
860
1096
|
try {
|
|
861
|
-
payload = await tryZstdDecompress(payload)
|
|
1097
|
+
payload = await tryZstdDecompress(payload, (info) => {
|
|
1098
|
+
if (opts.onProgress)
|
|
1099
|
+
opts.onProgress(info);
|
|
1100
|
+
});
|
|
862
1101
|
}
|
|
863
1102
|
catch (e) {
|
|
864
1103
|
const errMsg = e instanceof Error ? e.message : String(e);
|
|
@@ -936,7 +1175,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
936
1175
|
logicalData = rawRGB;
|
|
937
1176
|
}
|
|
938
1177
|
else {
|
|
939
|
-
const reconstructed = await cropAndReconstitute(
|
|
1178
|
+
const reconstructed = await cropAndReconstitute(processedBuf, opts.debugDir);
|
|
940
1179
|
const { data: rdata, info: rinfo } = await sharp(reconstructed)
|
|
941
1180
|
.ensureAlpha()
|
|
942
1181
|
.raw()
|
|
@@ -982,7 +1221,26 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
982
1221
|
const rawPayload = logicalData.slice(idx, idx + payloadLen);
|
|
983
1222
|
let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
|
|
984
1223
|
try {
|
|
985
|
-
|
|
1224
|
+
if (opts.outPath) {
|
|
1225
|
+
await tryZstdDecompress(payload, (info) => {
|
|
1226
|
+
if (opts.onProgress)
|
|
1227
|
+
opts.onProgress(info);
|
|
1228
|
+
}, undefined, opts.outPath);
|
|
1229
|
+
}
|
|
1230
|
+
else {
|
|
1231
|
+
payload = await tryZstdDecompress(payload, (info) => {
|
|
1232
|
+
if (opts.onProgress)
|
|
1233
|
+
opts.onProgress(info);
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
if (version === 3) {
|
|
1237
|
+
if (opts.outPath) {
|
|
1238
|
+
throw new Error('outPath not supported with delta encoding yet');
|
|
1239
|
+
}
|
|
1240
|
+
else {
|
|
1241
|
+
payload = deltaDecode(payload);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
986
1244
|
}
|
|
987
1245
|
catch (e) { }
|
|
988
1246
|
if (!payload.slice(0, MAGIC.length).equals(MAGIC)) {
|
|
@@ -1211,7 +1469,70 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
1211
1469
|
const rawPayload = pixelBytes.slice(idx, idx + payloadLen);
|
|
1212
1470
|
let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
|
|
1213
1471
|
try {
|
|
1214
|
-
|
|
1472
|
+
if (opts.outPath) {
|
|
1473
|
+
const ws = createWriteStream(opts.outPath, {
|
|
1474
|
+
highWaterMark: 64 * 1024,
|
|
1475
|
+
});
|
|
1476
|
+
let headerBuf = Buffer.alloc(0);
|
|
1477
|
+
let headerSkipped = false;
|
|
1478
|
+
let lastOutByte = null;
|
|
1479
|
+
await tryZstdDecompress(payload, (info) => {
|
|
1480
|
+
if (opts.onProgress)
|
|
1481
|
+
opts.onProgress(info);
|
|
1482
|
+
}, async (decChunk, idxChunk, totalChunks) => {
|
|
1483
|
+
let outChunk = decChunk;
|
|
1484
|
+
if (version === 3) {
|
|
1485
|
+
const out = Buffer.alloc(decChunk.length);
|
|
1486
|
+
for (let i = 0; i < decChunk.length; i++) {
|
|
1487
|
+
if (i === 0) {
|
|
1488
|
+
out[0] =
|
|
1489
|
+
typeof lastOutByte === 'number'
|
|
1490
|
+
? (lastOutByte + decChunk[0]) & 0xff
|
|
1491
|
+
: decChunk[0];
|
|
1492
|
+
}
|
|
1493
|
+
else {
|
|
1494
|
+
out[i] = (out[i - 1] + decChunk[i]) & 0xff;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
lastOutByte = out[out.length - 1];
|
|
1498
|
+
outChunk = out;
|
|
1499
|
+
}
|
|
1500
|
+
if (!headerSkipped) {
|
|
1501
|
+
if (outChunk.length < MAGIC.length) {
|
|
1502
|
+
headerBuf = Buffer.concat([headerBuf, outChunk]);
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
const mag = outChunk.slice(0, MAGIC.length);
|
|
1506
|
+
if (!mag.equals(MAGIC)) {
|
|
1507
|
+
ws.close();
|
|
1508
|
+
throw new DataFormatError('Invalid ROX format (pixel mode: missing ROX1 magic after decompression)');
|
|
1509
|
+
}
|
|
1510
|
+
const toWriteBuf = outChunk.slice(MAGIC.length);
|
|
1511
|
+
if (toWriteBuf.length > 0) {
|
|
1512
|
+
await writeInChunks(ws, toWriteBuf, 64 * 1024);
|
|
1513
|
+
}
|
|
1514
|
+
headerBuf = Buffer.alloc(0);
|
|
1515
|
+
headerSkipped = true;
|
|
1516
|
+
}
|
|
1517
|
+
else {
|
|
1518
|
+
await writeInChunks(ws, outChunk, 64 * 1024);
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
await new Promise((res, rej) => ws.end(() => res()));
|
|
1522
|
+
if (opts.onProgress)
|
|
1523
|
+
opts.onProgress({ phase: 'done' });
|
|
1524
|
+
progressBar?.stop();
|
|
1525
|
+
return { meta: { name } };
|
|
1526
|
+
}
|
|
1527
|
+
else {
|
|
1528
|
+
payload = await tryZstdDecompress(payload, (info) => {
|
|
1529
|
+
if (opts.onProgress)
|
|
1530
|
+
opts.onProgress(info);
|
|
1531
|
+
});
|
|
1532
|
+
if (version === 3) {
|
|
1533
|
+
payload = deltaDecode(payload);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1215
1536
|
}
|
|
1216
1537
|
catch (e) {
|
|
1217
1538
|
const errMsg = e instanceof Error ? e.message : String(e);
|
|
@@ -1276,7 +1597,18 @@ export function listFilesInPng(pngBuf) {
|
|
|
1276
1597
|
const data = Buffer.isBuffer(fileListChunk.data)
|
|
1277
1598
|
? fileListChunk.data
|
|
1278
1599
|
: Buffer.from(fileListChunk.data);
|
|
1279
|
-
|
|
1600
|
+
const files = JSON.parse(data.toString('utf8'));
|
|
1601
|
+
const dirs = new Set();
|
|
1602
|
+
for (const file of files) {
|
|
1603
|
+
const parts = file.split('/');
|
|
1604
|
+
let path = '';
|
|
1605
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1606
|
+
path += parts[i] + '/';
|
|
1607
|
+
dirs.add(path);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
const all = [...dirs, ...files];
|
|
1611
|
+
return all.sort();
|
|
1280
1612
|
}
|
|
1281
1613
|
}
|
|
1282
1614
|
catch (e) { }
|
package/dist/pack.d.ts
CHANGED
|
@@ -14,3 +14,13 @@ export declare function unpackBuffer(buf: Buffer, fileList?: string[]): {
|
|
|
14
14
|
buf: Buffer;
|
|
15
15
|
}[];
|
|
16
16
|
} | null;
|
|
17
|
+
/**
|
|
18
|
+
* Stream-unpack a packed buffer file on disk into a directory without
|
|
19
|
+
* loading the whole archive into memory.
|
|
20
|
+
*/
|
|
21
|
+
export declare function unpackFileToDir(filePath: string, baseDir: string, fileList?: string[]): {
|
|
22
|
+
files: {
|
|
23
|
+
path: string;
|
|
24
|
+
size: number;
|
|
25
|
+
}[];
|
|
26
|
+
};
|
package/dist/pack.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
-
import { join, relative, resolve, sep } from 'path';
|
|
1
|
+
import { closeSync, createWriteStream, mkdirSync, openSync, readFileSync, readSync, readdirSync, statSync, } from 'fs';
|
|
2
|
+
import { dirname, join, relative, resolve, sep } from 'path';
|
|
3
3
|
function collectFiles(paths) {
|
|
4
4
|
const files = [];
|
|
5
5
|
for (const p of paths) {
|
|
@@ -83,3 +83,60 @@ export function unpackBuffer(buf, fileList) {
|
|
|
83
83
|
}
|
|
84
84
|
return { files };
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Stream-unpack a packed buffer file on disk into a directory without
|
|
88
|
+
* loading the whole archive into memory.
|
|
89
|
+
*/
|
|
90
|
+
export function unpackFileToDir(filePath, baseDir, fileList) {
|
|
91
|
+
const fd = openSync(filePath, 'r');
|
|
92
|
+
try {
|
|
93
|
+
const headerBuf = Buffer.alloc(8);
|
|
94
|
+
let bytes = readSync(fd, headerBuf, 0, 8, 0);
|
|
95
|
+
if (bytes < 8)
|
|
96
|
+
throw new Error('Invalid archive');
|
|
97
|
+
if (headerBuf.readUInt32BE(0) !== 0x524f5850)
|
|
98
|
+
throw new Error('Not a pack archive');
|
|
99
|
+
const fileCount = headerBuf.readUInt32BE(4);
|
|
100
|
+
let offset = 8;
|
|
101
|
+
const files = [];
|
|
102
|
+
for (let i = 0; i < fileCount; i++) {
|
|
103
|
+
const nameLenBuf = Buffer.alloc(2);
|
|
104
|
+
readSync(fd, nameLenBuf, 0, 2, offset);
|
|
105
|
+
const nameLen = nameLenBuf.readUInt16BE(0);
|
|
106
|
+
offset += 2;
|
|
107
|
+
const nameBuf = Buffer.alloc(nameLen);
|
|
108
|
+
readSync(fd, nameBuf, 0, nameLen, offset);
|
|
109
|
+
const name = nameBuf.toString('utf8');
|
|
110
|
+
offset += nameLen;
|
|
111
|
+
const sizeBuf = Buffer.alloc(8);
|
|
112
|
+
readSync(fd, sizeBuf, 0, 8, offset);
|
|
113
|
+
const size = Number(sizeBuf.readBigUInt64BE(0));
|
|
114
|
+
offset += 8;
|
|
115
|
+
if (fileList && !fileList.includes(name)) {
|
|
116
|
+
offset += size;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const outPath = join(baseDir, name);
|
|
120
|
+
const outDir = dirname(outPath);
|
|
121
|
+
mkdirSync(outDir, { recursive: true });
|
|
122
|
+
const ws = createWriteStream(outPath);
|
|
123
|
+
let remaining = size;
|
|
124
|
+
const chunk = Buffer.alloc(64 * 1024);
|
|
125
|
+
while (remaining > 0) {
|
|
126
|
+
const toRead = Math.min(remaining, chunk.length);
|
|
127
|
+
const bytesRead = readSync(fd, chunk, 0, toRead, offset);
|
|
128
|
+
if (bytesRead <= 0)
|
|
129
|
+
break;
|
|
130
|
+
ws.write(chunk.slice(0, bytesRead));
|
|
131
|
+
offset += bytesRead;
|
|
132
|
+
remaining -= bytesRead;
|
|
133
|
+
}
|
|
134
|
+
ws.end();
|
|
135
|
+
files.push({ path: name, size });
|
|
136
|
+
}
|
|
137
|
+
return { files };
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
closeSync(fd);
|
|
141
|
+
}
|
|
142
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roxify",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"description": "Encode binary data into PNG images with Zstd compression and decode them back. Supports CLI and programmatic API (Node.js ESM).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"build": "tsc",
|
|
17
17
|
"check-publish": "node ../scripts/check-publish.js roxify",
|
|
18
18
|
"cli": "node dist/cli.js",
|
|
19
|
-
"test": "node test/pack.test.js"
|
|
19
|
+
"test": "npm run build && node test/pack.test.js && node test/screenshot.test.js"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
22
22
|
"steganography",
|