roxify 1.5.11 → 1.6.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/README.md +377 -0
- package/dist/_esmodule_test.d.ts +1 -0
- package/dist/_esmodule_test.js +1 -0
- package/dist/cli.js +63 -11
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/rox.exe +0 -0
- package/{libroxify_native-x86_64-pc-windows-gnu.node → dist/roxify_native-x86_64-pc-windows-gnu.node} +0 -0
- package/{libroxify_native-x86_64-unknown-linux-gnu.so → dist/roxify_native-x86_64-unknown-linux-gnu.node} +0 -0
- package/dist/roxify_native.exe +0 -0
- package/dist/roxify_native.node +0 -0
- package/dist/utils/decoder.js +0 -7
- package/dist/utils/encoder.d.ts +1 -2
- package/dist/utils/encoder.js +43 -2
- package/dist/utils/inspection.js +41 -41
- package/dist/utils/native.js +41 -47
- package/dist/utils/rust-cli-wrapper.d.ts +2 -0
- package/dist/utils/rust-cli-wrapper.js +167 -25
- package/libroxify_native.node +0 -0
- package/package.json +45 -15
- package/roxify_native-x86_64-pc-windows-msvc.node +0 -0
- package/roxify_native-x86_64-unknown-linux-gnu.node +0 -0
- package/roxify_native.node +0 -0
- package/dist/roxify-cli +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# RoxCompressor Transform
|
|
2
|
+
|
|
3
|
+
> Encode binary data into PNG images and decode them back. Fast, efficient, with optional encryption and native Rust acceleration.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/roxify)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- ⚡ **Blazing Fast**: Native Rust acceleration via N-API — **1GB/s** throughput on modern hardware
|
|
11
|
+
- 🚀 **Optimized Compression**: Multi-threaded Zstd compression (level 19) with parallel processing
|
|
12
|
+
- 🔒 **Secure**: AES-256-GCM encryption support with PBKDF2 key derivation
|
|
13
|
+
- 🎨 **Multiple modes**: Compact, chunk, pixel, and screenshot modes
|
|
14
|
+
- 📦 **CLI & API**: Use as command-line tool or JavaScript library
|
|
15
|
+
- 🔄 **Lossless**: Perfect roundtrip encoding/decoding
|
|
16
|
+
- 📖 **Full TSDoc**: Complete TypeScript documentation
|
|
17
|
+
- 🦀 **Rust Powered**: Optional native module for extreme performance (falls back to pure JS)
|
|
18
|
+
|
|
19
|
+
## Real-world benchmarks 🔧
|
|
20
|
+
|
|
21
|
+
**Highlights**
|
|
22
|
+
|
|
23
|
+
- Practical benchmarks on large codebase datasets showing significant compression and high throughput while handling many small files efficiently.
|
|
24
|
+
|
|
25
|
+
**Results**
|
|
26
|
+
|
|
27
|
+
| Dataset | Files | Original | Compressed | Ratio | Time | Throughput | Notes |
|
|
28
|
+
| -------- | ------: | -------: | ---------: | --------: | -----: | ---------: | ------------------------------------------- |
|
|
29
|
+
| 4,000 MB | 731,340 | 3.93 GB | 111.42 MB | **2.8%** | 26.9 s | 149.4 MB/s | gzip: 2.26 GB (57.5%); 7z: 1.87 GB (47.6%) |
|
|
30
|
+
| 1,000 MB | 141,522 | 1.03 GB | 205 MB | **19.4%** | ~6.2 s | ≈170 MB/s | shows benefits for many-small-file datasets |
|
|
31
|
+
|
|
32
|
+
### Methodology
|
|
33
|
+
|
|
34
|
+
- Compression: multithreaded Zstd (level 19) and Brotli (configurable).
|
|
35
|
+
- Setup: parallel I/O and multithreaded compression on modern SSD-backed systems.
|
|
36
|
+
- Measurements: wall-clock time; throughput = original size / time; comparisons against gzip and 7z with typical defaults.
|
|
37
|
+
- Reproducibility: full benchmark details, commands and raw data are available in `docs/BENCHMARK_FINAL_REPORT.md`.
|
|
38
|
+
|
|
39
|
+
These results demonstrate Roxify's strength for packaging large codebases and many-small-file archives where speed and a good compression/throughput trade-off matter.
|
|
40
|
+
|
|
41
|
+
## Documentation
|
|
42
|
+
|
|
43
|
+
- 📘 **[CLI Documentation](./docs/CLI.md)** - Complete command-line usage guide
|
|
44
|
+
- 📗 **[JavaScript SDK](./docs/JAVASCRIPT_SDK.md)** - Programmatic API reference with examples
|
|
45
|
+
- 📙 **[Quick Start](#quick-start)** - Get started in 2 minutes
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
### As CLI tool (npx)
|
|
50
|
+
|
|
51
|
+
No installation needed! Use directly with npx:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npx rox encode input.zip output.png
|
|
55
|
+
npx rox decode output.png original.zip
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### As library
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm install roxify
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## CLI Usage
|
|
65
|
+
|
|
66
|
+
### Quick Start
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Encode a file
|
|
70
|
+
npx rox encode document.pdf document.png
|
|
71
|
+
|
|
72
|
+
# Decode it back
|
|
73
|
+
npx rox decode document.png document.pdf
|
|
74
|
+
|
|
75
|
+
# With encryption
|
|
76
|
+
npx rox encode secret.zip secret.png -p mypassword
|
|
77
|
+
npx rox decode secret.png secret.zip -p mypassword
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### CLI Commands
|
|
81
|
+
|
|
82
|
+
#### `encode` - Encode file to PNG
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npx rox encode <input> [output] [options]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Options:**
|
|
89
|
+
|
|
90
|
+
- `-p, --passphrase <pass>` - Encrypt with passphrase (AES-256-GCM)
|
|
91
|
+
- `-m, --mode <mode>` - Encoding mode: `compact|chunk|pixel|screenshot` (default: `screenshot`)
|
|
92
|
+
- `-q, --quality <0-11>` - Brotli compression quality (default: `1`)
|
|
93
|
+
- `0` = fastest, largest
|
|
94
|
+
- `11` = slowest, smallest
|
|
95
|
+
- `-e, --encrypt <type>` - Encryption: `auto|aes|xor|none` (default: `aes` if passphrase)
|
|
96
|
+
- `--no-compress` - Disable compression
|
|
97
|
+
- `-o, --output <path>` - Output file path
|
|
98
|
+
|
|
99
|
+
**Examples:**
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Basic encoding
|
|
103
|
+
npx rox encode data.bin output.png
|
|
104
|
+
|
|
105
|
+
# Fast compression for large files
|
|
106
|
+
npx rox encode large-video.mp4 output.png -q 0
|
|
107
|
+
|
|
108
|
+
# High compression for small files
|
|
109
|
+
npx rox encode config.json output.png -q 11
|
|
110
|
+
|
|
111
|
+
# With encryption
|
|
112
|
+
npx rox encode secret.pdf secure.png -p "my secure password"
|
|
113
|
+
|
|
114
|
+
# Compact mode (smallest PNG)
|
|
115
|
+
npx rox encode data.bin tiny.png -m compact
|
|
116
|
+
|
|
117
|
+
# Screenshot mode (recommended, looks like a real image)
|
|
118
|
+
npx rox encode archive.tar.gz screenshot.png -m screenshot
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### `decode` - Decode PNG to file
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npx rox decode <input> [output] [options]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Options:**
|
|
128
|
+
|
|
129
|
+
- `-p, --passphrase <pass>` - Decryption passphrase
|
|
130
|
+
- `-o, --output <path>` - Output file path (auto-detected from metadata if not provided)
|
|
131
|
+
|
|
132
|
+
**Examples:**
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Basic decoding
|
|
136
|
+
npx rox decode encoded.png output.bin
|
|
137
|
+
|
|
138
|
+
# Auto-detect filename from metadata
|
|
139
|
+
npx rox decode encoded.png
|
|
140
|
+
|
|
141
|
+
# With decryption
|
|
142
|
+
npx rox decode encrypted.png output.pdf -p "my secure password"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## JavaScript API
|
|
146
|
+
|
|
147
|
+
### Basic Usage
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { encodeBinaryToPng, decodePngToBinary } from 'roxify';
|
|
151
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
152
|
+
|
|
153
|
+
const input = readFileSync('input.zip');
|
|
154
|
+
const png = await encodeBinaryToPng(input, {
|
|
155
|
+
mode: 'screenshot',
|
|
156
|
+
name: 'input.zip',
|
|
157
|
+
});
|
|
158
|
+
writeFileSync('output.png', png);
|
|
159
|
+
|
|
160
|
+
const encoded = readFileSync('output.png');
|
|
161
|
+
const result = await decodePngToBinary(encoded);
|
|
162
|
+
writeFileSync(result.meta?.name || 'output.bin', result.buf);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### With Encryption
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
const png = await encodeBinaryToPng(input, {
|
|
169
|
+
mode: 'screenshot',
|
|
170
|
+
passphrase: 'my-secret-password',
|
|
171
|
+
encrypt: 'aes',
|
|
172
|
+
name: 'secret.zip',
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const result = await decodePngToBinary(encoded, {
|
|
176
|
+
passphrase: 'my-secret-password',
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Fast Compression
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
const png = await encodeBinaryToPng(largeBuffer, {
|
|
184
|
+
mode: 'screenshot',
|
|
185
|
+
brQuality: 0,
|
|
186
|
+
name: 'large-file.bin',
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const png = await encodeBinaryToPng(smallBuffer, {
|
|
190
|
+
mode: 'compact',
|
|
191
|
+
brQuality: 11,
|
|
192
|
+
name: 'config.json',
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Encoding Modes
|
|
197
|
+
|
|
198
|
+
#### `screenshot` (Recommended)
|
|
199
|
+
|
|
200
|
+
Encodes data as RGB pixel values, optimized for screenshot-like appearance. Best balance of size and compatibility.
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const png = await encodeBinaryToPng(data, { mode: 'screenshot' });
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### `compact` (Smallest)
|
|
207
|
+
|
|
208
|
+
Minimal 1x1 PNG with data in custom chunk. Fastest and smallest.
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
const png = await encodeBinaryToPng(data, { mode: 'compact' });
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
#### `pixel`
|
|
215
|
+
|
|
216
|
+
Encodes data as RGB pixel values without screenshot optimization.
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
const png = await encodeBinaryToPng(data, { mode: 'pixel' });
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### `chunk`
|
|
223
|
+
|
|
224
|
+
Standard PNG with data in custom rXDT chunk.
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
const png = await encodeBinaryToPng(data, { mode: 'chunk' });
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## API Reference
|
|
231
|
+
|
|
232
|
+
### `encodeBinaryToPng(input, options)`
|
|
233
|
+
|
|
234
|
+
Encodes binary data into a PNG image.
|
|
235
|
+
|
|
236
|
+
**Parameters:**
|
|
237
|
+
|
|
238
|
+
- `input: Buffer` - The binary data to encode
|
|
239
|
+
- `options?: EncodeOptions` - Encoding options
|
|
240
|
+
|
|
241
|
+
**Returns:** `Promise<Buffer>` - The encoded PNG
|
|
242
|
+
|
|
243
|
+
**Options:**
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
interface EncodeOptions {
|
|
247
|
+
compression?: 'br' | 'none';
|
|
248
|
+
|
|
249
|
+
passphrase?: string;
|
|
250
|
+
|
|
251
|
+
name?: string;
|
|
252
|
+
|
|
253
|
+
mode?: 'compact' | 'chunk' | 'pixel' | 'screenshot';
|
|
254
|
+
|
|
255
|
+
encrypt?: 'auto' | 'aes' | 'xor' | 'none';
|
|
256
|
+
|
|
257
|
+
output?: 'auto' | 'png' | 'rox';
|
|
258
|
+
|
|
259
|
+
includeName?: boolean;
|
|
260
|
+
|
|
261
|
+
brQuality?: number;
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### `decodePngToBinary(pngBuf, options)`
|
|
266
|
+
|
|
267
|
+
Decodes a PNG image back to binary data.
|
|
268
|
+
|
|
269
|
+
**Parameters:**
|
|
270
|
+
|
|
271
|
+
- `pngBuf: Buffer` - The PNG image to decode
|
|
272
|
+
- `options?: DecodeOptions` - Decoding options
|
|
273
|
+
|
|
274
|
+
**Returns:** `Promise<DecodeResult>` - The decoded data and metadata
|
|
275
|
+
|
|
276
|
+
**Options:**
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
interface DecodeOptions {
|
|
280
|
+
passphrase?: string;
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Result:**
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
interface DecodeResult {
|
|
288
|
+
buf: Buffer;
|
|
289
|
+
|
|
290
|
+
meta?: {
|
|
291
|
+
name?: string;
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Performance Tips
|
|
297
|
+
|
|
298
|
+
### For Large Files (>10 MB)
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
# Use quality 0 for fastest encoding
|
|
302
|
+
npx rox encode large.bin output.png -q 0
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
const png = await encodeBinaryToPng(largeFile, {
|
|
307
|
+
mode: 'screenshot',
|
|
308
|
+
brQuality: 0,
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### For Small Files (<1 MB)
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
# Use quality 11 for best compression
|
|
316
|
+
npx rox encode small.json output.png -q 11 -m compact
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
const png = await encodeBinaryToPng(smallFile, {
|
|
321
|
+
mode: 'compact',
|
|
322
|
+
brQuality: 11,
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Benchmark Results
|
|
327
|
+
|
|
328
|
+
File: 3.8 MB binary
|
|
329
|
+
|
|
330
|
+
- **Quality 0**: ~500-800ms, output ~1.2 MB
|
|
331
|
+
- **Quality 1** (default): ~1-2s, output ~800 KB
|
|
332
|
+
- **Quality 5**: ~8-12s, output ~750 KB
|
|
333
|
+
- **Quality 11**: ~20-30s, output ~720 KB
|
|
334
|
+
|
|
335
|
+
## Error Handling
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
try {
|
|
339
|
+
const result = await decodePngToBinary(encoded, {
|
|
340
|
+
passphrase: 'wrong-password',
|
|
341
|
+
});
|
|
342
|
+
} catch (err) {
|
|
343
|
+
if (err.message.includes('Incorrect passphrase')) {
|
|
344
|
+
console.error('Wrong password!');
|
|
345
|
+
} else if (err.message.includes('Invalid ROX format')) {
|
|
346
|
+
console.error('Not a valid RoxCompressor PNG');
|
|
347
|
+
} else {
|
|
348
|
+
console.error('Decode failed:', err.message);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Security
|
|
354
|
+
|
|
355
|
+
- **AES-256-GCM**: Authenticated encryption with 100,000 PBKDF2 iterations
|
|
356
|
+
- **XOR cipher**: Simple obfuscation (not cryptographically secure)
|
|
357
|
+
- **No encryption**: Data is compressed but not encrypted
|
|
358
|
+
|
|
359
|
+
⚠️ **Warning**: Use strong passphrases for sensitive data. The `xor` encryption mode is not secure and should only be used for obfuscation.
|
|
360
|
+
|
|
361
|
+
## License
|
|
362
|
+
|
|
363
|
+
MIT © RoxCompressor
|
|
364
|
+
|
|
365
|
+
## Contributing
|
|
366
|
+
|
|
367
|
+
Contributions welcome! Please open an issue or PR on GitHub.
|
|
368
|
+
|
|
369
|
+
## Links
|
|
370
|
+
|
|
371
|
+
- [GitHub Repository](https://github.com/RoxasYTB/roxify)
|
|
372
|
+
- [npm Package](https://www.npmjs.com/package/roxify)
|
|
373
|
+
- [Report Issues](https://github.com/RoxasYTB/roxify/issues)
|
|
374
|
+
|
|
375
|
+
## CI / Multi-platform builds
|
|
376
|
+
|
|
377
|
+
This project runs continuous integration on Linux, Windows and macOS via GitHub Actions. Native modules are built on each platform and attached to the workflow (and release) as artifacts. On releases we also publish platform artifacts to GitHub Releases. For npm publishing, set the `NPM_TOKEN` secret in your repository settings to allow automated publishes on release.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const TEST_EMIT = 42;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const TEST_EMIT = 42;
|
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
2
|
+
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, } from 'fs';
|
|
3
3
|
import { open } from 'fs/promises';
|
|
4
4
|
import { basename, dirname, join, resolve } from 'path';
|
|
5
5
|
import { DataFormatError, decodePngToBinary, encodeBinaryToPng, hasPassphraseInPng, IncorrectPassphraseError, listFilesInPng, PassphraseRequiredError, } from './index.js';
|
|
6
6
|
import { packPathsGenerator, unpackBuffer } from './pack.js';
|
|
7
7
|
import * as cliProgress from './stub-progress.js';
|
|
8
8
|
import { encodeWithRustCLI, isRustBinaryAvailable, } from './utils/rust-cli-wrapper.js';
|
|
9
|
-
const VERSION = '1.
|
|
9
|
+
const VERSION = '1.6.1';
|
|
10
|
+
function getDirectorySize(dirPath) {
|
|
11
|
+
let totalSize = 0;
|
|
12
|
+
try {
|
|
13
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
const fullPath = join(dirPath, entry.name);
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
totalSize += getDirectorySize(fullPath);
|
|
18
|
+
}
|
|
19
|
+
else if (entry.isFile()) {
|
|
20
|
+
try {
|
|
21
|
+
totalSize += statSync(fullPath).size;
|
|
22
|
+
}
|
|
23
|
+
catch (e) { }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (e) { }
|
|
28
|
+
return totalSize;
|
|
29
|
+
}
|
|
10
30
|
async function readLargeFile(filePath) {
|
|
11
31
|
const st = statSync(filePath);
|
|
12
32
|
if (st.size <= 2 * 1024 * 1024 * 1024) {
|
|
@@ -177,7 +197,6 @@ async function encodeCommand(args) {
|
|
|
177
197
|
safeCwd = process.cwd();
|
|
178
198
|
}
|
|
179
199
|
catch (e) {
|
|
180
|
-
// ENOENT: fallback sur racine
|
|
181
200
|
safeCwd = '/';
|
|
182
201
|
}
|
|
183
202
|
const resolvedInputs = inputPaths.map((p) => resolve(safeCwd, p));
|
|
@@ -195,7 +214,6 @@ async function encodeCommand(args) {
|
|
|
195
214
|
catch (e) {
|
|
196
215
|
resolvedOutput = join('/', parsed.output || outputPath || outputName);
|
|
197
216
|
}
|
|
198
|
-
// Check for empty directories *before* attempting native Rust encoder.
|
|
199
217
|
try {
|
|
200
218
|
const anyDir = inputPaths.some((p) => {
|
|
201
219
|
try {
|
|
@@ -214,10 +232,15 @@ async function encodeCommand(args) {
|
|
|
214
232
|
}
|
|
215
233
|
}
|
|
216
234
|
}
|
|
235
|
+
catch (e) { }
|
|
236
|
+
let anyInputDir = false;
|
|
237
|
+
try {
|
|
238
|
+
anyInputDir = resolvedInputs.some((p) => statSync(p).isDirectory());
|
|
239
|
+
}
|
|
217
240
|
catch (e) {
|
|
218
|
-
|
|
241
|
+
anyInputDir = false;
|
|
219
242
|
}
|
|
220
|
-
if (isRustBinaryAvailable() && !parsed.forceTs) {
|
|
243
|
+
if (isRustBinaryAvailable() && !parsed.forceTs && !anyInputDir) {
|
|
221
244
|
try {
|
|
222
245
|
console.log(`Encoding to ${resolvedOutput} (Using native Rust encoder)\n`);
|
|
223
246
|
const startTime = Date.now();
|
|
@@ -246,11 +269,7 @@ async function encodeCommand(args) {
|
|
|
246
269
|
let inputSize = 0;
|
|
247
270
|
if (inputPaths.length === 1 &&
|
|
248
271
|
fstatSync(resolvedInputs[0]).isDirectory()) {
|
|
249
|
-
|
|
250
|
-
const sizeOutput = execSync(`du -sb "${resolvedInputs[0]}"`, {
|
|
251
|
-
encoding: 'utf-8',
|
|
252
|
-
});
|
|
253
|
-
inputSize = parseInt(sizeOutput.split(/\s+/)[0]);
|
|
272
|
+
inputSize = getDirectorySize(resolvedInputs[0]);
|
|
254
273
|
}
|
|
255
274
|
else {
|
|
256
275
|
inputSize = fstatSync(resolvedInputs[0]).size;
|
|
@@ -656,6 +675,39 @@ async function listCommand(args) {
|
|
|
656
675
|
process.exit(1);
|
|
657
676
|
}
|
|
658
677
|
const resolvedInput = resolve(inputPath);
|
|
678
|
+
if (isRustBinaryAvailable()) {
|
|
679
|
+
try {
|
|
680
|
+
const { findRustBinary } = await import('./utils/rust-cli-wrapper.js');
|
|
681
|
+
const cliPath = findRustBinary();
|
|
682
|
+
if (cliPath) {
|
|
683
|
+
const { execSync } = await import('child_process');
|
|
684
|
+
try {
|
|
685
|
+
const help = execSync(`"${cliPath}" --help`, { encoding: 'utf-8' });
|
|
686
|
+
if (!help.includes('list')) {
|
|
687
|
+
throw new Error('native CLI does not support list');
|
|
688
|
+
}
|
|
689
|
+
const output = execSync(`"${cliPath}" list "${resolvedInput}"`, {
|
|
690
|
+
encoding: 'utf-8',
|
|
691
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
692
|
+
timeout: 30000,
|
|
693
|
+
});
|
|
694
|
+
const fileList = JSON.parse(output.trim());
|
|
695
|
+
console.log(`Files in ${resolvedInput}:`);
|
|
696
|
+
for (const file of fileList) {
|
|
697
|
+
if (typeof file === 'string') {
|
|
698
|
+
console.log(` ${file}`);
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
console.log(` ${file.name} (${file.size} bytes)`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
catch (e) { }
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
catch (err) { }
|
|
710
|
+
}
|
|
659
711
|
try {
|
|
660
712
|
const inputBuffer = readFileSync(resolvedInput);
|
|
661
713
|
const fileList = await listFilesInPng(inputBuffer, {
|
package/dist/index.d.ts
CHANGED
|
@@ -5,9 +5,10 @@ export * from './utils/encoder.js';
|
|
|
5
5
|
export * from './utils/errors.js';
|
|
6
6
|
export * from './utils/helpers.js';
|
|
7
7
|
export * from './utils/inspection.js';
|
|
8
|
+
export { native } from './utils/native.js';
|
|
8
9
|
export * from './utils/optimization.js';
|
|
9
10
|
export * from './utils/reconstitution.js';
|
|
11
|
+
export { encodeWithRustCLI, isRustBinaryAvailable, } from './utils/rust-cli-wrapper.js';
|
|
10
12
|
export * from './utils/types.js';
|
|
11
13
|
export * from './utils/zstd.js';
|
|
12
|
-
export { decodeMinPng, encodeMinPng } from './minpng.js';
|
|
13
14
|
export { packPaths, packPathsToParts, unpackBuffer } from './pack.js';
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,10 @@ export * from './utils/encoder.js';
|
|
|
5
5
|
export * from './utils/errors.js';
|
|
6
6
|
export * from './utils/helpers.js';
|
|
7
7
|
export * from './utils/inspection.js';
|
|
8
|
+
export { native } from './utils/native.js';
|
|
8
9
|
export * from './utils/optimization.js';
|
|
9
10
|
export * from './utils/reconstitution.js';
|
|
11
|
+
export { encodeWithRustCLI, isRustBinaryAvailable, } from './utils/rust-cli-wrapper.js';
|
|
10
12
|
export * from './utils/types.js';
|
|
11
13
|
export * from './utils/zstd.js';
|
|
12
|
-
export { decodeMinPng, encodeMinPng } from './minpng.js';
|
|
13
14
|
export { packPaths, packPathsToParts, unpackBuffer } from './pack.js';
|
package/dist/rox.exe
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/utils/decoder.js
CHANGED
|
@@ -697,7 +697,6 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
697
697
|
throw new IncorrectPassphraseError(`Incorrect passphrase (screenshot mode, zstd failed: ` +
|
|
698
698
|
errMsg +
|
|
699
699
|
')');
|
|
700
|
-
// Fallback: try reconstituting the image and re-extracting the pixels
|
|
701
700
|
try {
|
|
702
701
|
if (process.env.ROX_DEBUG)
|
|
703
702
|
console.log('DEBUG: decompress failed, attempting cropAndReconstitute fallback');
|
|
@@ -706,7 +705,6 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
706
705
|
let logicalData2 = Buffer.from(raw2.pixels);
|
|
707
706
|
let logicalWidth2 = raw2.width;
|
|
708
707
|
let logicalHeight2 = raw2.height;
|
|
709
|
-
// find startIdx2 (linear)
|
|
710
708
|
let startIdx2 = -1;
|
|
711
709
|
const totalPixels2 = (logicalData2.length / 3) | 0;
|
|
712
710
|
for (let i2 = 0; i2 <= totalPixels2 - MARKER_START.length; i2++) {
|
|
@@ -725,7 +723,6 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
725
723
|
}
|
|
726
724
|
}
|
|
727
725
|
if (startIdx2 === -1) {
|
|
728
|
-
// try 2D scan
|
|
729
726
|
let found2D2 = false;
|
|
730
727
|
for (let y = 0; y < logicalHeight2 && !found2D2; y++) {
|
|
731
728
|
for (let x = 0; x <= logicalWidth2 - MARKER_START.length && !found2D2; x++) {
|
|
@@ -740,7 +737,6 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
740
737
|
}
|
|
741
738
|
}
|
|
742
739
|
if (match) {
|
|
743
|
-
// compute rectangle
|
|
744
740
|
let endX = x + MARKER_START.length - 1;
|
|
745
741
|
let endY = y;
|
|
746
742
|
for (let scanY = y; scanY < logicalHeight2; scanY++) {
|
|
@@ -800,7 +796,6 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
800
796
|
if (!found2D2)
|
|
801
797
|
throw new DataFormatError('Screenshot fallback failed: START not found');
|
|
802
798
|
}
|
|
803
|
-
// compute endStartPixel2
|
|
804
799
|
const curTotalPixels2 = (logicalData2.length / 3) | 0;
|
|
805
800
|
const lastLineStart2 = (logicalHeight2 - 1) * logicalWidth2;
|
|
806
801
|
const endMarkerStartCol2 = logicalWidth2 - MARKER_END.length;
|
|
@@ -843,7 +838,6 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
843
838
|
pixelBytes2[dstOffset + 1] = logicalData2[srcOffset + 1];
|
|
844
839
|
pixelBytes2[dstOffset + 2] = logicalData2[srcOffset + 2];
|
|
845
840
|
}
|
|
846
|
-
// try decompressing fallback payload
|
|
847
841
|
const foundPX = pixelBytes2.indexOf(PIXEL_MAGIC);
|
|
848
842
|
if (process.env.ROX_DEBUG)
|
|
849
843
|
console.log('DEBUG: PIXEL_MAGIC index in fallback:', foundPX);
|
|
@@ -891,7 +885,6 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
891
885
|
throw new DataFormatError('Screenshot mode zstd decompression failed: ' + errMsg);
|
|
892
886
|
}
|
|
893
887
|
catch (e2) {
|
|
894
|
-
// If fallback fails, rethrow original error
|
|
895
888
|
throw new DataFormatError(`Screenshot mode zstd decompression failed: ` + errMsg);
|
|
896
889
|
}
|
|
897
890
|
}
|
package/dist/utils/encoder.d.ts
CHANGED
|
@@ -17,7 +17,6 @@ import { EncodeOptions } from './types.js';
|
|
|
17
17
|
* outputFormat: 'png',
|
|
18
18
|
* });
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
* ```
|
|
20
|
+
* * ```
|
|
22
21
|
*/
|
|
23
22
|
export declare function encodeBinaryToPng(input: Buffer | Buffer[], opts?: EncodeOptions): Promise<Buffer>;
|
package/dist/utils/encoder.js
CHANGED
|
@@ -24,8 +24,7 @@ import { parallelZstdCompress } from './zstd.js';
|
|
|
24
24
|
* outputFormat: 'png',
|
|
25
25
|
* });
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
* ```
|
|
27
|
+
* * ```
|
|
29
28
|
*/
|
|
30
29
|
export async function encodeBinaryToPng(input, opts = {}) {
|
|
31
30
|
let progressBar = null;
|
|
@@ -149,6 +148,48 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
149
148
|
const payloadTotalLen = payload.reduce((a, b) => a + b.length, 0);
|
|
150
149
|
if (opts.onProgress)
|
|
151
150
|
opts.onProgress({ phase: 'meta_prep_done', loaded: payloadTotalLen });
|
|
151
|
+
if (typeof native.nativeEncodePngWithNameAndFilelist === 'function' &&
|
|
152
|
+
opts.includeFileList &&
|
|
153
|
+
opts.fileList) {
|
|
154
|
+
const fileName = opts.name || undefined;
|
|
155
|
+
let sizeMap = null;
|
|
156
|
+
if (!Array.isArray(input)) {
|
|
157
|
+
try {
|
|
158
|
+
const unpack = unpackBuffer(input);
|
|
159
|
+
if (unpack) {
|
|
160
|
+
sizeMap = {};
|
|
161
|
+
for (const ef of unpack.files)
|
|
162
|
+
sizeMap[ef.path] = ef.buf.length;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (e) { }
|
|
166
|
+
}
|
|
167
|
+
const normalized = opts.fileList.map((f) => {
|
|
168
|
+
if (typeof f === 'string')
|
|
169
|
+
return { name: f, size: sizeMap && sizeMap[f] ? sizeMap[f] : 0 };
|
|
170
|
+
if (f && typeof f === 'object') {
|
|
171
|
+
if (f.name)
|
|
172
|
+
return { name: f.name, size: f.size ?? 0 };
|
|
173
|
+
if (f.path)
|
|
174
|
+
return { name: f.path, size: f.size ?? 0 };
|
|
175
|
+
}
|
|
176
|
+
return { name: String(f), size: 0 };
|
|
177
|
+
});
|
|
178
|
+
const fileListJson = JSON.stringify(normalized);
|
|
179
|
+
const flatPayload = Buffer.concat(payload);
|
|
180
|
+
if (opts.passphrase && opts.encrypt && opts.encrypt !== 'auto') {
|
|
181
|
+
const result = native.nativeEncodePngWithEncryptionNameAndFilelist(flatPayload, compressionLevel, opts.passphrase, opts.encrypt, fileName, fileListJson);
|
|
182
|
+
if (opts.onProgress)
|
|
183
|
+
opts.onProgress({ phase: 'done' });
|
|
184
|
+
return Buffer.from(result);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const result = native.nativeEncodePngWithNameAndFilelist(flatPayload, compressionLevel, fileName, fileListJson);
|
|
188
|
+
if (opts.onProgress)
|
|
189
|
+
opts.onProgress({ phase: 'done' });
|
|
190
|
+
return Buffer.from(result);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
152
193
|
const metaParts = [];
|
|
153
194
|
const includeName = opts.includeName === undefined ? true : !!opts.includeName;
|
|
154
195
|
if (includeName && opts.name) {
|
package/dist/utils/inspection.js
CHANGED
|
@@ -23,6 +23,46 @@ import { cropAndReconstitute } from './reconstitution.js';
|
|
|
23
23
|
export async function listFilesInPng(pngBuf, opts = {}) {
|
|
24
24
|
try {
|
|
25
25
|
const chunks = native.extractPngChunks(pngBuf);
|
|
26
|
+
const fileListChunk = chunks.find((c) => c.name === 'rXFL');
|
|
27
|
+
if (fileListChunk) {
|
|
28
|
+
const data = Buffer.from(fileListChunk.data);
|
|
29
|
+
const parsedFiles = JSON.parse(data.toString('utf8'));
|
|
30
|
+
if (parsedFiles.length > 0 &&
|
|
31
|
+
typeof parsedFiles[0] === 'object' &&
|
|
32
|
+
(parsedFiles[0].name || parsedFiles[0].path)) {
|
|
33
|
+
const objs = parsedFiles.map((p) => ({
|
|
34
|
+
name: p.name ?? p.path,
|
|
35
|
+
size: typeof p.size === 'number' ? p.size : 0,
|
|
36
|
+
}));
|
|
37
|
+
return objs.sort((a, b) => a.name.localeCompare(b.name));
|
|
38
|
+
}
|
|
39
|
+
const files = parsedFiles;
|
|
40
|
+
return files.sort();
|
|
41
|
+
}
|
|
42
|
+
const metaChunk = chunks.find((c) => c.name === CHUNK_TYPE);
|
|
43
|
+
if (metaChunk) {
|
|
44
|
+
const dataBuf = Buffer.from(metaChunk.data);
|
|
45
|
+
const markerIdx = dataBuf.indexOf(Buffer.from('rXFL'));
|
|
46
|
+
if (markerIdx !== -1 && markerIdx + 8 <= dataBuf.length) {
|
|
47
|
+
const jsonLen = dataBuf.readUInt32BE(markerIdx + 4);
|
|
48
|
+
const jsonStart = markerIdx + 8;
|
|
49
|
+
const jsonEnd = jsonStart + jsonLen;
|
|
50
|
+
if (jsonEnd <= dataBuf.length) {
|
|
51
|
+
const parsedFiles = JSON.parse(dataBuf.slice(jsonStart, jsonEnd).toString('utf8'));
|
|
52
|
+
if (parsedFiles.length > 0 &&
|
|
53
|
+
typeof parsedFiles[0] === 'object' &&
|
|
54
|
+
(parsedFiles[0].name || parsedFiles[0].path)) {
|
|
55
|
+
const objs = parsedFiles.map((p) => ({
|
|
56
|
+
name: p.name ?? p.path,
|
|
57
|
+
size: typeof p.size === 'number' ? p.size : 0,
|
|
58
|
+
}));
|
|
59
|
+
return objs.sort((a, b) => a.name.localeCompare(b.name));
|
|
60
|
+
}
|
|
61
|
+
const files = parsedFiles;
|
|
62
|
+
return files.sort();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
26
66
|
const ihdr = chunks.find((c) => c.name === 'IHDR');
|
|
27
67
|
const idatChunks = chunks.filter((c) => c.name === 'IDAT');
|
|
28
68
|
if (ihdr && idatChunks.length > 0) {
|
|
@@ -101,23 +141,7 @@ export async function listFilesInPng(pngBuf, opts = {}) {
|
|
|
101
141
|
return;
|
|
102
142
|
}
|
|
103
143
|
const names = parsedFiles;
|
|
104
|
-
|
|
105
|
-
getFileSizesFromPng(pngBuf)
|
|
106
|
-
.then((sizes) => {
|
|
107
|
-
if (sizes) {
|
|
108
|
-
resolve(names
|
|
109
|
-
.map((f) => ({ name: f, size: sizes[f] ?? 0 }))
|
|
110
|
-
.sort((a, b) => a.name.localeCompare(b.name)));
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
resolve(names.sort());
|
|
114
|
-
}
|
|
115
|
-
})
|
|
116
|
-
.catch(() => resolve(names.sort()));
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
resolve(names.sort());
|
|
120
|
-
}
|
|
144
|
+
resolve(names.sort());
|
|
121
145
|
}
|
|
122
146
|
catch (e) {
|
|
123
147
|
resolved = true;
|
|
@@ -202,14 +226,6 @@ export async function listFilesInPng(pngBuf, opts = {}) {
|
|
|
202
226
|
return objs.sort((a, b) => a.name.localeCompare(b.name));
|
|
203
227
|
}
|
|
204
228
|
const files = parsedFiles;
|
|
205
|
-
if (opts.includeSizes) {
|
|
206
|
-
const sizes = await getFileSizesFromPng(pngBuf);
|
|
207
|
-
if (sizes) {
|
|
208
|
-
return files
|
|
209
|
-
.map((f) => ({ name: f, size: sizes[f] ?? 0 }))
|
|
210
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
229
|
return files.sort();
|
|
214
230
|
}
|
|
215
231
|
}
|
|
@@ -272,14 +288,6 @@ export async function listFilesInPng(pngBuf, opts = {}) {
|
|
|
272
288
|
return objs.sort((a, b) => a.name.localeCompare(b.name));
|
|
273
289
|
}
|
|
274
290
|
const files = parsedFiles;
|
|
275
|
-
if (opts.includeSizes) {
|
|
276
|
-
const sizes = await getFileSizesFromPng(reconstructed);
|
|
277
|
-
if (sizes) {
|
|
278
|
-
return files
|
|
279
|
-
.map((f) => ({ name: f, size: sizes[f] ?? 0 }))
|
|
280
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
291
|
return files.sort();
|
|
284
292
|
}
|
|
285
293
|
}
|
|
@@ -360,14 +368,6 @@ export async function listFilesInPng(pngBuf, opts = {}) {
|
|
|
360
368
|
return objs.sort((a, b) => a.name.localeCompare(b.name));
|
|
361
369
|
}
|
|
362
370
|
const files = parsedFiles;
|
|
363
|
-
if (opts.includeSizes) {
|
|
364
|
-
const sizes = await getFileSizesFromPng(pngBuf);
|
|
365
|
-
if (sizes) {
|
|
366
|
-
return files
|
|
367
|
-
.map((f) => ({ name: f, size: sizes[f] ?? 0 }))
|
|
368
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
371
|
return files.sort();
|
|
372
372
|
}
|
|
373
373
|
const metaChunk = chunks.find((c) => c.name === CHUNK_TYPE);
|
package/dist/utils/native.js
CHANGED
|
@@ -1,35 +1,21 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
2
|
import { createRequire } from 'module';
|
|
3
3
|
import { arch, platform } from 'os';
|
|
4
|
-
import {
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
4
|
+
import { join, resolve } from 'path';
|
|
6
5
|
function getNativeModule() {
|
|
7
6
|
let moduleDir;
|
|
8
7
|
let nativeRequire;
|
|
9
8
|
if (typeof __dirname !== 'undefined') {
|
|
10
|
-
// Mode CJS - variables globales disponibles
|
|
11
9
|
moduleDir = __dirname;
|
|
12
|
-
// @ts-ignore
|
|
13
10
|
nativeRequire = require;
|
|
14
11
|
}
|
|
15
12
|
else {
|
|
16
|
-
|
|
13
|
+
moduleDir = process.cwd();
|
|
17
14
|
try {
|
|
18
|
-
|
|
19
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
-
moduleDir = dirname(__filename);
|
|
21
|
-
nativeRequire = createRequire(import.meta.url);
|
|
15
|
+
nativeRequire = require;
|
|
22
16
|
}
|
|
23
17
|
catch {
|
|
24
|
-
|
|
25
|
-
moduleDir = process.cwd();
|
|
26
|
-
try {
|
|
27
|
-
// @ts-ignore
|
|
28
|
-
nativeRequire = require;
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
nativeRequire = createRequire(process.cwd());
|
|
32
|
-
}
|
|
18
|
+
nativeRequire = createRequire(process.cwd() + '/package.json');
|
|
33
19
|
}
|
|
34
20
|
}
|
|
35
21
|
function getNativePath() {
|
|
@@ -53,10 +39,12 @@ function getNativeModule() {
|
|
|
53
39
|
if (!target || !ext) {
|
|
54
40
|
throw new Error(`Unsupported platform: ${currentPlatform}`);
|
|
55
41
|
}
|
|
56
|
-
const prebuiltPath = join(moduleDir, '../../
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
42
|
+
const prebuiltPath = join(moduleDir, '../../roxify_native.node');
|
|
43
|
+
const prebuiltLibPath = join(moduleDir, '../../libroxify_native.node');
|
|
44
|
+
const bundlePath = join(moduleDir, '../roxify_native.node');
|
|
45
|
+
const bundleLibPath = join(moduleDir, '../libroxify_native.node');
|
|
46
|
+
const bundlePathWithTarget = join(moduleDir, `../roxify_native-${target}.node`);
|
|
47
|
+
const bundleLibPathWithTarget = join(moduleDir, `../libroxify_native-${target}.node`);
|
|
60
48
|
console.debug('[native] moduleDir', moduleDir);
|
|
61
49
|
let root = moduleDir && moduleDir !== '.' ? moduleDir : process.cwd();
|
|
62
50
|
while (root.length > 1 &&
|
|
@@ -67,48 +55,54 @@ function getNativeModule() {
|
|
|
67
55
|
break;
|
|
68
56
|
root = parent;
|
|
69
57
|
}
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
58
|
+
const bundleNode = resolve(moduleDir, '../roxify_native.node');
|
|
59
|
+
const bundleLibNode = resolve(moduleDir, '../libroxify_native.node');
|
|
60
|
+
const bundleNodeWithTarget = resolve(moduleDir, `../roxify_native-${target}.node`);
|
|
61
|
+
const bundleLibNodeWithTarget = resolve(moduleDir, `../libroxify_native-${target}.node`);
|
|
62
|
+
const repoNode = resolve(root, 'roxify_native.node');
|
|
63
|
+
const repoLibNode = resolve(root, 'libroxify_native.node');
|
|
64
|
+
const repoNodeWithTarget = resolve(root, `roxify_native-${target}.node`);
|
|
65
|
+
const repoLibNodeWithTarget = resolve(root, `libroxify_native-${target}.node`);
|
|
66
|
+
const targetNode = resolve(root, 'target/release/roxify_native.node');
|
|
67
|
+
const targetSo = resolve(root, 'target/release/roxify_native.so');
|
|
68
|
+
const targetLibSo = resolve(root, 'target/release/libroxify_native.so');
|
|
69
|
+
const nodeModulesNode = resolve(root, 'node_modules/roxify/roxify_native.node');
|
|
70
|
+
const nodeModulesNodeWithTarget = resolve(root, `node_modules/roxify/roxify_native-${target}.node`);
|
|
71
|
+
const prebuiltNode = resolve(moduleDir, '../../roxify_native.node');
|
|
72
|
+
const prebuiltLibNode = resolve(moduleDir, '../../libroxify_native.node');
|
|
73
|
+
const prebuiltNodeWithTarget = resolve(moduleDir, `../../roxify_native-${target}.node`);
|
|
74
|
+
const prebuiltLibNodeWithTarget = resolve(moduleDir, `../../libroxify_native-${target}.node`);
|
|
75
|
+
// Support multiple possible OS triples (e.g. windows-gnu and windows-msvc)
|
|
76
|
+
const targets = targetAlt ? [target, targetAlt] : [target];
|
|
77
|
+
const candidates = [];
|
|
78
|
+
for (const t of targets) {
|
|
79
|
+
const bundleNodeWithT = resolve(moduleDir, `../roxify_native-${t}.node`);
|
|
80
|
+
const bundleLibNodeWithT = resolve(moduleDir, `../libroxify_native-${t}.node`);
|
|
81
|
+
const repoNodeWithT = resolve(root, `roxify_native-${t}.node`);
|
|
82
|
+
const repoLibNodeWithT = resolve(root, `libroxify_native-${t}.node`);
|
|
83
|
+
const nodeModulesNodeWithT = resolve(root, `node_modules/roxify/roxify_native-${t}.node`);
|
|
84
|
+
const prebuiltNodeWithT = resolve(moduleDir, `../../roxify_native-${t}.node`);
|
|
85
|
+
const prebuiltLibNodeWithT = resolve(moduleDir, `../../libroxify_native-${t}.node`);
|
|
86
|
+
candidates.push(bundleLibNodeWithT, bundleNodeWithT, repoLibNodeWithT, repoNodeWithT, nodeModulesNodeWithT, prebuiltLibNodeWithT, prebuiltNodeWithT);
|
|
87
|
+
}
|
|
88
|
+
candidates.push(bundleLibNode, bundleNode, repoLibNode, repoNode, targetNode, targetLibSo, targetSo, nodeModulesNode, prebuiltLibNode, prebuiltNode);
|
|
87
89
|
for (const c of candidates) {
|
|
88
90
|
try {
|
|
89
91
|
if (!existsSync(c))
|
|
90
92
|
continue;
|
|
91
|
-
// If it's a .so (native build) but Node expects .node extension, create a .node symlink
|
|
92
93
|
if (c.endsWith('.so')) {
|
|
93
94
|
const nodeAlias = c.replace(/\.so$/, '.node');
|
|
94
95
|
try {
|
|
95
96
|
if (!existsSync(nodeAlias)) {
|
|
96
|
-
// copy the .so to a .node so Node treats it as a native addon
|
|
97
|
-
// @ts-ignore
|
|
98
97
|
require('fs').copyFileSync(c, nodeAlias);
|
|
99
98
|
}
|
|
100
|
-
// debug
|
|
101
|
-
// @ts-ignore
|
|
102
99
|
console.debug('[native] using node alias', nodeAlias);
|
|
103
100
|
return nodeAlias;
|
|
104
101
|
}
|
|
105
102
|
catch (e) {
|
|
106
|
-
// fallback to original .so (might fail to load via require)
|
|
107
103
|
return c;
|
|
108
104
|
}
|
|
109
105
|
}
|
|
110
|
-
// debug
|
|
111
|
-
// @ts-ignore
|
|
112
106
|
console.debug('[native] using path', c);
|
|
113
107
|
return c;
|
|
114
108
|
}
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
declare function findRustBinary(): string | null;
|
|
2
|
+
export { findRustBinary };
|
|
1
3
|
export declare function isRustBinaryAvailable(): boolean;
|
|
2
4
|
export declare function encodeWithRustCLI(inputPath: string, outputPath: string, compressionLevel?: number, passphrase?: string, encryptType?: 'aes' | 'xor', name?: string): Promise<void>;
|
|
@@ -1,21 +1,11 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
1
|
+
import { execSync, spawn } from 'child_process';
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
3
|
import { dirname, join } from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
4
|
let moduleDir;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (typeof __dirname !== 'undefined') {
|
|
9
|
-
// @ts-ignore
|
|
10
|
-
moduleDir = __dirname;
|
|
11
|
-
}
|
|
12
|
-
else {
|
|
13
|
-
// @ts-ignore - import.meta.url exists in ESM
|
|
14
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
-
moduleDir = dirname(__filename);
|
|
16
|
-
}
|
|
5
|
+
if (typeof __dirname !== 'undefined') {
|
|
6
|
+
moduleDir = __dirname;
|
|
17
7
|
}
|
|
18
|
-
|
|
8
|
+
else {
|
|
19
9
|
moduleDir = process.cwd();
|
|
20
10
|
}
|
|
21
11
|
function findRustBinary() {
|
|
@@ -23,7 +13,85 @@ function findRustBinary() {
|
|
|
23
13
|
? ['roxify_native.exe', 'roxify-cli.exe', 'roxify_cli.exe']
|
|
24
14
|
: ['roxify_native', 'roxify-cli', 'roxify_cli'];
|
|
25
15
|
const baseDir = typeof moduleDir !== 'undefined' ? moduleDir : process.cwd();
|
|
26
|
-
|
|
16
|
+
if (process.pkg) {
|
|
17
|
+
const snapshotPaths = [
|
|
18
|
+
join(baseDir, '..', '..', 'target', 'release'),
|
|
19
|
+
join(baseDir, '..', 'target', 'release'),
|
|
20
|
+
join(baseDir, 'target', 'release'),
|
|
21
|
+
];
|
|
22
|
+
for (const basePath of snapshotPaths) {
|
|
23
|
+
for (const name of binNames) {
|
|
24
|
+
const binPath = join(basePath, name);
|
|
25
|
+
if (existsSync(binPath)) {
|
|
26
|
+
return binPath;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const execDir = require('path').dirname(process.execPath || '');
|
|
32
|
+
if (execDir) {
|
|
33
|
+
const execCandidates = [
|
|
34
|
+
join(execDir, 'tools', 'roxify', 'dist'),
|
|
35
|
+
join(execDir, 'tools', 'roxify'),
|
|
36
|
+
join(execDir, '..', 'tools', 'roxify', 'dist'),
|
|
37
|
+
join(execDir, '..', 'tools', 'roxify'),
|
|
38
|
+
join(execDir, 'tools', 'roxify', 'roxify_native.exe'),
|
|
39
|
+
];
|
|
40
|
+
for (const c of execCandidates) {
|
|
41
|
+
for (const name of binNames) {
|
|
42
|
+
const p = c.endsWith(name) ? c : join(c, name);
|
|
43
|
+
if (existsSync(p)) {
|
|
44
|
+
return p;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (e) { }
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
let paths = [];
|
|
54
|
+
if (process.platform === 'win32') {
|
|
55
|
+
try {
|
|
56
|
+
const out = execSync('where rox', { encoding: 'utf-8' }).trim();
|
|
57
|
+
if (out)
|
|
58
|
+
paths = out
|
|
59
|
+
.split(/\r?\n/)
|
|
60
|
+
.map((s) => s.trim())
|
|
61
|
+
.filter(Boolean);
|
|
62
|
+
}
|
|
63
|
+
catch (e) { }
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
try {
|
|
67
|
+
const out = execSync('which rox', { encoding: 'utf-8' }).trim();
|
|
68
|
+
if (out)
|
|
69
|
+
paths = [out.trim()];
|
|
70
|
+
}
|
|
71
|
+
catch (e) { }
|
|
72
|
+
}
|
|
73
|
+
for (const p of paths) {
|
|
74
|
+
try {
|
|
75
|
+
const d = dirname(p);
|
|
76
|
+
const candidates = [
|
|
77
|
+
d,
|
|
78
|
+
join(d, 'dist'),
|
|
79
|
+
join(d, '..', 'dist'),
|
|
80
|
+
join(d, '..'),
|
|
81
|
+
];
|
|
82
|
+
for (const c of candidates) {
|
|
83
|
+
for (const name of binNames) {
|
|
84
|
+
const candidate = join(c, name);
|
|
85
|
+
if (existsSync(candidate)) {
|
|
86
|
+
return candidate;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (e) { }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (e) { }
|
|
27
95
|
for (const name of binNames) {
|
|
28
96
|
const local = join(baseDir, name);
|
|
29
97
|
if (existsSync(local)) {
|
|
@@ -33,8 +101,15 @@ function findRustBinary() {
|
|
|
33
101
|
if (existsSync(parentLocal)) {
|
|
34
102
|
return parentLocal;
|
|
35
103
|
}
|
|
104
|
+
const parentParentLocal = join(baseDir, '..', '..', name);
|
|
105
|
+
if (existsSync(parentParentLocal)) {
|
|
106
|
+
return parentParentLocal;
|
|
107
|
+
}
|
|
108
|
+
const nodeModulesPath = join(baseDir, '..', '..', '..', '..', name);
|
|
109
|
+
if (existsSync(nodeModulesPath)) {
|
|
110
|
+
return nodeModulesPath;
|
|
111
|
+
}
|
|
36
112
|
}
|
|
37
|
-
// Check target/release (for development)
|
|
38
113
|
const targetRelease = join(baseDir, '..', '..', 'target', 'release');
|
|
39
114
|
for (const name of binNames) {
|
|
40
115
|
const targetPath = join(targetRelease, name);
|
|
@@ -44,33 +119,100 @@ function findRustBinary() {
|
|
|
44
119
|
}
|
|
45
120
|
return null;
|
|
46
121
|
}
|
|
122
|
+
export { findRustBinary };
|
|
47
123
|
export function isRustBinaryAvailable() {
|
|
48
124
|
return findRustBinary() !== null;
|
|
49
125
|
}
|
|
126
|
+
import { chmodSync, mkdtempSync, readFileSync, unlinkSync, writeFileSync, } from 'fs';
|
|
127
|
+
import { tmpdir } from 'os';
|
|
50
128
|
export async function encodeWithRustCLI(inputPath, outputPath, compressionLevel = 3, passphrase, encryptType = 'aes', name) {
|
|
51
129
|
const cliPath = findRustBinary();
|
|
52
130
|
if (!cliPath) {
|
|
53
131
|
throw new Error('Rust CLI binary not found');
|
|
54
132
|
}
|
|
133
|
+
function extractToTemp(pathToRead) {
|
|
134
|
+
const buf = readFileSync(pathToRead);
|
|
135
|
+
const tmp = mkdtempSync(join(tmpdir(), 'roxify-'));
|
|
136
|
+
const dest = join(tmp, pathToRead.replace(/.*[\\/]/, ''));
|
|
137
|
+
writeFileSync(dest, buf);
|
|
138
|
+
try {
|
|
139
|
+
chmodSync(dest, 0o755);
|
|
140
|
+
}
|
|
141
|
+
catch (e) { }
|
|
142
|
+
return dest;
|
|
143
|
+
}
|
|
55
144
|
return new Promise((resolve, reject) => {
|
|
56
145
|
const args = ['encode', '--level', String(compressionLevel)];
|
|
146
|
+
let supportsName = false;
|
|
57
147
|
if (name) {
|
|
58
|
-
|
|
148
|
+
try {
|
|
149
|
+
const helpOut = execSync(`"${cliPath}" --help`, {
|
|
150
|
+
encoding: 'utf8',
|
|
151
|
+
timeout: 2000,
|
|
152
|
+
});
|
|
153
|
+
if (helpOut && helpOut.includes('--name'))
|
|
154
|
+
supportsName = true;
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
supportsName = false;
|
|
158
|
+
}
|
|
159
|
+
if (supportsName) {
|
|
160
|
+
args.push('--name', name);
|
|
161
|
+
}
|
|
59
162
|
}
|
|
60
163
|
if (passphrase) {
|
|
61
164
|
args.push('--passphrase', passphrase);
|
|
62
165
|
args.push('--encrypt', encryptType);
|
|
63
166
|
}
|
|
64
167
|
args.push(inputPath, outputPath);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
168
|
+
let triedExtract = false;
|
|
169
|
+
let tempExe;
|
|
170
|
+
const runSpawn = (exePath) => {
|
|
171
|
+
let proc;
|
|
172
|
+
try {
|
|
173
|
+
proc = spawn(exePath, args, { stdio: 'inherit' });
|
|
70
174
|
}
|
|
71
|
-
|
|
72
|
-
|
|
175
|
+
catch (err) {
|
|
176
|
+
if (!triedExtract) {
|
|
177
|
+
triedExtract = true;
|
|
178
|
+
try {
|
|
179
|
+
tempExe = extractToTemp(cliPath);
|
|
180
|
+
}
|
|
181
|
+
catch (ex) {
|
|
182
|
+
return reject(ex);
|
|
183
|
+
}
|
|
184
|
+
return runSpawn(tempExe);
|
|
185
|
+
}
|
|
186
|
+
return reject(err);
|
|
73
187
|
}
|
|
74
|
-
|
|
188
|
+
proc.on('error', (err) => {
|
|
189
|
+
if (!triedExtract) {
|
|
190
|
+
triedExtract = true;
|
|
191
|
+
try {
|
|
192
|
+
tempExe = extractToTemp(cliPath);
|
|
193
|
+
}
|
|
194
|
+
catch (ex) {
|
|
195
|
+
return reject(ex);
|
|
196
|
+
}
|
|
197
|
+
return runSpawn(tempExe);
|
|
198
|
+
}
|
|
199
|
+
reject(err);
|
|
200
|
+
});
|
|
201
|
+
proc.on('close', (code) => {
|
|
202
|
+
if (tempExe) {
|
|
203
|
+
try {
|
|
204
|
+
unlinkSync(tempExe);
|
|
205
|
+
}
|
|
206
|
+
catch (e) { }
|
|
207
|
+
}
|
|
208
|
+
if (code === 0) {
|
|
209
|
+
resolve();
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
reject(new Error(`Rust encoder exited with status ${code}`));
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
runSpawn(cliPath);
|
|
75
217
|
});
|
|
76
218
|
}
|
package/libroxify_native.node
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roxify",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Ultra-lightweight PNG steganography with native Rust acceleration. Encode binary data into PNG images with zstd compression.",
|
|
3
|
+
"version": "1.6.1",
|
|
5
4
|
"type": "module",
|
|
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",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"bin": {
|
|
@@ -11,33 +11,63 @@
|
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"dist",
|
|
14
|
+
"roxify_native.node",
|
|
14
15
|
"libroxify_native.node",
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"libroxify_native-x86_64-
|
|
19
|
-
"libroxify_native-x86_64-pc-windows-msvc.node",
|
|
16
|
+
"roxify_native-x86_64-pc-windows-gnu.node",
|
|
17
|
+
"roxify_native-x86_64-pc-windows-msvc.node",
|
|
18
|
+
"roxify_native-x86_64-unknown-linux-gnu.node",
|
|
19
|
+
"libroxify_native-x86_64-unknown-linux-gnu.node",
|
|
20
20
|
"README.md",
|
|
21
21
|
"LICENSE"
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "tsc",
|
|
25
|
+
"prebuild:pkg": "node scripts/copy-cli-binary.js",
|
|
25
26
|
"build:native": "cargo build --release --lib",
|
|
26
|
-
"build:native:linux": "cargo build --release --lib --target x86_64-unknown-linux-gnu
|
|
27
|
-
"build:native:macos-x64": "cargo build --release --lib --target x86_64-apple-darwin
|
|
28
|
-
"build:native:macos-arm": "cargo build --release --lib --target aarch64-apple-darwin
|
|
29
|
-
"build:native:windows": "cargo build --release --lib --target x86_64-pc-windows-msvc
|
|
27
|
+
"build:native:linux": "cargo build --release --lib --target x86_64-unknown-linux-gnu",
|
|
28
|
+
"build:native:macos-x64": "cargo build --release --lib --target x86_64-apple-darwin",
|
|
29
|
+
"build:native:macos-arm": "cargo build --release --lib --target aarch64-apple-darwin",
|
|
30
|
+
"build:native:windows": "cargo build --release --lib --target x86_64-pc-windows-msvc",
|
|
30
31
|
"build:cli": "cargo build --release --bin roxify_native && cp target/release/roxify_native dist/roxify-cli",
|
|
32
|
+
"build:cli:windows": "cargo build --release --bin roxify_native --target x86_64-pc-windows-gnu && node scripts/copy-cli-binary.js",
|
|
31
33
|
"build:all": "npm run build:native && npm run build && npm run build:cli",
|
|
32
34
|
"build:cross": "node scripts/build-all-platforms.js && npm run build && npm run build:cli",
|
|
35
|
+
"build:native:fast": "cargo build -p roxify_native --release --lib --no-default-features",
|
|
36
|
+
"build:native:quick-release": "FAST_RELEASE=1 cargo build --profile fastdev -p roxify_native --lib --no-default-features",
|
|
37
|
+
"build:native:super-fast": "USE_SYSTEM_ZSTD=1 FAST_RELEASE=1 cargo build --profile fastdev -p roxify_native --lib --no-default-features -j 1",
|
|
38
|
+
"build:native:low-cpu": "LOW_CPU=1 USE_SYSTEM_ZSTD=1 FAST_RELEASE=1 MAX_JOBS=1 node scripts/build-native-targets.cjs",
|
|
39
|
+
"build:native:targets": "node scripts/build-native-targets.cjs",
|
|
40
|
+
"build:native:targets:fast": "FAST_RELEASE=1 node scripts/build-native-targets.cjs",
|
|
41
|
+
"build:pkg": "npm run build && npx pkg . --target node18-win-x64 --output dist/rox.exe --compress Brotli --public",
|
|
33
42
|
"postbuild:native": "node scripts/copy-native.js",
|
|
34
|
-
"
|
|
35
|
-
"
|
|
43
|
+
"clean:targets": "node scripts/clean-artifacts.js",
|
|
44
|
+
"release:prepare": "npm run build && npm run build:native:targets && node scripts/prepare-release.cjs",
|
|
45
|
+
"release:github": "npm run release:prepare && node scripts/create-gh-release.cjs",
|
|
46
|
+
"package:prepare": "npm run build && npm run build:native:targets && node scripts/pack-npm.cjs",
|
|
47
|
+
"publish:npm": "npm run package:prepare && echo 'Run npm publish --access public'",
|
|
48
|
+
"release:flow": "node scripts/release-flow.cjs",
|
|
49
|
+
"release:flow:auto": "AUTO_PUBLISH=1 node scripts/release-flow.cjs",
|
|
50
|
+
"publish": "node scripts/publish.cjs",
|
|
51
|
+
"prepublishOnly": "npm run build && npm run build:native && npm run postbuild:native && npm run clean:targets",
|
|
52
|
+
"test": "npm run build && node ./test/run-all-tests.cjs",
|
|
53
|
+
"test:integration": "node scripts/run-integration-tests.cjs",
|
|
36
54
|
"cli": "node dist/cli.js"
|
|
37
55
|
},
|
|
56
|
+
"pkg": {
|
|
57
|
+
"targets": [
|
|
58
|
+
"node18-win-x64"
|
|
59
|
+
],
|
|
60
|
+
"outputPath": "dist",
|
|
61
|
+
"scripts": [
|
|
62
|
+
"dist/**/*.js"
|
|
63
|
+
],
|
|
64
|
+
"assets": [
|
|
65
|
+
"dist/roxify_native.exe"
|
|
66
|
+
]
|
|
67
|
+
},
|
|
38
68
|
"repository": {
|
|
39
69
|
"type": "git",
|
|
40
|
-
"url": "https://github.com/
|
|
70
|
+
"url": "https://github.com/RoxasYTB/roxify.git"
|
|
41
71
|
},
|
|
42
72
|
"keywords": [
|
|
43
73
|
"steganography",
|
|
@@ -53,9 +83,9 @@
|
|
|
53
83
|
],
|
|
54
84
|
"author": "",
|
|
55
85
|
"license": "MIT",
|
|
56
|
-
"dependencies": {},
|
|
57
86
|
"devDependencies": {
|
|
58
87
|
"@types/node": "^22.0.0",
|
|
88
|
+
"pkg": "^5.8.1",
|
|
59
89
|
"typescript": "^5.6.0"
|
|
60
90
|
},
|
|
61
91
|
"engines": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/roxify-cli
DELETED
|
Binary file
|