roxify 1.2.2 → 1.2.4
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/dist/cli.js +49 -4
- package/dist/index.d.ts +5 -0
- package/dist/index.js +105 -25
- package/dist/minpng.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import cliProgress from 'cli-progress';
|
|
3
3
|
import { mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
4
4
|
import { basename, dirname, join, resolve } from 'path';
|
|
5
|
-
import { DataFormatError, decodePngToBinary, encodeBinaryToPng, IncorrectPassphraseError, listFilesInPng, PassphraseRequiredError, } from './index.js';
|
|
5
|
+
import { DataFormatError, decodePngToBinary, encodeBinaryToPng, hasPassphraseInPng, IncorrectPassphraseError, listFilesInPng, PassphraseRequiredError, } from './index.js';
|
|
6
6
|
import { packPaths, unpackBuffer } from './pack.js';
|
|
7
|
-
const VERSION = '1.
|
|
7
|
+
const VERSION = '1.2.4';
|
|
8
8
|
function showHelp() {
|
|
9
9
|
console.log(`
|
|
10
10
|
ROX CLI — Encode/decode binary in PNG
|
|
@@ -15,7 +15,8 @@ Usage:
|
|
|
15
15
|
Commands:
|
|
16
16
|
encode <input>... [output] Encode one or more files/directories into a PNG
|
|
17
17
|
decode <input> [output] Decode PNG to original file
|
|
18
|
-
list <input> List files in a Rox PNG archive
|
|
18
|
+
list <input> List files in a Rox PNG archive
|
|
19
|
+
havepassphrase <input> Check whether the PNG requires a passphrase
|
|
19
20
|
|
|
20
21
|
Options:
|
|
21
22
|
-p, --passphrase <pass> Use passphrase (AES-256-GCM)
|
|
@@ -207,7 +208,7 @@ async function encodeCommand(args) {
|
|
|
207
208
|
if (st.isDirectory()) {
|
|
208
209
|
console.log(`Packing directory...`);
|
|
209
210
|
currentEncodeStep = 'Reading files';
|
|
210
|
-
const packResult = packPaths([resolvedInput], resolvedInput, onProgress);
|
|
211
|
+
const packResult = packPaths([resolvedInput], dirname(resolvedInput), onProgress);
|
|
211
212
|
inputBuffer = packResult.buf;
|
|
212
213
|
console.log('');
|
|
213
214
|
console.log(`Packed ${packResult.list.length} files -> ${(inputBuffer.length /
|
|
@@ -221,6 +222,8 @@ async function encodeCommand(args) {
|
|
|
221
222
|
inputBuffer = readFileSync(resolvedInput);
|
|
222
223
|
console.log('');
|
|
223
224
|
displayName = basename(resolvedInput);
|
|
225
|
+
options.includeFileList = true;
|
|
226
|
+
options.fileList = [basename(resolvedInput)];
|
|
224
227
|
}
|
|
225
228
|
}
|
|
226
229
|
Object.assign(options, {
|
|
@@ -402,12 +405,20 @@ async function decodeCommand(args) {
|
|
|
402
405
|
}
|
|
403
406
|
if (result.files) {
|
|
404
407
|
const baseDir = parsed.output || outputPath || '.';
|
|
408
|
+
const totalBytes = result.files.reduce((s, f) => s + f.buf.length, 0);
|
|
409
|
+
const extractBar = new cliProgress.SingleBar({ format: ' {bar} {percentage}% | {step} | {elapsed}s' }, cliProgress.Presets.shades_classic);
|
|
410
|
+
extractBar.start(totalBytes, 0, { step: 'Writing files' });
|
|
411
|
+
let written = 0;
|
|
405
412
|
for (const file of result.files) {
|
|
406
413
|
const fullPath = join(baseDir, file.path);
|
|
407
414
|
const dir = dirname(fullPath);
|
|
408
415
|
mkdirSync(dir, { recursive: true });
|
|
409
416
|
writeFileSync(fullPath, file.buf);
|
|
417
|
+
written += file.buf.length;
|
|
418
|
+
extractBar.update(written, { step: `Writing ${file.path}` });
|
|
410
419
|
}
|
|
420
|
+
extractBar.update(totalBytes, { step: 'Done' });
|
|
421
|
+
extractBar.stop();
|
|
411
422
|
console.log(`\nSuccess!`);
|
|
412
423
|
console.log(`Unpacked ${result.files.length} files to directory : ${resolve(baseDir)}`);
|
|
413
424
|
console.log(`Time: ${decodeTime}ms`);
|
|
@@ -416,12 +427,20 @@ async function decodeCommand(args) {
|
|
|
416
427
|
const unpacked = unpackBuffer(result.buf);
|
|
417
428
|
if (unpacked) {
|
|
418
429
|
const baseDir = parsed.output || outputPath || '.';
|
|
430
|
+
const totalBytes = unpacked.files.reduce((s, f) => s + f.buf.length, 0);
|
|
431
|
+
const extractBar = new cliProgress.SingleBar({ format: ' {bar} {percentage}% | {step} | {elapsed}s' }, cliProgress.Presets.shades_classic);
|
|
432
|
+
extractBar.start(totalBytes, 0, { step: 'Writing files' });
|
|
433
|
+
let written = 0;
|
|
419
434
|
for (const file of unpacked.files) {
|
|
420
435
|
const fullPath = join(baseDir, file.path);
|
|
421
436
|
const dir = dirname(fullPath);
|
|
422
437
|
mkdirSync(dir, { recursive: true });
|
|
423
438
|
writeFileSync(fullPath, file.buf);
|
|
439
|
+
written += file.buf.length;
|
|
440
|
+
extractBar.update(written, { step: `Writing ${file.path}` });
|
|
424
441
|
}
|
|
442
|
+
extractBar.update(totalBytes, { step: 'Done' });
|
|
443
|
+
extractBar.stop();
|
|
425
444
|
console.log(`\nSuccess!`);
|
|
426
445
|
console.log(`Time: ${decodeTime}ms`);
|
|
427
446
|
console.log(`Unpacked ${unpacked.files.length} files to current directory`);
|
|
@@ -511,6 +530,29 @@ async function listCommand(args) {
|
|
|
511
530
|
process.exit(1);
|
|
512
531
|
}
|
|
513
532
|
}
|
|
533
|
+
async function havePassphraseCommand(args) {
|
|
534
|
+
const parsed = parseArgs(args);
|
|
535
|
+
const [inputPath] = parsed._;
|
|
536
|
+
if (!inputPath) {
|
|
537
|
+
console.log(' ');
|
|
538
|
+
console.error('Error: Input PNG file required');
|
|
539
|
+
console.log('Usage: npx rox havepassphrase <input>');
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
const resolvedInput = resolve(inputPath);
|
|
543
|
+
try {
|
|
544
|
+
const inputBuffer = readFileSync(resolvedInput);
|
|
545
|
+
const has = await hasPassphraseInPng(inputBuffer);
|
|
546
|
+
console.log(has ? 'Passphrase detected.' : 'No passphrase detected.');
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
console.log(' ');
|
|
550
|
+
console.error('Failed to check passphrase. Use --verbose for details.');
|
|
551
|
+
if (parsed.verbose)
|
|
552
|
+
console.error('Details:', err.stack || err.message);
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
514
556
|
async function main() {
|
|
515
557
|
const args = process.argv.slice(2);
|
|
516
558
|
if (args.length === 0 || args[0] === 'help' || args[0] === '--help') {
|
|
@@ -533,6 +575,9 @@ async function main() {
|
|
|
533
575
|
case 'list':
|
|
534
576
|
await listCommand(commandArgs);
|
|
535
577
|
break;
|
|
578
|
+
case 'havepassphrase':
|
|
579
|
+
await havePassphraseCommand(commandArgs);
|
|
580
|
+
break;
|
|
536
581
|
default:
|
|
537
582
|
console.error(`Unknown command: ${command}`);
|
|
538
583
|
console.log('Run "npx rox help" for usage information');
|
package/dist/index.d.ts
CHANGED
|
@@ -127,3 +127,8 @@ export { packPaths, unpackBuffer } from './pack.js';
|
|
|
127
127
|
* @public
|
|
128
128
|
*/
|
|
129
129
|
export declare function listFilesInPng(pngBuf: Buffer): Promise<string[] | null>;
|
|
130
|
+
/**
|
|
131
|
+
* Detect if a PNG/ROX buffer contains an encrypted payload (requires passphrase)
|
|
132
|
+
* Returns true if encryption flag indicates AES or XOR.
|
|
133
|
+
*/
|
|
134
|
+
export declare function hasPassphraseInPng(pngBuf: Buffer): Promise<boolean>;
|
package/dist/index.js
CHANGED
|
@@ -87,7 +87,7 @@ function deltaDecode(data) {
|
|
|
87
87
|
}
|
|
88
88
|
return out;
|
|
89
89
|
}
|
|
90
|
-
async function parallelZstdCompress(payload, level =
|
|
90
|
+
async function parallelZstdCompress(payload, level = 11, onProgress) {
|
|
91
91
|
const chunkSize = 1024 * 1024 * 1024;
|
|
92
92
|
if (payload.length <= chunkSize) {
|
|
93
93
|
if (onProgress)
|
|
@@ -245,9 +245,10 @@ export async function optimizePngBuffer(pngBuf, fast = false) {
|
|
|
245
245
|
const inPath = join(tmpdir(), `rox_zop_in_${Date.now()}_${Math.random().toString(36).slice(2)}.png`);
|
|
246
246
|
const outPath = inPath + '.out.png';
|
|
247
247
|
writeFileSync(inPath, pngBuf);
|
|
248
|
+
const iterations = fast ? 15 : 40;
|
|
248
249
|
const args = [
|
|
249
250
|
'-y',
|
|
250
|
-
|
|
251
|
+
`--iterations=${iterations}`,
|
|
251
252
|
'--filters=01234mepb',
|
|
252
253
|
inPath,
|
|
253
254
|
outPath,
|
|
@@ -1277,7 +1278,7 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
1277
1278
|
opts.onProgress({ phase: 'compress_start', total: payload.length });
|
|
1278
1279
|
const useDelta = mode !== 'screenshot';
|
|
1279
1280
|
const deltaEncoded = useDelta ? deltaEncode(payload) : payload;
|
|
1280
|
-
payload = await parallelZstdCompress(deltaEncoded,
|
|
1281
|
+
payload = await parallelZstdCompress(deltaEncoded, 11, (loaded, total) => {
|
|
1281
1282
|
if (opts.onProgress) {
|
|
1282
1283
|
opts.onProgress({
|
|
1283
1284
|
phase: 'compress_progress',
|
|
@@ -1465,10 +1466,10 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
1465
1466
|
raw: { width, height, channels: 3 },
|
|
1466
1467
|
})
|
|
1467
1468
|
.png({
|
|
1468
|
-
compressionLevel:
|
|
1469
|
+
compressionLevel: 6,
|
|
1469
1470
|
palette: false,
|
|
1470
|
-
effort:
|
|
1471
|
-
adaptiveFiltering:
|
|
1471
|
+
effort: 1,
|
|
1472
|
+
adaptiveFiltering: false,
|
|
1472
1473
|
})
|
|
1473
1474
|
.toBuffer();
|
|
1474
1475
|
if (opts.onProgress)
|
|
@@ -1476,13 +1477,10 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
1476
1477
|
if (opts.onProgress)
|
|
1477
1478
|
opts.onProgress({ phase: 'optimizing', loaded: 0, total: 100 });
|
|
1478
1479
|
let optInterval = null;
|
|
1479
|
-
const MIN_OPT_MS = 8000;
|
|
1480
|
-
let optStart = Date.now();
|
|
1481
1480
|
if (opts.onProgress) {
|
|
1482
1481
|
let optLoaded = 0;
|
|
1483
|
-
optStart = Date.now();
|
|
1484
1482
|
optInterval = setInterval(() => {
|
|
1485
|
-
optLoaded = Math.min(optLoaded +
|
|
1483
|
+
optLoaded = Math.min(optLoaded + 5, 95);
|
|
1486
1484
|
opts.onProgress?.({
|
|
1487
1485
|
phase: 'optimizing',
|
|
1488
1486
|
loaded: optLoaded,
|
|
@@ -1491,12 +1489,7 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
1491
1489
|
}, 100);
|
|
1492
1490
|
}
|
|
1493
1491
|
try {
|
|
1494
|
-
const
|
|
1495
|
-
const optimized = await optimizedPromise;
|
|
1496
|
-
const elapsedOpt = Date.now() - optStart;
|
|
1497
|
-
if (elapsedOpt < MIN_OPT_MS) {
|
|
1498
|
-
await new Promise((r) => setTimeout(r, MIN_OPT_MS - elapsedOpt));
|
|
1499
|
-
}
|
|
1492
|
+
const optimized = await optimizePngBuffer(bufScr, true);
|
|
1500
1493
|
clearInterval(progressInterval);
|
|
1501
1494
|
if (optInterval) {
|
|
1502
1495
|
clearInterval(optInterval);
|
|
@@ -1504,18 +1497,13 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
1504
1497
|
}
|
|
1505
1498
|
if (opts.onProgress)
|
|
1506
1499
|
opts.onProgress({ phase: 'optimizing', loaded: 100, total: 100 });
|
|
1507
|
-
try {
|
|
1508
|
-
const verified = await decodePngToBinary(optimized);
|
|
1509
|
-
if (verified.buf && verified.buf.equals(input)) {
|
|
1510
|
-
progressBar?.stop();
|
|
1511
|
-
return optimized;
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
catch (e) { }
|
|
1515
1500
|
progressBar?.stop();
|
|
1516
|
-
return
|
|
1501
|
+
return optimized;
|
|
1517
1502
|
}
|
|
1518
1503
|
catch (e) {
|
|
1504
|
+
clearInterval(progressInterval);
|
|
1505
|
+
if (optInterval)
|
|
1506
|
+
clearInterval(optInterval);
|
|
1519
1507
|
progressBar?.stop();
|
|
1520
1508
|
return bufScr;
|
|
1521
1509
|
}
|
|
@@ -2559,3 +2547,95 @@ export async function listFilesInPng(pngBuf) {
|
|
|
2559
2547
|
catch (e) { }
|
|
2560
2548
|
return null;
|
|
2561
2549
|
}
|
|
2550
|
+
/**
|
|
2551
|
+
* Detect if a PNG/ROX buffer contains an encrypted payload (requires passphrase)
|
|
2552
|
+
* Returns true if encryption flag indicates AES or XOR.
|
|
2553
|
+
*/
|
|
2554
|
+
export async function hasPassphraseInPng(pngBuf) {
|
|
2555
|
+
try {
|
|
2556
|
+
if (pngBuf.slice(0, MAGIC.length).equals(MAGIC)) {
|
|
2557
|
+
let offset = MAGIC.length;
|
|
2558
|
+
if (offset >= pngBuf.length)
|
|
2559
|
+
return false;
|
|
2560
|
+
const nameLen = pngBuf.readUInt8(offset);
|
|
2561
|
+
offset += 1 + nameLen;
|
|
2562
|
+
if (offset >= pngBuf.length)
|
|
2563
|
+
return false;
|
|
2564
|
+
const flag = pngBuf[offset];
|
|
2565
|
+
return flag === ENC_AES || flag === ENC_XOR;
|
|
2566
|
+
}
|
|
2567
|
+
try {
|
|
2568
|
+
const chunksRaw = extract(pngBuf);
|
|
2569
|
+
const target = chunksRaw.find((c) => c.name === CHUNK_TYPE);
|
|
2570
|
+
if (target) {
|
|
2571
|
+
const data = Buffer.isBuffer(target.data)
|
|
2572
|
+
? target.data
|
|
2573
|
+
: Buffer.from(target.data);
|
|
2574
|
+
if (data.length >= 1) {
|
|
2575
|
+
const nameLen = data.readUInt8(0);
|
|
2576
|
+
const payloadStart = 1 + nameLen;
|
|
2577
|
+
if (payloadStart < data.length) {
|
|
2578
|
+
const flag = data[payloadStart];
|
|
2579
|
+
return flag === ENC_AES || flag === ENC_XOR;
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
catch (e) { }
|
|
2585
|
+
try {
|
|
2586
|
+
const sharpLib = await import('sharp');
|
|
2587
|
+
const { data } = await sharpLib
|
|
2588
|
+
.default(pngBuf)
|
|
2589
|
+
.raw()
|
|
2590
|
+
.toBuffer({ resolveWithObject: true });
|
|
2591
|
+
const rawRGB = Buffer.from(data);
|
|
2592
|
+
const markerLen = MARKER_COLORS.length * 3;
|
|
2593
|
+
for (let i = 0; i <= rawRGB.length - markerLen; i += 3) {
|
|
2594
|
+
let ok = true;
|
|
2595
|
+
for (let m = 0; m < MARKER_COLORS.length; m++) {
|
|
2596
|
+
const j = i + m * 3;
|
|
2597
|
+
if (rawRGB[j] !== MARKER_COLORS[m].r ||
|
|
2598
|
+
rawRGB[j + 1] !== MARKER_COLORS[m].g ||
|
|
2599
|
+
rawRGB[j + 2] !== MARKER_COLORS[m].b) {
|
|
2600
|
+
ok = false;
|
|
2601
|
+
break;
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
if (!ok)
|
|
2605
|
+
continue;
|
|
2606
|
+
const headerStart = i + markerLen;
|
|
2607
|
+
if (headerStart + PIXEL_MAGIC.length >= rawRGB.length)
|
|
2608
|
+
continue;
|
|
2609
|
+
if (!rawRGB
|
|
2610
|
+
.slice(headerStart, headerStart + PIXEL_MAGIC.length)
|
|
2611
|
+
.equals(PIXEL_MAGIC))
|
|
2612
|
+
continue;
|
|
2613
|
+
const metaStart = headerStart + PIXEL_MAGIC.length;
|
|
2614
|
+
if (metaStart + 2 >= rawRGB.length)
|
|
2615
|
+
continue;
|
|
2616
|
+
const nameLen = rawRGB[metaStart + 1];
|
|
2617
|
+
const payloadLenOff = metaStart + 2 + nameLen;
|
|
2618
|
+
const payloadStart = payloadLenOff + 4;
|
|
2619
|
+
if (payloadStart >= rawRGB.length)
|
|
2620
|
+
continue;
|
|
2621
|
+
const flag = rawRGB[payloadStart];
|
|
2622
|
+
return flag === ENC_AES || flag === ENC_XOR;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
catch (e) { }
|
|
2626
|
+
try {
|
|
2627
|
+
await decodePngToBinary(pngBuf, { showProgress: false });
|
|
2628
|
+
return false;
|
|
2629
|
+
}
|
|
2630
|
+
catch (e) {
|
|
2631
|
+
if (e instanceof PassphraseRequiredError)
|
|
2632
|
+
return true;
|
|
2633
|
+
if (e.message && e.message.toLowerCase().includes('passphrase'))
|
|
2634
|
+
return true;
|
|
2635
|
+
return false;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
catch (e) {
|
|
2639
|
+
return false;
|
|
2640
|
+
}
|
|
2641
|
+
}
|
package/dist/minpng.js
CHANGED
|
@@ -75,7 +75,7 @@ export async function encodeMinPng(rgb, width, height) {
|
|
|
75
75
|
transformed.push(g, r, b);
|
|
76
76
|
}
|
|
77
77
|
const transformedBuf = Buffer.from(transformed);
|
|
78
|
-
const compressed = Buffer.from(await zstdCompress(transformedBuf,
|
|
78
|
+
const compressed = Buffer.from(await zstdCompress(transformedBuf, 11));
|
|
79
79
|
const header = Buffer.alloc(4 + 1 + 4 + 4);
|
|
80
80
|
PIXEL_MAGIC.copy(header, 0);
|
|
81
81
|
header[4] = 1;
|
package/package.json
CHANGED