roxify 1.1.4 → 1.1.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/README.md +83 -16
- package/dist/cli.js +226 -28
- package/dist/index.d.ts +50 -1
- package/dist/index.js +176 -5
- package/dist/pack.d.ts +16 -0
- package/dist/pack.js +85 -0
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -15,6 +15,8 @@ Key benefits:
|
|
|
15
15
|
- **Code Efficiency**: Hyper-efficient for compressing source code, reducing file sizes dramatically.
|
|
16
16
|
- **Obfuscation & Security**: Obfuscate code or lock files with AES-256-GCM encryption, more compact than password-protected ZIPs.
|
|
17
17
|
- **Visual Data Indicator**: PNG size visually represents embedded data size, providing an intuitive overview.
|
|
18
|
+
- **Archive Support**: Pack directories into archives, list contents without decoding, and extract individual files selectively.
|
|
19
|
+
- **Central Directory**: Access file lists without passphrase, even for encrypted archives.
|
|
18
20
|
|
|
19
21
|
## Installation
|
|
20
22
|
|
|
@@ -37,8 +39,9 @@ If no output name is provided:
|
|
|
37
39
|
|
|
38
40
|
**Commands:**
|
|
39
41
|
|
|
40
|
-
- `encode <input
|
|
41
|
-
- `decode <input> [output]` — Decode PNG to file
|
|
42
|
+
- `encode <input>... [output]` — Encode file(s)/directory to PNG
|
|
43
|
+
- `decode <input> [output]` — Decode PNG to file(s)
|
|
44
|
+
- `list <input>` — List files in archive without decoding
|
|
42
45
|
|
|
43
46
|
**Options:**
|
|
44
47
|
|
|
@@ -46,6 +49,7 @@ If no output name is provided:
|
|
|
46
49
|
- `-m, --mode <mode>` — Encoding mode: `screenshot` (default), `pixel`, `compact`, `chunk`
|
|
47
50
|
- `-q, --quality <0-22>` — Roxify compression level (default: 22)
|
|
48
51
|
- `--no-compress` — Disable compression
|
|
52
|
+
- `--files <list>` — Extract only specified files (comma-separated, for archives)
|
|
49
53
|
- `-v, --verbose` — Show detailed errors
|
|
50
54
|
|
|
51
55
|
Run `npx rox help` for full options.
|
|
@@ -53,46 +57,109 @@ Run `npx rox help` for full options.
|
|
|
53
57
|
## API Usage
|
|
54
58
|
|
|
55
59
|
```js
|
|
56
|
-
import { encodeBinaryToPng, decodePngToBinary } from 'roxify';
|
|
60
|
+
import { encodeBinaryToPng, decodePngToBinary, listFilesInPng } from 'roxify';
|
|
57
61
|
|
|
62
|
+
// Encode a file
|
|
58
63
|
const data = Buffer.from('Hello world');
|
|
59
64
|
const png = await encodeBinaryToPng(data, {
|
|
60
65
|
mode: 'screenshot',
|
|
61
66
|
name: 'message.txt',
|
|
62
67
|
});
|
|
63
68
|
|
|
69
|
+
// Decode
|
|
64
70
|
const { buf, meta } = await decodePngToBinary(png);
|
|
65
71
|
console.log(buf.toString('utf8'));
|
|
66
72
|
console.log(meta?.name);
|
|
73
|
+
|
|
74
|
+
// List files in archive
|
|
75
|
+
const files = listFilesInPng(png);
|
|
76
|
+
console.log(files);
|
|
77
|
+
|
|
78
|
+
// Selective extraction
|
|
79
|
+
const result = await decodePngToBinary(png, { files: ['file1.txt'] });
|
|
80
|
+
if (result.files) {
|
|
81
|
+
// result.files contains only the selected files
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Example: Progress Logging
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
import { encodeBinaryToPng, decodePngToBinary } from 'roxify';
|
|
89
|
+
|
|
90
|
+
const data = Buffer.from('Large data to encode...');
|
|
91
|
+
|
|
92
|
+
// Encode with progress logging
|
|
93
|
+
const png = await encodeBinaryToPng(data, {
|
|
94
|
+
onProgress: (info) => {
|
|
95
|
+
console.log(`Encoding phase: ${info.phase}`);
|
|
96
|
+
if (info.loaded && info.total) {
|
|
97
|
+
const percent = Math.round((info.loaded / info.total) * 100);
|
|
98
|
+
console.log(`Progress: ${percent}% (${info.loaded}/${info.total} bytes)`);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Decode with progress logging
|
|
104
|
+
const { buf } = await decodePngToBinary(png, {
|
|
105
|
+
onProgress: (info) => {
|
|
106
|
+
console.log(`Decoding phase: ${info.phase}`);
|
|
107
|
+
},
|
|
108
|
+
});
|
|
67
109
|
```
|
|
68
110
|
|
|
69
111
|
**API:**
|
|
70
112
|
|
|
71
113
|
- `encodeBinaryToPng(input: Buffer, opts?: EncodeOptions): Promise<Buffer>`
|
|
72
|
-
- `decodePngToBinary(pngBuf: Buffer): Promise<
|
|
114
|
+
- `decodePngToBinary(pngBuf: Buffer, opts?: DecodeOptions): Promise<DecodeResult>`
|
|
115
|
+
- `listFilesInPng(pngBuf: Buffer): string[] | null`
|
|
73
116
|
|
|
74
|
-
**
|
|
117
|
+
**EncodeOptions:**
|
|
75
118
|
|
|
76
119
|
- `mode` — `'screenshot'` | `'pixel'` | `'compact'` | `'chunk'` (default: `'screenshot'`)
|
|
77
120
|
- `name` — Original filename (embedded as metadata)
|
|
78
121
|
- `passphrase` — Encryption passphrase (uses AES-256-GCM)
|
|
79
122
|
- `compression` — `'Roxify'` | `'none'` (default: `'Roxify'`)
|
|
80
123
|
- `brQuality` — Roxify compression level 0-22 (default: 22)
|
|
124
|
+
- `showProgress` — Display progress bar (default: `false`)
|
|
125
|
+
- `onProgress` — Callback for progress updates: `(info: { phase: string; loaded?: number; total?: number }) => void`
|
|
126
|
+
- `includeFileList` — Include file list for archives (default: `true` for directories)
|
|
127
|
+
|
|
128
|
+
**DecodeOptions:**
|
|
81
129
|
|
|
82
|
-
|
|
130
|
+
- `passphrase` — Decryption passphrase
|
|
131
|
+
- `files` — List of files to extract selectively (for archives)
|
|
132
|
+
- `showProgress` — Display progress bar (default: `false`)
|
|
133
|
+
- `onProgress` — Callback for progress updates: `(info: { phase: string; loaded?: number; total?: number }) => void`
|
|
134
|
+
|
|
135
|
+
**DecodeResult:**
|
|
136
|
+
|
|
137
|
+
- `buf?: Buffer` — Decoded data (if not selective extraction)
|
|
138
|
+
- `files?: PackedFile[]` — Extracted files (if selective extraction)
|
|
139
|
+
- `meta?: { name?: string }` — Metadata
|
|
140
|
+
|
|
141
|
+
## Example: Archive with Selective Extraction
|
|
83
142
|
|
|
84
143
|
```js
|
|
85
|
-
import
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
res.send(png);
|
|
144
|
+
import { encodeBinaryToPng, decodePngToBinary, listFilesInPng } from 'roxify';
|
|
145
|
+
|
|
146
|
+
// Pack a directory
|
|
147
|
+
const fs = require('fs');
|
|
148
|
+
const dirData = packPaths(['myfolder']); // From pack.js
|
|
149
|
+
const png = await encodeBinaryToPng(dirData.buf, {
|
|
150
|
+
includeFileList: true,
|
|
151
|
+
fileList: dirData.list,
|
|
94
152
|
});
|
|
95
|
-
|
|
153
|
+
|
|
154
|
+
// List files without decoding
|
|
155
|
+
const files = listFilesInPng(png);
|
|
156
|
+
console.log('Files:', files);
|
|
157
|
+
|
|
158
|
+
// Extract only one file
|
|
159
|
+
const result = await decodePngToBinary(png, { files: ['myfolder/file.txt'] });
|
|
160
|
+
if (result.files) {
|
|
161
|
+
fs.writeFileSync('extracted.txt', result.files[0].buf);
|
|
162
|
+
}
|
|
96
163
|
```
|
|
97
164
|
|
|
98
165
|
## Requirements
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
2
|
+
import cliProgress from 'cli-progress';
|
|
3
|
+
import { mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
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.1.5';
|
|
6
8
|
function showHelp() {
|
|
7
9
|
console.log(`
|
|
8
10
|
ROX CLI — Encode/decode binary in PNG
|
|
@@ -11,8 +13,9 @@ Usage:
|
|
|
11
13
|
npx rox <command> [options]
|
|
12
14
|
|
|
13
15
|
Commands:
|
|
14
|
-
encode <input
|
|
15
|
-
decode <input> [output] Decode PNG to original file
|
|
16
|
+
encode <input>... [output] Encode one or more files/directories into a PNG
|
|
17
|
+
decode <input> [output] Decode PNG to original file
|
|
18
|
+
list <input> List files in a Rox PNG archive
|
|
16
19
|
|
|
17
20
|
Options:
|
|
18
21
|
-p, --passphrase <pass> Use passphrase (AES-256-GCM)
|
|
@@ -21,6 +24,7 @@ Options:
|
|
|
21
24
|
-e, --encrypt <type> auto|aes|xor|none
|
|
22
25
|
--no-compress Disable compression
|
|
23
26
|
-o, --output <path> Output file path
|
|
27
|
+
--files <list> Extract only specified files (comma-separated)
|
|
24
28
|
--view-reconst Export the reconstituted PNG for debugging
|
|
25
29
|
--debug Export debug images (doubled.png, reconstructed.png)
|
|
26
30
|
-v, --verbose Show detailed errors
|
|
@@ -55,6 +59,10 @@ function parseArgs(args) {
|
|
|
55
59
|
parsed.debugDir = args[i + 1];
|
|
56
60
|
i += 2;
|
|
57
61
|
}
|
|
62
|
+
else if (key === 'files') {
|
|
63
|
+
parsed.files = args[i + 1].split(',');
|
|
64
|
+
i += 2;
|
|
65
|
+
}
|
|
58
66
|
else {
|
|
59
67
|
const value = args[i + 1];
|
|
60
68
|
parsed[key] = value;
|
|
@@ -107,25 +115,92 @@ function parseArgs(args) {
|
|
|
107
115
|
}
|
|
108
116
|
async function encodeCommand(args) {
|
|
109
117
|
const parsed = parseArgs(args);
|
|
110
|
-
const
|
|
111
|
-
|
|
118
|
+
const inputPaths = parsed.output
|
|
119
|
+
? parsed._
|
|
120
|
+
: parsed._.length > 1
|
|
121
|
+
? parsed._.slice(0, -1)
|
|
122
|
+
: parsed._;
|
|
123
|
+
const outputPath = parsed.output
|
|
124
|
+
? undefined
|
|
125
|
+
: parsed._.length > 1
|
|
126
|
+
? parsed._[parsed._.length - 1]
|
|
127
|
+
: undefined;
|
|
128
|
+
const firstInput = inputPaths[0];
|
|
129
|
+
if (!firstInput) {
|
|
112
130
|
console.error('Error: Input file required');
|
|
113
131
|
console.log('Usage: npx rox encode <input> [output] [options]');
|
|
114
132
|
process.exit(1);
|
|
115
133
|
}
|
|
116
|
-
const
|
|
117
|
-
const resolvedOutput = parsed.output ||
|
|
134
|
+
const resolvedInputs = inputPaths.map((p) => resolve(p));
|
|
135
|
+
const resolvedOutput = parsed.output ||
|
|
136
|
+
outputPath ||
|
|
137
|
+
(inputPaths.length === 1
|
|
138
|
+
? firstInput.replace(/(\.[^.]+)?$/, '.png')
|
|
139
|
+
: 'archive.png');
|
|
140
|
+
let options = {};
|
|
118
141
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
142
|
+
let inputBuffer;
|
|
143
|
+
let displayName;
|
|
144
|
+
if (inputPaths.length > 1) {
|
|
145
|
+
console.log(`Packing ${inputPaths.length} inputs...`);
|
|
146
|
+
const bar = new cliProgress.SingleBar({
|
|
147
|
+
format: ' {bar} {percentage}% | {step} | {mbValue}/{mbTotal} MB',
|
|
148
|
+
}, cliProgress.Presets.shades_classic);
|
|
149
|
+
let totalBytes = 0;
|
|
150
|
+
const onProgress = (readBytes, total) => {
|
|
151
|
+
if (totalBytes === 0) {
|
|
152
|
+
totalBytes = total;
|
|
153
|
+
bar.start(totalBytes, 0, {
|
|
154
|
+
step: 'Reading files',
|
|
155
|
+
mbValue: '0.00',
|
|
156
|
+
mbTotal: (totalBytes / 1024 / 1024).toFixed(2),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
bar.update(readBytes, {
|
|
160
|
+
step: 'Reading files',
|
|
161
|
+
mbValue: (readBytes / 1024 / 1024).toFixed(2),
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
const packResult = packPaths(inputPaths, undefined, onProgress);
|
|
165
|
+
bar.stop();
|
|
166
|
+
inputBuffer = packResult.buf;
|
|
167
|
+
console.log(`Packed ${packResult.list.length} files -> ${(inputBuffer.length /
|
|
168
|
+
1024 /
|
|
169
|
+
1024).toFixed(2)} MB`);
|
|
170
|
+
displayName = parsed.outputName || 'archive';
|
|
171
|
+
options.includeFileList = true;
|
|
172
|
+
options.fileList = packResult.list;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const resolvedInput = resolvedInputs[0];
|
|
176
|
+
console.log(' ');
|
|
177
|
+
console.log(`Reading: ${resolvedInput}`);
|
|
178
|
+
console.log(' ');
|
|
179
|
+
const st = statSync(resolvedInput);
|
|
180
|
+
if (st.isDirectory()) {
|
|
181
|
+
console.log(`Packing directory...`);
|
|
182
|
+
const packResult = packPaths([resolvedInput]);
|
|
183
|
+
inputBuffer = packResult.buf;
|
|
184
|
+
console.log(`Packed ${packResult.list.length} files -> ${(inputBuffer.length /
|
|
185
|
+
1024 /
|
|
186
|
+
1024).toFixed(2)} MB`);
|
|
187
|
+
displayName = parsed.outputName || basename(resolvedInput);
|
|
188
|
+
options.includeFileList = true;
|
|
189
|
+
options.fileList = packResult.list;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const startRead = Date.now();
|
|
193
|
+
inputBuffer = readFileSync(resolvedInput);
|
|
194
|
+
const readTime = Date.now() - startRead;
|
|
195
|
+
console.log('');
|
|
196
|
+
displayName = basename(resolvedInput);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
Object.assign(options, {
|
|
125
200
|
mode: parsed.mode || 'screenshot',
|
|
126
|
-
name:
|
|
201
|
+
name: displayName,
|
|
127
202
|
brQuality: parsed.quality !== undefined ? parsed.quality : 11,
|
|
128
|
-
};
|
|
203
|
+
});
|
|
129
204
|
if (parsed.noCompress) {
|
|
130
205
|
options.compression = 'none';
|
|
131
206
|
}
|
|
@@ -133,10 +208,45 @@ async function encodeCommand(args) {
|
|
|
133
208
|
options.passphrase = parsed.passphrase;
|
|
134
209
|
options.encrypt = parsed.encrypt || 'aes';
|
|
135
210
|
}
|
|
136
|
-
console.log(`Encoding ${
|
|
211
|
+
console.log(`Encoding ${displayName} -> ${resolvedOutput}\n`);
|
|
212
|
+
const encodeBar = new cliProgress.SingleBar({
|
|
213
|
+
format: ' {bar} {percentage}% | {step} | {elapsed}s',
|
|
214
|
+
}, cliProgress.Presets.shades_classic);
|
|
215
|
+
let totalMB = Math.max(1, Math.round(inputBuffer.length / 1024 / 1024));
|
|
216
|
+
encodeBar.start(100, 0, {
|
|
217
|
+
step: 'Starting',
|
|
218
|
+
elapsed: '0',
|
|
219
|
+
});
|
|
137
220
|
const startEncode = Date.now();
|
|
221
|
+
options.onProgress = (info) => {
|
|
222
|
+
let pct = 0;
|
|
223
|
+
if (info.phase === 'compress_progress') {
|
|
224
|
+
pct = (info.loaded / info.total) * 50;
|
|
225
|
+
}
|
|
226
|
+
else if (info.phase === 'compress_done') {
|
|
227
|
+
pct = 50;
|
|
228
|
+
}
|
|
229
|
+
else if (info.phase === 'encrypt_done') {
|
|
230
|
+
pct = 80;
|
|
231
|
+
}
|
|
232
|
+
else if (info.phase === 'png_gen') {
|
|
233
|
+
pct = 90;
|
|
234
|
+
}
|
|
235
|
+
else if (info.phase === 'done') {
|
|
236
|
+
pct = 100;
|
|
237
|
+
}
|
|
238
|
+
encodeBar.update(Math.floor(pct), {
|
|
239
|
+
step: info.phase.replace('_', ' '),
|
|
240
|
+
elapsed: String(Math.floor((Date.now() - startEncode) / 1000)),
|
|
241
|
+
});
|
|
242
|
+
};
|
|
138
243
|
const output = await encodeBinaryToPng(inputBuffer, options);
|
|
139
244
|
const encodeTime = Date.now() - startEncode;
|
|
245
|
+
encodeBar.update(100, {
|
|
246
|
+
step: 'done',
|
|
247
|
+
elapsed: String(Math.floor(encodeTime / 1000)),
|
|
248
|
+
});
|
|
249
|
+
encodeBar.stop();
|
|
140
250
|
writeFileSync(resolvedOutput, output);
|
|
141
251
|
const outputSize = (output.length / 1024 / 1024).toFixed(2);
|
|
142
252
|
const inputSize = (inputBuffer.length / 1024 / 1024).toFixed(2);
|
|
@@ -146,6 +256,7 @@ async function encodeCommand(args) {
|
|
|
146
256
|
console.log(` Output: ${outputSize} MB (${ratio}% of original)`);
|
|
147
257
|
console.log(` Time: ${encodeTime}ms`);
|
|
148
258
|
console.log(` Saved: ${resolvedOutput}`);
|
|
259
|
+
console.log(' ');
|
|
149
260
|
}
|
|
150
261
|
catch (err) {
|
|
151
262
|
console.error('Error: Failed to encode file. Use --verbose for details.');
|
|
@@ -165,6 +276,7 @@ async function decodeCommand(args) {
|
|
|
165
276
|
const resolvedInput = resolve(inputPath);
|
|
166
277
|
try {
|
|
167
278
|
const inputBuffer = readFileSync(resolvedInput);
|
|
279
|
+
console.log(' ');
|
|
168
280
|
console.log(`Reading: ${resolvedInput}`);
|
|
169
281
|
const options = {};
|
|
170
282
|
if (parsed.passphrase) {
|
|
@@ -173,20 +285,74 @@ async function decodeCommand(args) {
|
|
|
173
285
|
if (parsed.debug) {
|
|
174
286
|
options.debugDir = dirname(resolvedInput);
|
|
175
287
|
}
|
|
288
|
+
if (parsed.files) {
|
|
289
|
+
options.files = parsed.files;
|
|
290
|
+
}
|
|
291
|
+
console.log(' ');
|
|
292
|
+
console.log(' ');
|
|
176
293
|
console.log(`Decoding...`);
|
|
294
|
+
console.log(' ');
|
|
295
|
+
const decodeBar = new cliProgress.SingleBar({
|
|
296
|
+
format: ' {bar} {percentage}% | {step} | {elapsed}s',
|
|
297
|
+
}, cliProgress.Presets.shades_classic);
|
|
298
|
+
decodeBar.start(100, 0, {
|
|
299
|
+
step: 'Decoding',
|
|
300
|
+
elapsed: '0',
|
|
301
|
+
});
|
|
177
302
|
const startDecode = Date.now();
|
|
303
|
+
options.onProgress = (info) => {
|
|
304
|
+
let pct = 50;
|
|
305
|
+
if (info.phase === 'done')
|
|
306
|
+
pct = 100;
|
|
307
|
+
decodeBar.update(pct, {
|
|
308
|
+
step: 'Decoding',
|
|
309
|
+
elapsed: String(Math.floor((Date.now() - startDecode) / 1000)),
|
|
310
|
+
});
|
|
311
|
+
};
|
|
178
312
|
const result = await decodePngToBinary(inputBuffer, options);
|
|
179
313
|
const decodeTime = Date.now() - startDecode;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
314
|
+
decodeBar.update(100, {
|
|
315
|
+
step: 'done',
|
|
316
|
+
elapsed: String(Math.floor(decodeTime / 1000)),
|
|
317
|
+
});
|
|
318
|
+
decodeBar.stop();
|
|
319
|
+
if (result.files) {
|
|
320
|
+
const baseDir = parsed.output || outputPath || 'extracted';
|
|
321
|
+
for (const file of result.files) {
|
|
322
|
+
const fullPath = join(baseDir, file.path);
|
|
323
|
+
const dir = dirname(fullPath);
|
|
324
|
+
mkdirSync(dir, { recursive: true });
|
|
325
|
+
writeFileSync(fullPath, file.buf);
|
|
326
|
+
}
|
|
327
|
+
console.log(`\nSuccess!`);
|
|
328
|
+
console.log(`Extracted ${result.files.length} files to ${baseDir}`);
|
|
329
|
+
console.log(`Time: ${decodeTime}ms`);
|
|
186
330
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
331
|
+
else if (result.buf) {
|
|
332
|
+
const unpacked = unpackBuffer(result.buf);
|
|
333
|
+
if (unpacked) {
|
|
334
|
+
const baseDir = parsed.output || outputPath || result.meta?.name || 'unpacked';
|
|
335
|
+
for (const file of unpacked.files) {
|
|
336
|
+
const fullPath = join(baseDir, file.path);
|
|
337
|
+
const dir = dirname(fullPath);
|
|
338
|
+
mkdirSync(dir, { recursive: true });
|
|
339
|
+
writeFileSync(fullPath, file.buf);
|
|
340
|
+
}
|
|
341
|
+
console.log(`Unpacked ${unpacked.files.length} files to ${baseDir}`);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
const resolvedOutput = parsed.output || outputPath || result.meta?.name || 'decoded.bin';
|
|
345
|
+
writeFileSync(resolvedOutput, result.buf);
|
|
346
|
+
console.log(`\nSuccess!`);
|
|
347
|
+
if (result.meta?.name) {
|
|
348
|
+
console.log(` Original name: ${result.meta.name}`);
|
|
349
|
+
}
|
|
350
|
+
console.log(` Output size: ${(result.buf.length / 1024 / 1024).toFixed(2)} MB`);
|
|
351
|
+
console.log(` Time: ${decodeTime}ms`);
|
|
352
|
+
console.log(` Saved: ${resolvedOutput}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
console.log(' ');
|
|
190
356
|
}
|
|
191
357
|
catch (err) {
|
|
192
358
|
if (err instanceof PassphraseRequiredError ||
|
|
@@ -214,6 +380,35 @@ async function decodeCommand(args) {
|
|
|
214
380
|
process.exit(1);
|
|
215
381
|
}
|
|
216
382
|
}
|
|
383
|
+
async function listCommand(args) {
|
|
384
|
+
const parsed = parseArgs(args);
|
|
385
|
+
const [inputPath] = parsed._;
|
|
386
|
+
if (!inputPath) {
|
|
387
|
+
console.error('Error: Input PNG file required');
|
|
388
|
+
console.log('Usage: npx rox list <input>');
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
const resolvedInput = resolve(inputPath);
|
|
392
|
+
try {
|
|
393
|
+
const inputBuffer = readFileSync(resolvedInput);
|
|
394
|
+
const fileList = listFilesInPng(inputBuffer);
|
|
395
|
+
if (fileList) {
|
|
396
|
+
console.log(`Files in ${resolvedInput}:`);
|
|
397
|
+
for (const file of fileList) {
|
|
398
|
+
console.log(` ${file}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
console.log('No file list found in the archive.');
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
catch (err) {
|
|
406
|
+
console.error('Failed to list files. Use --verbose for details.');
|
|
407
|
+
if (parsed.verbose)
|
|
408
|
+
console.error('Details:', err.stack || err.message);
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
217
412
|
async function main() {
|
|
218
413
|
const args = process.argv.slice(2);
|
|
219
414
|
if (args.length === 0 || args[0] === 'help' || args[0] === '--help') {
|
|
@@ -233,6 +428,9 @@ async function main() {
|
|
|
233
428
|
case 'decode':
|
|
234
429
|
await decodeCommand(commandArgs);
|
|
235
430
|
break;
|
|
431
|
+
case 'list':
|
|
432
|
+
await listCommand(commandArgs);
|
|
433
|
+
break;
|
|
236
434
|
default:
|
|
237
435
|
console.error(`Unknown command: ${command}`);
|
|
238
436
|
console.log('Run "npx rox help" for usage information');
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
|
+
import { PackedFile } from './pack.js';
|
|
3
4
|
export declare class PassphraseRequiredError extends Error {
|
|
4
5
|
constructor(message?: string);
|
|
5
6
|
}
|
|
@@ -63,6 +64,15 @@ export interface EncodeOptions {
|
|
|
63
64
|
* @defaultValue `true`
|
|
64
65
|
*/
|
|
65
66
|
includeName?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Whether to include the file list in the encoded metadata for archives.
|
|
69
|
+
* @defaultValue `false`
|
|
70
|
+
*/
|
|
71
|
+
includeFileList?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* List of file paths for archives (used if includeFileList is true).
|
|
74
|
+
*/
|
|
75
|
+
fileList?: string[];
|
|
66
76
|
/**
|
|
67
77
|
* Brotli compression quality (0-11).
|
|
68
78
|
* - Lower values = faster compression, larger output
|
|
@@ -70,6 +80,16 @@ export interface EncodeOptions {
|
|
|
70
80
|
* @defaultValue `1` (optimized for speed)
|
|
71
81
|
*/
|
|
72
82
|
brQuality?: number;
|
|
83
|
+
onProgress?: (info: {
|
|
84
|
+
phase: string;
|
|
85
|
+
loaded?: number;
|
|
86
|
+
total?: number;
|
|
87
|
+
}) => void;
|
|
88
|
+
/**
|
|
89
|
+
* Whether to display a progress bar in the console.
|
|
90
|
+
* @defaultValue `false`
|
|
91
|
+
*/
|
|
92
|
+
showProgress?: boolean;
|
|
73
93
|
}
|
|
74
94
|
/**
|
|
75
95
|
* Result of decoding a PNG back to binary data.
|
|
@@ -79,7 +99,7 @@ export interface DecodeResult {
|
|
|
79
99
|
/**
|
|
80
100
|
* The decoded binary data.
|
|
81
101
|
*/
|
|
82
|
-
buf
|
|
102
|
+
buf?: Buffer;
|
|
83
103
|
/**
|
|
84
104
|
* Metadata extracted from the encoded image.
|
|
85
105
|
*/
|
|
@@ -89,6 +109,10 @@ export interface DecodeResult {
|
|
|
89
109
|
*/
|
|
90
110
|
name?: string;
|
|
91
111
|
};
|
|
112
|
+
/**
|
|
113
|
+
* Extracted files, if selective extraction was requested.
|
|
114
|
+
*/
|
|
115
|
+
files?: PackedFile[];
|
|
92
116
|
}
|
|
93
117
|
/**
|
|
94
118
|
* Options for decoding a PNG back to binary data.
|
|
@@ -103,6 +127,23 @@ export interface DecodeOptions {
|
|
|
103
127
|
* Directory to save debug images (doubled.png, reconstructed.png).
|
|
104
128
|
*/
|
|
105
129
|
debugDir?: string;
|
|
130
|
+
/**
|
|
131
|
+
* List of files to extract selectively from archives.
|
|
132
|
+
*/
|
|
133
|
+
files?: string[];
|
|
134
|
+
/**
|
|
135
|
+
* Progress callback for decoding phases.
|
|
136
|
+
*/
|
|
137
|
+
onProgress?: (info: {
|
|
138
|
+
phase: string;
|
|
139
|
+
loaded?: number;
|
|
140
|
+
total?: number;
|
|
141
|
+
}) => void;
|
|
142
|
+
/**
|
|
143
|
+
* Whether to display a progress bar in the console.
|
|
144
|
+
* @defaultValue `false`
|
|
145
|
+
*/
|
|
146
|
+
showProgress?: boolean;
|
|
106
147
|
}
|
|
107
148
|
export declare function cropAndReconstitute(input: Buffer, debugDir?: string): Promise<Buffer>;
|
|
108
149
|
/**
|
|
@@ -123,3 +164,11 @@ export declare function encodeBinaryToPng(input: Buffer, opts?: EncodeOptions):
|
|
|
123
164
|
* @public
|
|
124
165
|
*/
|
|
125
166
|
export declare function decodePngToBinary(pngBuf: Buffer, opts?: DecodeOptions): Promise<DecodeResult>;
|
|
167
|
+
export { packPaths, unpackBuffer } from './pack.js';
|
|
168
|
+
/**
|
|
169
|
+
* List files in a Rox PNG archive without decoding the full payload.
|
|
170
|
+
* Returns the file list if available, otherwise null.
|
|
171
|
+
* @param pngBuf - PNG data
|
|
172
|
+
* @public
|
|
173
|
+
*/
|
|
174
|
+
export declare function listFilesInPng(pngBuf: Buffer): string[] | null;
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { compress as zstdCompress, decompress as zstdDecompress, } from '@mongodb-js/zstd';
|
|
2
|
+
import cliProgress from 'cli-progress';
|
|
2
3
|
import { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes, } from 'crypto';
|
|
3
4
|
import { join } from 'path';
|
|
4
5
|
import encode from 'png-chunks-encode';
|
|
5
6
|
import extract from 'png-chunks-extract';
|
|
6
7
|
import sharp from 'sharp';
|
|
7
8
|
import * as zlib from 'zlib';
|
|
9
|
+
import { unpackBuffer } from './pack.js';
|
|
8
10
|
const CHUNK_TYPE = 'rXDT';
|
|
9
11
|
const MAGIC = Buffer.from('ROX1');
|
|
10
12
|
const PIXEL_MAGIC = Buffer.from('PXL1');
|
|
@@ -394,11 +396,60 @@ export async function cropAndReconstitute(input, debugDir) {
|
|
|
394
396
|
* @public
|
|
395
397
|
*/
|
|
396
398
|
export async function encodeBinaryToPng(input, opts = {}) {
|
|
399
|
+
let progressBar = null;
|
|
400
|
+
if (opts.showProgress) {
|
|
401
|
+
progressBar = new cliProgress.SingleBar({
|
|
402
|
+
format: ' {bar} {percentage}% | {step} | {elapsed}s',
|
|
403
|
+
}, cliProgress.Presets.shades_classic);
|
|
404
|
+
progressBar.start(100, 0, { step: 'Starting', elapsed: '0' });
|
|
405
|
+
const startTime = Date.now();
|
|
406
|
+
if (!opts.onProgress) {
|
|
407
|
+
opts.onProgress = (info) => {
|
|
408
|
+
let pct = 0;
|
|
409
|
+
if (info.phase === 'compress_progress' && info.loaded && info.total) {
|
|
410
|
+
pct = (info.loaded / info.total) * 50;
|
|
411
|
+
}
|
|
412
|
+
else if (info.phase === 'compress_done') {
|
|
413
|
+
pct = 50;
|
|
414
|
+
}
|
|
415
|
+
else if (info.phase === 'encrypt_done') {
|
|
416
|
+
pct = 80;
|
|
417
|
+
}
|
|
418
|
+
else if (info.phase === 'png_gen') {
|
|
419
|
+
pct = 90;
|
|
420
|
+
}
|
|
421
|
+
else if (info.phase === 'done') {
|
|
422
|
+
pct = 100;
|
|
423
|
+
}
|
|
424
|
+
progressBar.update(Math.floor(pct), {
|
|
425
|
+
step: info.phase.replace('_', ' '),
|
|
426
|
+
elapsed: String(Math.floor((Date.now() - startTime) / 1000)),
|
|
427
|
+
});
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
397
431
|
let payload = Buffer.concat([MAGIC, input]);
|
|
398
432
|
const brQuality = typeof opts.brQuality === 'number' ? opts.brQuality : 11;
|
|
399
433
|
const mode = opts.mode === undefined ? 'screenshot' : opts.mode;
|
|
400
434
|
const compression = opts.compression || 'zstd';
|
|
401
|
-
|
|
435
|
+
if (opts.onProgress)
|
|
436
|
+
opts.onProgress({ phase: 'compress_start', total: payload.length });
|
|
437
|
+
const compressedChunks = [];
|
|
438
|
+
const chunkSize = 1024 * 1024;
|
|
439
|
+
for (let i = 0; i < payload.length; i += chunkSize) {
|
|
440
|
+
const chunk = payload.slice(i, Math.min(i + chunkSize, payload.length));
|
|
441
|
+
const compressedChunk = Buffer.from(await zstdCompress(chunk, 22));
|
|
442
|
+
compressedChunks.push(compressedChunk);
|
|
443
|
+
if (opts.onProgress)
|
|
444
|
+
opts.onProgress({
|
|
445
|
+
phase: 'compress_progress',
|
|
446
|
+
loaded: i + chunk.length,
|
|
447
|
+
total: payload.length,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
payload = Buffer.concat(compressedChunks);
|
|
451
|
+
if (opts.onProgress)
|
|
452
|
+
opts.onProgress({ phase: 'compress_done', loaded: payload.length });
|
|
402
453
|
if (opts.passphrase && !opts.encrypt) {
|
|
403
454
|
opts.encrypt = 'aes';
|
|
404
455
|
}
|
|
@@ -418,6 +469,8 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
418
469
|
}
|
|
419
470
|
if (opts.passphrase && opts.encrypt && opts.encrypt !== 'auto') {
|
|
420
471
|
const encChoice = opts.encrypt;
|
|
472
|
+
if (opts.onProgress)
|
|
473
|
+
opts.onProgress({ phase: 'encrypt_start' });
|
|
421
474
|
if (encChoice === 'aes') {
|
|
422
475
|
const salt = randomBytes(16);
|
|
423
476
|
const iv = randomBytes(12);
|
|
@@ -431,14 +484,20 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
431
484
|
else if (encChoice === 'xor') {
|
|
432
485
|
const xored = applyXor(payload, opts.passphrase);
|
|
433
486
|
payload = Buffer.concat([Buffer.from([ENC_XOR]), xored]);
|
|
487
|
+
if (opts.onProgress)
|
|
488
|
+
opts.onProgress({ phase: 'encrypt_done' });
|
|
434
489
|
}
|
|
435
490
|
else if (encChoice === 'none') {
|
|
436
491
|
payload = Buffer.concat([Buffer.from([ENC_NONE]), payload]);
|
|
492
|
+
if (opts.onProgress)
|
|
493
|
+
opts.onProgress({ phase: 'encrypt_done' });
|
|
437
494
|
}
|
|
438
495
|
}
|
|
439
496
|
else {
|
|
440
497
|
payload = Buffer.concat([Buffer.from([ENC_NONE]), payload]);
|
|
441
498
|
}
|
|
499
|
+
if (opts.onProgress)
|
|
500
|
+
opts.onProgress({ phase: 'meta_prep_done', loaded: payload.length });
|
|
442
501
|
const metaParts = [];
|
|
443
502
|
const includeName = opts.includeName === undefined ? true : !!opts.includeName;
|
|
444
503
|
if (includeName && opts.name) {
|
|
@@ -532,9 +591,9 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
532
591
|
}
|
|
533
592
|
}
|
|
534
593
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
})
|
|
594
|
+
if (opts.onProgress)
|
|
595
|
+
opts.onProgress({ phase: 'png_gen' });
|
|
596
|
+
let bufScr = await sharp(raw, { raw: { width, height, channels: 3 } })
|
|
538
597
|
.png({
|
|
539
598
|
compressionLevel: 9,
|
|
540
599
|
palette: false,
|
|
@@ -542,6 +601,19 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
542
601
|
adaptiveFiltering: true,
|
|
543
602
|
})
|
|
544
603
|
.toBuffer();
|
|
604
|
+
if (opts.includeFileList && opts.fileList) {
|
|
605
|
+
const chunks = extract(bufScr);
|
|
606
|
+
const fileListChunk = {
|
|
607
|
+
name: 'rXFL',
|
|
608
|
+
data: Buffer.from(JSON.stringify(opts.fileList), 'utf8'),
|
|
609
|
+
};
|
|
610
|
+
chunks.splice(-1, 0, fileListChunk);
|
|
611
|
+
bufScr = Buffer.from(encode(chunks));
|
|
612
|
+
}
|
|
613
|
+
if (opts.onProgress)
|
|
614
|
+
opts.onProgress({ phase: 'done', loaded: bufScr.length });
|
|
615
|
+
progressBar?.stop();
|
|
616
|
+
return bufScr;
|
|
545
617
|
}
|
|
546
618
|
if (mode === 'pixel') {
|
|
547
619
|
const nameBuf = opts.name
|
|
@@ -600,10 +672,15 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
600
672
|
chunksPixel.push({ name: 'IHDR', data: ihdrData });
|
|
601
673
|
chunksPixel.push({ name: 'IDAT', data: idatData });
|
|
602
674
|
chunksPixel.push({ name: 'IEND', data: Buffer.alloc(0) });
|
|
675
|
+
if (opts.onProgress)
|
|
676
|
+
opts.onProgress({ phase: 'png_gen' });
|
|
603
677
|
const tmp = Buffer.from(encode(chunksPixel));
|
|
604
678
|
const outPng = tmp.slice(0, 8).toString('hex') === PNG_HEADER_HEX
|
|
605
679
|
? tmp
|
|
606
680
|
: Buffer.concat([PNG_HEADER, tmp]);
|
|
681
|
+
if (opts.onProgress)
|
|
682
|
+
opts.onProgress({ phase: 'done', loaded: outPng.length });
|
|
683
|
+
progressBar?.stop();
|
|
607
684
|
return outPng;
|
|
608
685
|
}
|
|
609
686
|
if (mode === 'compact') {
|
|
@@ -633,11 +710,23 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
633
710
|
chunks2.push({ name: 'IHDR', data: ihdrData });
|
|
634
711
|
chunks2.push({ name: 'IDAT', data: idatData });
|
|
635
712
|
chunks2.push({ name: CHUNK_TYPE, data: meta });
|
|
713
|
+
if (opts.includeFileList && opts.fileList) {
|
|
714
|
+
chunks2.push({
|
|
715
|
+
name: 'rXFL',
|
|
716
|
+
data: Buffer.from(JSON.stringify(opts.fileList), 'utf8'),
|
|
717
|
+
});
|
|
718
|
+
}
|
|
636
719
|
chunks2.push({ name: 'IEND', data: Buffer.alloc(0) });
|
|
720
|
+
if (opts.onProgress)
|
|
721
|
+
opts.onProgress({ phase: 'png_gen' });
|
|
637
722
|
const out = Buffer.from(encode(chunks2));
|
|
638
|
-
|
|
723
|
+
const outBuf = out.slice(0, 8).toString('hex') === PNG_HEADER_HEX
|
|
639
724
|
? out
|
|
640
725
|
: Buffer.concat([PNG_HEADER, out]);
|
|
726
|
+
if (opts.onProgress)
|
|
727
|
+
opts.onProgress({ phase: 'done', loaded: outBuf.length });
|
|
728
|
+
progressBar?.stop();
|
|
729
|
+
return outBuf;
|
|
641
730
|
}
|
|
642
731
|
throw new Error(`Unsupported mode: ${mode}`);
|
|
643
732
|
}
|
|
@@ -650,6 +739,34 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
650
739
|
* @public
|
|
651
740
|
*/
|
|
652
741
|
export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
742
|
+
let progressBar = null;
|
|
743
|
+
if (opts.showProgress) {
|
|
744
|
+
progressBar = new cliProgress.SingleBar({
|
|
745
|
+
format: ' {bar} {percentage}% | {step} | {elapsed}s',
|
|
746
|
+
}, cliProgress.Presets.shades_classic);
|
|
747
|
+
progressBar.start(100, 0, { step: 'Starting', elapsed: '0' });
|
|
748
|
+
const startTime = Date.now();
|
|
749
|
+
if (!opts.onProgress) {
|
|
750
|
+
opts.onProgress = (info) => {
|
|
751
|
+
let pct = 0;
|
|
752
|
+
if (info.phase === 'start') {
|
|
753
|
+
pct = 10;
|
|
754
|
+
}
|
|
755
|
+
else if (info.phase === 'decompress') {
|
|
756
|
+
pct = 50;
|
|
757
|
+
}
|
|
758
|
+
else if (info.phase === 'done') {
|
|
759
|
+
pct = 100;
|
|
760
|
+
}
|
|
761
|
+
progressBar.update(Math.floor(pct), {
|
|
762
|
+
step: info.phase.replace('_', ' '),
|
|
763
|
+
elapsed: String(Math.floor((Date.now() - startTime) / 1000)),
|
|
764
|
+
});
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (opts.onProgress)
|
|
769
|
+
opts.onProgress({ phase: 'start' });
|
|
653
770
|
let processedBuf = pngBuf;
|
|
654
771
|
try {
|
|
655
772
|
const info = await sharp(pngBuf).metadata();
|
|
@@ -666,6 +783,8 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
666
783
|
}
|
|
667
784
|
}
|
|
668
785
|
catch (e) { }
|
|
786
|
+
if (opts.onProgress)
|
|
787
|
+
opts.onProgress({ phase: 'processed' });
|
|
669
788
|
if (processedBuf.slice(0, MAGIC.length).equals(MAGIC)) {
|
|
670
789
|
const d = processedBuf.slice(MAGIC.length);
|
|
671
790
|
const nameLen = d[0];
|
|
@@ -677,6 +796,8 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
677
796
|
}
|
|
678
797
|
const rawPayload = d.slice(idx);
|
|
679
798
|
let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
|
|
799
|
+
if (opts.onProgress)
|
|
800
|
+
opts.onProgress({ phase: 'decompress' });
|
|
680
801
|
try {
|
|
681
802
|
payload = await tryZstdDecompress(payload);
|
|
682
803
|
}
|
|
@@ -690,6 +811,9 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
690
811
|
throw new Error('Invalid ROX format (ROX direct: missing ROX1 magic after decompression)');
|
|
691
812
|
}
|
|
692
813
|
payload = payload.slice(MAGIC.length);
|
|
814
|
+
if (opts.onProgress)
|
|
815
|
+
opts.onProgress({ phase: 'done' });
|
|
816
|
+
progressBar?.stop();
|
|
693
817
|
return { buf: payload, meta: { name } };
|
|
694
818
|
}
|
|
695
819
|
let chunks = [];
|
|
@@ -731,6 +855,8 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
731
855
|
if (rawPayload.length === 0)
|
|
732
856
|
throw new DataFormatError('Compact mode payload empty');
|
|
733
857
|
let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
|
|
858
|
+
if (opts.onProgress)
|
|
859
|
+
opts.onProgress({ phase: 'decompress' });
|
|
734
860
|
try {
|
|
735
861
|
payload = await tryZstdDecompress(payload);
|
|
736
862
|
}
|
|
@@ -744,6 +870,18 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
744
870
|
throw new DataFormatError('Invalid ROX format (compact mode: missing ROX1 magic after decompression)');
|
|
745
871
|
}
|
|
746
872
|
payload = payload.slice(MAGIC.length);
|
|
873
|
+
if (opts.files) {
|
|
874
|
+
const unpacked = unpackBuffer(payload, opts.files);
|
|
875
|
+
if (unpacked) {
|
|
876
|
+
if (opts.onProgress)
|
|
877
|
+
opts.onProgress({ phase: 'done' });
|
|
878
|
+
progressBar?.stop();
|
|
879
|
+
return { files: unpacked.files, meta: { name } };
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
if (opts.onProgress)
|
|
883
|
+
opts.onProgress({ phase: 'done' });
|
|
884
|
+
progressBar?.stop();
|
|
747
885
|
return { buf: payload, meta: { name } };
|
|
748
886
|
}
|
|
749
887
|
try {
|
|
@@ -1087,6 +1225,18 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
1087
1225
|
throw new DataFormatError('Invalid ROX format (pixel mode: missing ROX1 magic after decompression)');
|
|
1088
1226
|
}
|
|
1089
1227
|
payload = payload.slice(MAGIC.length);
|
|
1228
|
+
if (opts.files) {
|
|
1229
|
+
const unpacked = unpackBuffer(payload, opts.files);
|
|
1230
|
+
if (unpacked) {
|
|
1231
|
+
if (opts.onProgress)
|
|
1232
|
+
opts.onProgress({ phase: 'done' });
|
|
1233
|
+
progressBar?.stop();
|
|
1234
|
+
return { files: unpacked.files, meta: { name } };
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
if (opts.onProgress)
|
|
1238
|
+
opts.onProgress({ phase: 'done' });
|
|
1239
|
+
progressBar?.stop();
|
|
1090
1240
|
return { buf: payload, meta: { name } };
|
|
1091
1241
|
}
|
|
1092
1242
|
}
|
|
@@ -1111,3 +1261,24 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
1111
1261
|
}
|
|
1112
1262
|
throw new DataFormatError('No valid data found in image');
|
|
1113
1263
|
}
|
|
1264
|
+
export { packPaths, unpackBuffer } from './pack.js';
|
|
1265
|
+
/**
|
|
1266
|
+
* List files in a Rox PNG archive without decoding the full payload.
|
|
1267
|
+
* Returns the file list if available, otherwise null.
|
|
1268
|
+
* @param pngBuf - PNG data
|
|
1269
|
+
* @public
|
|
1270
|
+
*/
|
|
1271
|
+
export function listFilesInPng(pngBuf) {
|
|
1272
|
+
try {
|
|
1273
|
+
const chunks = extract(pngBuf);
|
|
1274
|
+
const fileListChunk = chunks.find((c) => c.name === 'rXFL');
|
|
1275
|
+
if (fileListChunk) {
|
|
1276
|
+
const data = Buffer.isBuffer(fileListChunk.data)
|
|
1277
|
+
? fileListChunk.data
|
|
1278
|
+
: Buffer.from(fileListChunk.data);
|
|
1279
|
+
return JSON.parse(data.toString('utf8'));
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
catch (e) { }
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
package/dist/pack.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
export interface PackedFile {
|
|
4
|
+
path: string;
|
|
5
|
+
buf: Buffer;
|
|
6
|
+
}
|
|
7
|
+
export declare function packPaths(paths: string[], baseDir?: string, onProgress?: (readBytes: number, totalBytes: number) => void): {
|
|
8
|
+
buf: Buffer;
|
|
9
|
+
list: string[];
|
|
10
|
+
};
|
|
11
|
+
export declare function unpackBuffer(buf: Buffer, fileList?: string[]): {
|
|
12
|
+
files: {
|
|
13
|
+
path: string;
|
|
14
|
+
buf: Buffer;
|
|
15
|
+
}[];
|
|
16
|
+
} | null;
|
package/dist/pack.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, relative, resolve, sep } from 'path';
|
|
3
|
+
function collectFiles(paths) {
|
|
4
|
+
const files = [];
|
|
5
|
+
for (const p of paths) {
|
|
6
|
+
const abs = resolve(p);
|
|
7
|
+
const st = statSync(abs);
|
|
8
|
+
if (st.isFile()) {
|
|
9
|
+
files.push(abs);
|
|
10
|
+
}
|
|
11
|
+
else if (st.isDirectory()) {
|
|
12
|
+
const names = readdirSync(abs);
|
|
13
|
+
const childPaths = names.map((n) => join(abs, n));
|
|
14
|
+
files.push(...collectFiles(childPaths));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return files;
|
|
18
|
+
}
|
|
19
|
+
export function packPaths(paths, baseDir, onProgress) {
|
|
20
|
+
const files = collectFiles(paths);
|
|
21
|
+
const base = baseDir ? resolve(baseDir) : process.cwd();
|
|
22
|
+
const parts = [];
|
|
23
|
+
const list = [];
|
|
24
|
+
let total = 0;
|
|
25
|
+
const sizes = files.map((f) => {
|
|
26
|
+
const st = statSync(f);
|
|
27
|
+
total += st.size;
|
|
28
|
+
return st.size;
|
|
29
|
+
});
|
|
30
|
+
let readSoFar = 0;
|
|
31
|
+
for (let idx = 0; idx < files.length; idx++) {
|
|
32
|
+
const f = files[idx];
|
|
33
|
+
const rel = relative(base, f).split(sep).join('/');
|
|
34
|
+
const content = readFileSync(f);
|
|
35
|
+
const nameBuf = Buffer.from(rel, 'utf8');
|
|
36
|
+
const nameLen = Buffer.alloc(2);
|
|
37
|
+
nameLen.writeUInt16BE(nameBuf.length, 0);
|
|
38
|
+
const sizeBuf = Buffer.alloc(8);
|
|
39
|
+
sizeBuf.writeBigUInt64BE(BigInt(content.length), 0);
|
|
40
|
+
parts.push(nameLen, nameBuf, sizeBuf, content);
|
|
41
|
+
list.push(rel);
|
|
42
|
+
readSoFar += sizes[idx];
|
|
43
|
+
if (onProgress)
|
|
44
|
+
onProgress(readSoFar, total);
|
|
45
|
+
}
|
|
46
|
+
const header = Buffer.alloc(8);
|
|
47
|
+
header.writeUInt32BE(0x524f5850, 0);
|
|
48
|
+
header.writeUInt32BE(files.length, 4);
|
|
49
|
+
parts.unshift(header);
|
|
50
|
+
return { buf: Buffer.concat(parts), list };
|
|
51
|
+
}
|
|
52
|
+
export function unpackBuffer(buf, fileList) {
|
|
53
|
+
if (buf.length < 8)
|
|
54
|
+
return null;
|
|
55
|
+
const header = buf.slice(0, 8);
|
|
56
|
+
if (header.readUInt32BE(0) !== 0x524f5850)
|
|
57
|
+
return null;
|
|
58
|
+
const fileCount = header.readUInt32BE(4);
|
|
59
|
+
let offset = 8;
|
|
60
|
+
const files = [];
|
|
61
|
+
for (let i = 0; i < fileCount; i++) {
|
|
62
|
+
if (offset + 2 > buf.length)
|
|
63
|
+
return null;
|
|
64
|
+
const nameLen = buf.readUInt16BE(offset);
|
|
65
|
+
offset += 2;
|
|
66
|
+
if (offset + nameLen > buf.length)
|
|
67
|
+
return null;
|
|
68
|
+
const name = buf.slice(offset, offset + nameLen).toString('utf8');
|
|
69
|
+
offset += nameLen;
|
|
70
|
+
if (offset + 8 > buf.length)
|
|
71
|
+
return null;
|
|
72
|
+
const size = buf.readBigUInt64BE(offset);
|
|
73
|
+
offset += 8;
|
|
74
|
+
if (offset + Number(size) > buf.length)
|
|
75
|
+
return null;
|
|
76
|
+
const content = buf.slice(offset, offset + Number(size));
|
|
77
|
+
offset += Number(size);
|
|
78
|
+
files.push({ path: name, buf: content });
|
|
79
|
+
}
|
|
80
|
+
if (fileList) {
|
|
81
|
+
const filtered = files.filter((f) => fileList.includes(f.path));
|
|
82
|
+
return { files: filtered };
|
|
83
|
+
}
|
|
84
|
+
return { files };
|
|
85
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roxify",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
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",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc",
|
|
17
17
|
"check-publish": "node ../scripts/check-publish.js roxify",
|
|
18
|
-
"cli": "node dist/cli.js"
|
|
18
|
+
"cli": "node dist/cli.js",
|
|
19
|
+
"test": "node test/pack.test.js"
|
|
19
20
|
},
|
|
20
21
|
"keywords": [
|
|
21
22
|
"steganography",
|
|
@@ -40,12 +41,14 @@
|
|
|
40
41
|
"node": ">=18"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
|
-
"@mongodb-js/zstd": "^
|
|
44
|
+
"@mongodb-js/zstd": "^2.0.1",
|
|
44
45
|
"png-chunks-encode": "^1.0.0",
|
|
45
46
|
"png-chunks-extract": "^1.0.0",
|
|
46
|
-
"sharp": "^0.34.5"
|
|
47
|
+
"sharp": "^0.34.5",
|
|
48
|
+
"cli-progress": "^3.9.1"
|
|
47
49
|
},
|
|
48
50
|
"devDependencies": {
|
|
49
|
-
"typescript": "^4.9.5"
|
|
51
|
+
"typescript": "^4.9.5",
|
|
52
|
+
"@types/cli-progress": "^3.9.0"
|
|
50
53
|
}
|
|
51
54
|
}
|