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 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.1.9';
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 = 22, onProgress) {
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
- '--iterations=500',
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, 22, (loaded, total) => {
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: 9,
1469
+ compressionLevel: 6,
1469
1470
  palette: false,
1470
- effort: 10,
1471
- adaptiveFiltering: true,
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 + 2, 99);
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 optimizedPromise = optimizePngBuffer(bufScr, !!opts.onProgress);
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 bufScr;
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, 22));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roxify",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
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",