roxify 1.15.3 → 1.16.1
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 +5 -10
- package/dist/utils/decoder.d.ts +1 -1
- package/dist/utils/decoder.js +25 -14
- package/dist/utils/helpers.js +2 -1
- package/dist/utils/rust-cli-wrapper.js +26 -17
- package/dist/utils/types.d.ts +2 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,8 @@ async function loadJsEngine() {
|
|
|
20
20
|
VFSIndexEntry: undefined,
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
// Keep in sync with package.json#version.
|
|
24
|
+
const VERSION = '1.16.1';
|
|
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'
|
|
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';
|
package/dist/utils/decoder.d.ts
CHANGED
|
@@ -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,
|
|
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.
|
package/dist/utils/decoder.js
CHANGED
|
@@ -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,
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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 {
|
package/dist/utils/helpers.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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',
|
|
168
|
-
:
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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);
|
package/dist/utils/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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