roxify 1.2.3 → 1.2.5

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';
6
- import { packPaths, unpackBuffer } from './pack.js';
7
- const VERSION = '1.2.3';
5
+ import { DataFormatError, decodePngToBinary, encodeBinaryToPng, hasPassphraseInPng, IncorrectPassphraseError, listFilesInPng, PassphraseRequiredError, } from './index.js';
6
+ import { packPathsGenerator, unpackBuffer } from './pack.js';
7
+ const VERSION = '1.2.4';
8
8
  function showHelp() {
9
9
  console.log(`
10
10
  ROX CLI — Encode/decode binary in PNG
@@ -15,12 +15,12 @@ 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)
22
- -m, --mode <mode> Mode: compact|chunk|pixel|screenshot (default: screenshot)
23
- -q, --quality <0-11> Brotli quality (default: 11)
23
+ -m, --mode <mode> Mode: screenshot (default)
24
24
  -e, --encrypt <type> auto|aes|xor|none
25
25
  --no-compress Disable compression
26
26
  -o, --output <path> Output file path
@@ -78,11 +78,6 @@ function parseArgs(args) {
78
78
  i += 2;
79
79
  break;
80
80
  case 'm':
81
- parsed.mode = value;
82
- i += 2;
83
- break;
84
- case 'q':
85
- parsed.quality = parseInt(value, 10);
86
81
  i += 2;
87
82
  break;
88
83
  case 'e':
@@ -143,8 +138,6 @@ async function encodeCommand(args) {
143
138
  const resolvedOutput = parsed.output || outputPath || outputName;
144
139
  let options = {};
145
140
  try {
146
- let inputBuffer;
147
- let displayName;
148
141
  const encodeBar = new cliProgress.SingleBar({
149
142
  format: ' {bar} {percentage}% | {step} | {elapsed}s',
150
143
  }, cliProgress.Presets.shades_classic);
@@ -175,69 +168,63 @@ async function encodeCommand(args) {
175
168
  elapsed: String(Math.floor(elapsed / 1000)),
176
169
  });
177
170
  }, TICK_MS);
171
+ const mode = 'screenshot';
172
+ Object.assign(options, {
173
+ mode,
174
+ name: parsed.outputName || 'archive',
175
+ });
176
+ if (parsed.verbose)
177
+ options.verbose = true;
178
+ if (parsed.noCompress)
179
+ options.compression = 'none';
180
+ if (parsed.passphrase) {
181
+ options.passphrase = parsed.passphrase;
182
+ options.encrypt = parsed.encrypt || 'aes';
183
+ }
184
+ console.log(`Encoding to ${resolvedOutput} (Mode: ${mode})\n`);
185
+ let inputData;
186
+ let inputSizeVal = 0;
187
+ let displayName;
178
188
  let totalBytes = 0;
179
- let lastShownFile;
180
189
  const onProgress = (readBytes, total, currentFile) => {
181
190
  if (totalBytes === 0)
182
191
  totalBytes = total;
183
192
  const packPct = Math.floor((readBytes / totalBytes) * 25);
184
193
  targetPct = Math.max(targetPct, packPct);
185
- if (currentFile && currentFile !== lastShownFile) {
186
- lastShownFile = currentFile;
187
- }
188
194
  currentEncodeStep = currentFile
189
195
  ? `Reading files: ${currentFile}`
190
196
  : 'Reading files';
191
197
  };
192
198
  if (inputPaths.length > 1) {
193
199
  currentEncodeStep = 'Reading files';
194
- const packResult = packPaths(inputPaths, undefined, onProgress);
195
- inputBuffer = packResult.buf;
196
- console.log('');
197
- console.log(`Packed ${packResult.list.length} files -> ${(inputBuffer.length /
198
- 1024 /
199
- 1024).toFixed(2)} MB`);
200
+ const { index, stream, totalSize } = await packPathsGenerator(inputPaths, undefined, onProgress);
201
+ inputData = stream;
202
+ inputSizeVal = totalSize;
200
203
  displayName = parsed.outputName || 'archive';
201
204
  options.includeFileList = true;
202
- options.fileList = packResult.list;
205
+ options.fileList = index.map((e) => e.path);
203
206
  }
204
207
  else {
205
208
  const resolvedInput = resolvedInputs[0];
206
209
  const st = statSync(resolvedInput);
207
210
  if (st.isDirectory()) {
208
- console.log(`Packing directory...`);
209
211
  currentEncodeStep = 'Reading files';
210
- const packResult = packPaths([resolvedInput], resolvedInput, onProgress);
211
- inputBuffer = packResult.buf;
212
- console.log('');
213
- console.log(`Packed ${packResult.list.length} files -> ${(inputBuffer.length /
214
- 1024 /
215
- 1024).toFixed(2)} MB`);
212
+ const { index, stream, totalSize } = await packPathsGenerator([resolvedInput], dirname(resolvedInput), onProgress);
213
+ inputData = stream;
214
+ inputSizeVal = totalSize;
216
215
  displayName = parsed.outputName || basename(resolvedInput);
217
216
  options.includeFileList = true;
218
- options.fileList = packResult.list;
217
+ options.fileList = index.map((e) => e.path);
219
218
  }
220
219
  else {
221
- inputBuffer = readFileSync(resolvedInput);
222
- console.log('');
220
+ inputData = readFileSync(resolvedInput);
221
+ inputSizeVal = inputData.length;
223
222
  displayName = basename(resolvedInput);
224
223
  options.includeFileList = true;
225
224
  options.fileList = [basename(resolvedInput)];
226
225
  }
227
226
  }
228
- Object.assign(options, {
229
- mode: parsed.mode || 'screenshot',
230
- name: displayName,
231
- brQuality: parsed.quality !== undefined ? parsed.quality : 11,
232
- });
233
- if (parsed.noCompress) {
234
- options.compression = 'none';
235
- }
236
- if (parsed.passphrase) {
237
- options.passphrase = parsed.passphrase;
238
- options.encrypt = parsed.encrypt || 'aes';
239
- }
240
- console.log(`Encoding ${displayName} -> ${resolvedOutput}\n`);
227
+ options.name = displayName;
241
228
  options.onProgress = (info) => {
242
229
  let stepLabel = 'Processing';
243
230
  let pct = 0;
@@ -290,6 +277,17 @@ async function encodeCommand(args) {
290
277
  targetPct = Math.max(targetPct, pct);
291
278
  currentEncodeStep = stepLabel;
292
279
  };
280
+ let inputBuffer;
281
+ if (typeof inputData[Symbol.asyncIterator] === 'function') {
282
+ const chunks = [];
283
+ for await (const chunk of inputData) {
284
+ chunks.push(chunk);
285
+ }
286
+ inputBuffer = Buffer.concat(chunks);
287
+ }
288
+ else {
289
+ inputBuffer = inputData;
290
+ }
293
291
  const output = await encodeBinaryToPng(inputBuffer, options);
294
292
  const encodeTime = Date.now() - startEncode;
295
293
  clearInterval(encodeHeartbeat);
@@ -302,8 +300,8 @@ async function encodeCommand(args) {
302
300
  }
303
301
  writeFileSync(resolvedOutput, output);
304
302
  const outputSize = (output.length / 1024 / 1024).toFixed(2);
305
- const inputSize = (inputBuffer.length / 1024 / 1024).toFixed(2);
306
- const ratio = ((output.length / inputBuffer.length) * 100).toFixed(1);
303
+ const inputSize = (inputSizeVal / 1024 / 1024).toFixed(2);
304
+ const ratio = ((output.length / inputSizeVal) * 100).toFixed(1);
307
305
  console.log(`\nSuccess!`);
308
306
  console.log(` Input: ${inputSize} MB`);
309
307
  console.log(` Output: ${outputSize} MB (${ratio}% of original)`);
@@ -404,12 +402,20 @@ async function decodeCommand(args) {
404
402
  }
405
403
  if (result.files) {
406
404
  const baseDir = parsed.output || outputPath || '.';
405
+ const totalBytes = result.files.reduce((s, f) => s + f.buf.length, 0);
406
+ const extractBar = new cliProgress.SingleBar({ format: ' {bar} {percentage}% | {step} | {elapsed}s' }, cliProgress.Presets.shades_classic);
407
+ extractBar.start(totalBytes, 0, { step: 'Writing files' });
408
+ let written = 0;
407
409
  for (const file of result.files) {
408
410
  const fullPath = join(baseDir, file.path);
409
411
  const dir = dirname(fullPath);
410
412
  mkdirSync(dir, { recursive: true });
411
413
  writeFileSync(fullPath, file.buf);
414
+ written += file.buf.length;
415
+ extractBar.update(written, { step: `Writing ${file.path}` });
412
416
  }
417
+ extractBar.update(totalBytes, { step: 'Done' });
418
+ extractBar.stop();
413
419
  console.log(`\nSuccess!`);
414
420
  console.log(`Unpacked ${result.files.length} files to directory : ${resolve(baseDir)}`);
415
421
  console.log(`Time: ${decodeTime}ms`);
@@ -418,12 +424,20 @@ async function decodeCommand(args) {
418
424
  const unpacked = unpackBuffer(result.buf);
419
425
  if (unpacked) {
420
426
  const baseDir = parsed.output || outputPath || '.';
427
+ const totalBytes = unpacked.files.reduce((s, f) => s + f.buf.length, 0);
428
+ const extractBar = new cliProgress.SingleBar({ format: ' {bar} {percentage}% | {step} | {elapsed}s' }, cliProgress.Presets.shades_classic);
429
+ extractBar.start(totalBytes, 0, { step: 'Writing files' });
430
+ let written = 0;
421
431
  for (const file of unpacked.files) {
422
432
  const fullPath = join(baseDir, file.path);
423
433
  const dir = dirname(fullPath);
424
434
  mkdirSync(dir, { recursive: true });
425
435
  writeFileSync(fullPath, file.buf);
436
+ written += file.buf.length;
437
+ extractBar.update(written, { step: `Writing ${file.path}` });
426
438
  }
439
+ extractBar.update(totalBytes, { step: 'Done' });
440
+ extractBar.stop();
427
441
  console.log(`\nSuccess!`);
428
442
  console.log(`Time: ${decodeTime}ms`);
429
443
  console.log(`Unpacked ${unpacked.files.length} files to current directory`);
@@ -466,8 +480,7 @@ async function decodeCommand(args) {
466
480
  (err.message.includes('decompression failed') ||
467
481
  err.message.includes('missing ROX1') ||
468
482
  err.message.includes('Pixel payload truncated') ||
469
- err.message.includes('Marker START not found') ||
470
- err.message.includes('Brotli decompression failed')))) {
483
+ err.message.includes('Marker START not found')))) {
471
484
  console.log(' ');
472
485
  console.error('Data corrupted or unsupported format. Use --verbose for details.');
473
486
  }
@@ -513,6 +526,29 @@ async function listCommand(args) {
513
526
  process.exit(1);
514
527
  }
515
528
  }
529
+ async function havePassphraseCommand(args) {
530
+ const parsed = parseArgs(args);
531
+ const [inputPath] = parsed._;
532
+ if (!inputPath) {
533
+ console.log(' ');
534
+ console.error('Error: Input PNG file required');
535
+ console.log('Usage: npx rox havepassphrase <input>');
536
+ process.exit(1);
537
+ }
538
+ const resolvedInput = resolve(inputPath);
539
+ try {
540
+ const inputBuffer = readFileSync(resolvedInput);
541
+ const has = await hasPassphraseInPng(inputBuffer);
542
+ console.log(has ? 'Passphrase detected.' : 'No passphrase detected.');
543
+ }
544
+ catch (err) {
545
+ console.log(' ');
546
+ console.error('Failed to check passphrase. Use --verbose for details.');
547
+ if (parsed.verbose)
548
+ console.error('Details:', err.stack || err.message);
549
+ process.exit(1);
550
+ }
551
+ }
516
552
  async function main() {
517
553
  const args = process.argv.slice(2);
518
554
  if (args.length === 0 || args[0] === 'help' || args[0] === '--help') {
@@ -535,6 +571,9 @@ async function main() {
535
571
  case 'list':
536
572
  await listCommand(commandArgs);
537
573
  break;
574
+ case 'havepassphrase':
575
+ await havePassphraseCommand(commandArgs);
576
+ break;
538
577
  default:
539
578
  console.error(`Unknown command: ${command}`);
540
579
  console.log('Run "npx rox help" for usage information');
package/dist/index.d.ts CHANGED
@@ -1,129 +1,13 @@
1
- /// <reference types="node" />
2
- /// <reference types="node" />
3
- import { PackedFile } from './pack.js';
4
- export declare class PassphraseRequiredError extends Error {
5
- constructor(message?: string);
6
- }
7
- export declare class IncorrectPassphraseError extends Error {
8
- constructor(message?: string);
9
- }
10
- export declare class DataFormatError extends Error {
11
- constructor(message?: string);
12
- }
13
- /**
14
- * Options for encoding binary data into PNG format.
15
- * @public
16
- */
17
- export interface EncodeOptions {
18
- compression?: 'zstd';
19
- passphrase?: string;
20
- name?: string;
21
- mode?: 'compact' | 'pixel' | 'screenshot';
22
- encrypt?: 'auto' | 'aes' | 'xor' | 'none';
23
- _skipAuto?: boolean;
24
- output?: 'auto' | 'png' | 'rox';
25
- includeName?: boolean;
26
- includeFileList?: boolean;
27
- fileList?: string[];
28
- brQuality?: number;
29
- onProgress?: (info: {
30
- phase: string;
31
- loaded?: number;
32
- total?: number;
33
- }) => void;
34
- showProgress?: boolean;
35
- }
36
- /**
37
- * Result of decoding a PNG back to binary data.
38
- * @public
39
- */
40
- export interface DecodeResult {
41
- buf?: Buffer;
42
- meta?: {
43
- name?: string;
44
- };
45
- files?: PackedFile[];
46
- }
47
- export declare function optimizePngBuffer(pngBuf: Buffer, fast?: boolean): Promise<Buffer>;
48
- /**
49
- * Path to write decoded output directly to disk (streamed) to avoid high memory usage.
50
- */
51
- export interface DecodeOptions {
52
- /**
53
- * Passphrase for encrypted inputs.
54
- */
55
- passphrase?: string;
56
- /**
57
- * Directory to save debug images (doubled.png, reconstructed.png).
58
- */
59
- debugDir?: string;
60
- /**
61
- * Path to write decoded output directly to disk (streamed) to avoid high memory usage.
62
- */
63
- outPath?: string;
64
- /**
65
- * List of files to extract selectively from archives.
66
- */
67
- files?: string[];
68
- /**
69
- * Progress callback for decoding phases.
70
- */
71
- onProgress?: (info: {
72
- phase: string;
73
- loaded?: number;
74
- total?: number;
75
- }) => void;
76
- /**
77
- * Whether to display a progress bar in the console.
78
- * @defaultValue `false`
79
- */
80
- showProgress?: boolean;
81
- }
82
- export declare function cropAndReconstitute(input: Buffer, debugDir?: string): Promise<Buffer>;
83
- /**
84
- * Encode a Buffer into a PNG wrapper. Supports optional compression and
85
- * encryption. Defaults are chosen for a good balance between speed and size.
86
- *
87
- * @param input - Data to encode
88
- * @param opts - Encoding options
89
- * @public
90
- * @example
91
- * ```typescript
92
- * import { readFileSync, writeFileSync } from 'fs';
93
- * import { encodeBinaryToPng } from 'roxify';
94
- *
95
- * const fileName = 'input.bin'; //Path of your input file here
96
- * const inputBuffer = readFileSync(fileName);
97
- * const pngBuffer = await encodeBinaryToPng(inputBuffer, {
98
- * name: fileName,
99
- * });
100
- * writeFileSync('output.png', pngBuffer);
101
-
102
- * ```
103
- */
104
- export declare function encodeBinaryToPng(input: Buffer, opts?: EncodeOptions): Promise<Buffer>;
105
- /**
106
- * Decode a PNG produced by this library back to the original Buffer.
107
- * Supports the ROX binary format, rXDT chunk, and pixel encodings.
108
- *
109
- * @param pngBuf - PNG data
110
- * @param opts - Options (passphrase for encrypted inputs)
111
- * @public
112
- * @example
113
- * import { readFileSync, writeFileSync } from 'fs';
114
- * import { decodePngToBinary } from 'roxify';
115
- *
116
- * const pngFromDisk = readFileSync('output.png'); //Path of the encoded PNG here
117
- * const { buf, meta } = await decodePngToBinary(pngFromDisk);
118
- * writeFileSync(meta?.name ?? 'decoded.txt', buf);
119
- */
120
- export declare function decodePngToBinary(pngBuf: Buffer, opts?: DecodeOptions): Promise<DecodeResult>;
1
+ export * from './utils/constants.js';
2
+ export * from './utils/crc.js';
3
+ export * from './utils/decoder.js';
4
+ export * from './utils/encoder.js';
5
+ export * from './utils/errors.js';
6
+ export * from './utils/helpers.js';
7
+ export * from './utils/inspection.js';
8
+ export * from './utils/optimization.js';
9
+ export * from './utils/reconstitution.js';
10
+ export * from './utils/types.js';
11
+ export * from './utils/zstd.js';
121
12
  export { decodeMinPng, encodeMinPng } from './minpng.js';
122
13
  export { packPaths, unpackBuffer } from './pack.js';
123
- /**
124
- * List files in a Rox PNG archive without decoding the full payload.
125
- * Returns the file list if available, otherwise null.
126
- * @param pngBuf - PNG data
127
- * @public
128
- */
129
- export declare function listFilesInPng(pngBuf: Buffer): Promise<string[] | null>;