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 +47 -4
- package/dist/index.d.ts +5 -0
- package/dist/index.js +92 -0
- 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.2.
|
|
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