roxify 1.15.2 → 1.16.0

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
@@ -20,7 +20,8 @@ async function loadJsEngine() {
20
20
  VFSIndexEntry: undefined,
21
21
  };
22
22
  }
23
- const VERSION = '1.14.9';
23
+ // Keep in sync with package.json#version.
24
+ const VERSION = '1.16.0';
24
25
  function getDirectorySize(dirPath) {
25
26
  let totalSize = 0;
26
27
  try {
@@ -80,7 +81,6 @@ Commands:
80
81
  Options:
81
82
  --image Use PNG container (default)
82
83
  --sound Use WAV audio container (smaller overhead, faster)
83
- --bwt-ans Use BWT-ANS compression instead of Zstd
84
84
  -p, --passphrase <pass> Use passphrase (AES-256-GCM)
85
85
  -m, --mode <mode> Mode: screenshot (default)
86
86
  -e, --encrypt <type> auto|aes|xor|none
@@ -151,10 +151,6 @@ function parseArgs(args) {
151
151
  parsed.forceTs = true;
152
152
  i++;
153
153
  }
154
- else if (key === 'bwt-ans') {
155
- parsed.compression = 'bwt-ans';
156
- i++;
157
- }
158
154
  else if (key === 'lossy-resilient') {
159
155
  parsed.lossyResilient = true;
160
156
  i++;
@@ -221,6 +217,8 @@ function parseArgs(args) {
221
217
  i += 2;
222
218
  break;
223
219
  case 'm':
220
+ // -m / --mode (only "screenshot" is supported); kept for compat,
221
+ // value is consumed but ignored.
224
222
  i += 2;
225
223
  break;
226
224
  case 'e':
@@ -239,7 +237,6 @@ function parseArgs(args) {
239
237
  parsed.sizes = true;
240
238
  i += 1;
241
239
  break;
242
- break;
243
240
  case 'd':
244
241
  parsed.debugDir = value;
245
242
  i += 2;
@@ -315,7 +312,7 @@ async function encodeCommand(args) {
315
312
  }
316
313
  }
317
314
  catch (e) { }
318
- if (isRustBinaryAvailable() && !parsed.forceTs && containerMode !== 'sound' && parsed.compression !== 'bwt-ans') {
315
+ if (isRustBinaryAvailable() && !parsed.forceTs && containerMode !== 'sound') {
319
316
  try {
320
317
  console.log(`Encoding to ${resolvedOutput} (Using native Rust encoder)\n`);
321
318
  const startTime = Date.now();
@@ -419,8 +416,6 @@ async function encodeCommand(args) {
419
416
  options.verbose = true;
420
417
  if (parsed.noCompress)
421
418
  options.compression = 'none';
422
- if (parsed.compression === 'bwt-ans')
423
- options.compression = 'bwt-ans';
424
419
  if (parsed.passphrase) {
425
420
  options.passphrase = parsed.passphrase;
426
421
  options.encrypt = parsed.encrypt || 'aes';
@@ -42,7 +42,7 @@ import { DecodeOptions, DecodeResult } from './types.js';
42
42
  * @param opts - Optional decode options.
43
43
  * @returns A Promise resolving to DecodeResult ({ buf, meta } or { files }).
44
44
  */
45
- export declare function decodePngToBinary(input: Buffer | string, _opts?: DecodeOptions): Promise<DecodeResult>;
45
+ export declare function decodePngToBinary(input: Buffer | string, opts?: DecodeOptions): Promise<DecodeResult>;
46
46
  /**
47
47
  * Detect and reverse simple pixel-stretching where each logical pixel
48
48
  * is repeated in an Fx×Fy block. Returns { width, height, data } or null.
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from 'fs';
2
2
  import { native } from './native.js';
3
3
  import { unpackBuffer } from '../pack.js';
4
+ import { tryDecryptIfNeeded } from './helpers.js';
4
5
  /**
5
6
  * Find PXL1 magic in pixel buffer
6
7
  */
@@ -115,7 +116,7 @@ function extractPayloadFromPixels(pixels) {
115
116
  * @param opts - Optional decode options.
116
117
  * @returns A Promise resolving to DecodeResult ({ buf, meta } or { files }).
117
118
  */
118
- export async function decodePngToBinary(input, _opts = {}) {
119
+ export async function decodePngToBinary(input, opts = {}) {
119
120
  // Get PNG buffer
120
121
  let pngBuf;
121
122
  if (Buffer.isBuffer(input)) {
@@ -126,27 +127,37 @@ export async function decodePngToBinary(input, _opts = {}) {
126
127
  }
127
128
  const payload = Buffer.from(native.extractPayloadFromPng(pngBuf));
128
129
  let name;
130
+ // Single-pass name lookup: ask the native side (which already keeps a
131
+ // decoded RGB cache during extract_payload_from_png) instead of decoding
132
+ // pixels again from TS.
129
133
  try {
130
- const rgbResult = native.pngToRgb(pngBuf);
131
- const pixels = Buffer.from(rgbResult.pixels);
132
- ({ name } = extractPayloadFromPixels(pixels));
134
+ const fromNative = native.extractNameFromPng?.(pngBuf);
135
+ if (typeof fromNative === 'string' && fromNative.length > 0) {
136
+ name = fromNative;
137
+ }
133
138
  }
134
139
  catch { }
140
+ if (!name) {
141
+ // Older native binaries don't expose extractNameFromPng; fall back to the
142
+ // previous TS path (re-decodes pixels, kept only for compat).
143
+ try {
144
+ const rgbResult = native.pngToRgb(pngBuf);
145
+ const pixels = Buffer.from(rgbResult.pixels);
146
+ ({ name } = extractPayloadFromPixels(pixels));
147
+ }
148
+ catch { }
149
+ }
135
150
  if (payload.length === 0) {
136
151
  throw new Error('Empty payload extracted');
137
152
  }
138
- // Handle encryption flag (first byte)
139
- // 0x00 = none, 0x01 = XOR, 0x02 = AES, 0x03 = AES-CTR
153
+ // Handle encryption flag (first byte): 0x00 none, 0x01 XOR, 0x02 AES-GCM, 0x03 AES-CTR.
154
+ // tryDecryptIfNeeded handles 0x00/0x01/0x02; 0x03 (streaming AES-CTR) needs native path.
140
155
  let data;
141
- if (payload[0] !== 0x00) {
142
- // Encrypted payload - not supported in current decoder
143
- // The native encoder handles encryption, but decoder needs native decrypt support
144
- throw new Error('Encrypted payload requires passphrase (not yet implemented in decoder)');
145
- }
146
- else {
147
- // Non-encrypted: skip the flag byte
148
- data = payload.subarray(1);
156
+ const flag = payload[0];
157
+ if (flag === 0x03) {
158
+ throw new Error('AES-CTR streaming payload requires the native decoder');
149
159
  }
160
+ data = tryDecryptIfNeeded(payload, opts.passphrase);
150
161
  // Decompress with zstd
151
162
  let decompressed;
152
163
  try {
@@ -86,7 +86,8 @@ export function tryDecryptIfNeeded(buf, passphrase) {
86
86
  const iv = buf.slice(17, 29);
87
87
  const tag = buf.slice(29, 45);
88
88
  const enc = buf.slice(45);
89
- const PBKDF2_ITERS = 1000000;
89
+ // Must match native/crypto.rs:PBKDF2_ITERS derived key depends on it.
90
+ const PBKDF2_ITERS = 600000;
90
91
  const key = pbkdf2Sync(passphrase, salt, PBKDF2_ITERS, 32, 'sha256');
91
92
  const dec = createDecipheriv('aes-256-gcm', key, iv);
92
93
  dec.setAuthTag(tag);
@@ -160,14 +160,22 @@ function spawnRustCLI(args, options) {
160
160
  let triedExtract = false;
161
161
  let tempExe;
162
162
  let stdout = '';
163
+ // Keep a tail of recent stderr lines so a non-zero exit produces an
164
+ // actionable error message instead of bare "exited with status 1".
165
+ const STDERR_TAIL = 32;
166
+ const stderrTail = [];
167
+ const pushStderr = (line) => {
168
+ stderrTail.push(line);
169
+ if (stderrTail.length > STDERR_TAIL)
170
+ stderrTail.shift();
171
+ };
163
172
  const runSpawn = (exePath) => {
164
173
  let proc;
165
- const hasProgress = !!options?.onProgress;
174
+ // Always pipe stderr so we can surface failure context, even when no
175
+ // progress callback is registered.
166
176
  const stdio = options?.collectStdout
167
- ? ['pipe', 'pipe', hasProgress ? 'pipe' : 'inherit']
168
- : hasProgress
169
- ? ['pipe', 'inherit', 'pipe']
170
- : 'inherit';
177
+ ? ['pipe', 'pipe', 'pipe']
178
+ : ['pipe', 'inherit', 'pipe'];
171
179
  try {
172
180
  proc = spawn(exePath, args, { stdio });
173
181
  }
@@ -187,18 +195,20 @@ function spawnRustCLI(args, options) {
187
195
  if (options?.collectStdout && proc.stdout) {
188
196
  proc.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
189
197
  }
190
- if (hasProgress && proc.stderr) {
198
+ if (proc.stderr) {
199
+ const hasProgress = !!options?.onProgress;
191
200
  let stderrBuf = '';
192
201
  proc.stderr.on('data', (chunk) => {
193
202
  stderrBuf += chunk.toString();
194
203
  const lines = stderrBuf.split('\n');
195
204
  stderrBuf = lines.pop() || '';
196
205
  for (const line of lines) {
197
- const match = line.match(/^PROGRESS:(\d+):(\d+):(.+)$/);
206
+ const match = hasProgress ? line.match(/^PROGRESS:(\d+):(\d+):(.+)$/) : null;
198
207
  if (match) {
199
208
  options.onProgress(Number(match[1]), Number(match[2]), match[3]);
200
209
  }
201
210
  else if (line.trim()) {
211
+ pushStderr(line);
202
212
  process.stderr.write(line + '\n');
203
213
  }
204
214
  }
@@ -226,8 +236,10 @@ function spawnRustCLI(args, options) {
226
236
  }
227
237
  if (code === 0 || (code === null && signal === null))
228
238
  resolve(stdout);
229
- else
230
- reject(new Error(`Rust CLI exited with status ${code ?? signal}`));
239
+ else {
240
+ const trailer = stderrTail.length > 0 ? `\n stderr tail:\n ${stderrTail.join('\n ')}` : '';
241
+ reject(new Error(`Rust CLI exited with status ${code ?? signal}${trailer}`));
242
+ }
231
243
  });
232
244
  };
233
245
  runSpawn(cliPath);
@@ -238,14 +250,11 @@ export async function encodeWithRustCLI(inputPath, outputPath, compressionLevel
238
250
  if (!cliPath)
239
251
  throw new Error('Rust CLI binary not found');
240
252
  const args = ['encode', '--level', String(compressionLevel)];
241
- if (name) {
242
- try {
243
- const helpOut = execSync(`"${cliPath}" --help`, { encoding: 'utf8', timeout: 2000 });
244
- if (helpOut && helpOut.includes('--name'))
245
- args.push('--name', name);
246
- }
247
- catch (e) { }
248
- }
253
+ // --name is supported on all roxify_native binaries shipped since 1.14.x.
254
+ // We used to spawn `--help` here to feature-detect, which cost a full
255
+ // process fork per encode call. Just pass it.
256
+ if (name)
257
+ args.push('--name', name);
249
258
  if (passphrase) {
250
259
  args.push('--passphrase', passphrase);
251
260
  args.push('--encrypt', encryptType);
@@ -1,7 +1,8 @@
1
1
  import { PackedFile } from '../pack.js';
2
2
  import type { EccLevel } from './ecc.js';
3
3
  export interface EncodeOptions {
4
- compression?: 'zstd' | 'bwt-ans';
4
+ /** Only 'zstd' is wired in the current encoder; reserved for future codecs. */
5
+ compression?: 'zstd';
5
6
  compressionLevel?: number;
6
7
  passphrase?: string;
7
8
  /** optional dictionary to use for zstd compression */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roxify",
3
- "version": "1.15.2",
3
+ "version": "1.16.0",
4
4
  "type": "module",
5
5
  "description": "Ultra-lightweight PNG steganography with native Rust acceleration. Encode binary data into PNG images with zstd compression.",
6
6
  "main": "dist/index.js",