roxify 1.2.3 → 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.2.3';
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 /
@@ -404,12 +405,20 @@ async function decodeCommand(args) {
404
405
  }
405
406
  if (result.files) {
406
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;
407
412
  for (const file of result.files) {
408
413
  const fullPath = join(baseDir, file.path);
409
414
  const dir = dirname(fullPath);
410
415
  mkdirSync(dir, { recursive: true });
411
416
  writeFileSync(fullPath, file.buf);
417
+ written += file.buf.length;
418
+ extractBar.update(written, { step: `Writing ${file.path}` });
412
419
  }
420
+ extractBar.update(totalBytes, { step: 'Done' });
421
+ extractBar.stop();
413
422
  console.log(`\nSuccess!`);
414
423
  console.log(`Unpacked ${result.files.length} files to directory : ${resolve(baseDir)}`);
415
424
  console.log(`Time: ${decodeTime}ms`);
@@ -418,12 +427,20 @@ async function decodeCommand(args) {
418
427
  const unpacked = unpackBuffer(result.buf);
419
428
  if (unpacked) {
420
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;
421
434
  for (const file of unpacked.files) {
422
435
  const fullPath = join(baseDir, file.path);
423
436
  const dir = dirname(fullPath);
424
437
  mkdirSync(dir, { recursive: true });
425
438
  writeFileSync(fullPath, file.buf);
439
+ written += file.buf.length;
440
+ extractBar.update(written, { step: `Writing ${file.path}` });
426
441
  }
442
+ extractBar.update(totalBytes, { step: 'Done' });
443
+ extractBar.stop();
427
444
  console.log(`\nSuccess!`);
428
445
  console.log(`Time: ${decodeTime}ms`);
429
446
  console.log(`Unpacked ${unpacked.files.length} files to current directory`);
@@ -513,6 +530,29 @@ async function listCommand(args) {
513
530
  process.exit(1);
514
531
  }
515
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
+ }
516
556
  async function main() {
517
557
  const args = process.argv.slice(2);
518
558
  if (args.length === 0 || args[0] === 'help' || args[0] === '--help') {
@@ -535,6 +575,9 @@ async function main() {
535
575
  case 'list':
536
576
  await listCommand(commandArgs);
537
577
  break;
578
+ case 'havepassphrase':
579
+ await havePassphraseCommand(commandArgs);
580
+ break;
538
581
  default:
539
582
  console.error(`Unknown command: ${command}`);
540
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
@@ -2547,3 +2547,95 @@ export async function listFilesInPng(pngBuf) {
2547
2547
  catch (e) { }
2548
2548
  return null;
2549
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roxify",
3
- "version": "1.2.3",
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",