roxify 1.13.1 → 1.13.2

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/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "roxify_native"
3
- version = "1.13.1"
3
+ version = "1.13.2"
4
4
  edition = "2021"
5
5
  publish = false
6
6
 
@@ -52,25 +52,15 @@ rand = "0.8"
52
52
  sha2 = "0.10"
53
53
  mimalloc = "0.1"
54
54
 
55
- wgpu = { version = "0.19", optional = true }
56
55
  memmap2 = "0.9"
57
56
  bytemuck = { version = "1.14", features = ["derive"] }
58
- # tokio is optional now; enable via the 'async' feature
59
57
  tokio = { version = "1", features = ["sync", "rt"], optional = true }
60
58
  parking_lot = "0.12"
61
- pollster = { version = "0.3", optional = true }
62
59
  libsais = { version = "0.2.0", default-features = false }
63
60
 
64
61
  [features]
65
- # default is intentionally empty so the crate compiles fast for local checks.
66
- # Enable 'gpu' to pull in the WGPU and pollster dependencies (heavy).
67
- # Enable 'async' to include tokio runtime (optional).
68
- # Example: `cargo build -p roxify_native --features gpu`
69
62
  default = []
70
-
71
- gpu = ["wgpu", "pollster"]
72
63
  async = ["tokio"]
73
- full = ["gpu", "async"]
74
64
 
75
65
  [profile.release]
76
66
  opt-level = 3
package/README.md CHANGED
@@ -57,151 +57,27 @@ The core compression and image-processing logic is written in Rust and exposed t
57
57
 
58
58
  ## Benchmarks
59
59
 
60
- All measurements were taken on Linux x64 (Intel i7-6700K @ 4.0 GHz, 32 GB RAM) with Node.js v20. Every tool uses its **maximum compression** setting: zip -9, gzip -9, 7z LZMA2 -mx=9, and Roxify Zstd level 19. Roxify produces a valid PNG or WAV file rather than a raw archive.
61
-
62
- ### BWT-ANS Native Compression (Rust, via N-API)
63
-
64
- Direct API calls through the native module (BWT → MTF → RLE0 → rANS, 1 MB blocks):
65
-
66
- | Dataset | Original | Compressed | Ratio | Encode | Decode | Enc Throughput | Dec Throughput |
67
- | ------- | -------- | ---------- | ----- | ------ | ------ | -------------- | -------------- |
68
- | Repetitive text 45 KB | 45 KB | 207 B | 0.5% | < 1 ms | < 1 ms | — | — |
69
- | Rust source 6 KB | 6 KB | 2.0 KB | 33.7% | < 1 ms | < 1 ms | — | — |
70
- | node_modules tar 175 MB | 175.5 MB | 38.4 MB | 21.9% | 9.68 s | 5.62 s | 18.1 MB/s | 31.2 MB/s |
71
- | Random 100 KB | 100 KB | 101 KB | 101.5% | < 1 ms | < 1 ms | — | — |
72
- | Zeros 100 KB | 100 KB | 51 B | 0.1% | < 1 ms | < 1 ms | — | — |
73
-
74
- > BWT-ANS achieves **21.9% on real-world node_modules** (175 MB), with **18 MB/s encode** and **31 MB/s decode** throughput. 100% lossless roundtrip verified on all datasets.
75
-
76
- ### Compression Ratio (Maximum Compression for All Tools)
77
-
78
- | Dataset | Original | zip -9 | gzip -9 | 7z LZMA2 -9 | Roxify PNG | Roxify WAV |
79
- | ----------- | -------- | --------------- | --------------- | --------------- | ------------------- | ------------------- |
80
- | Text 1 MB | 1.00 MB | 219 KB (21.4%) | 219 KB (21.4%) | 187 KB (18.3%) | **188 KB (18.3%)** | **187 KB (18.3%)** |
81
- | JSON 1 MB | 1.00 MB | 263 KB (25.7%) | 263 KB (25.7%) | 225 KB (22.0%) | **220 KB (21.5%)** | **219 KB (21.4%)** |
82
- | Binary 1 MB | 1.00 MB | 1.00 MB (100%) | 1.00 MB (100%) | 1.00 MB (100%) | 1.00 MB (100%) | 1.00 MB (100%) |
83
- | Mixed 5 MB | 5.00 MB | 2.45 MB (49.0%) | 2.45 MB (49.1%) | 2.33 MB (46.6%) | 2.38 MB (47.6%) | 2.38 MB (47.6%) |
84
- | Text 10 MB | 10.00 MB | 2.13 MB (21.3%) | 2.13 MB (21.3%) | 1.71 MB (17.1%) | **1.71 MB (17.1%)** | **1.70 MB (17.0%)** |
85
- | Mixed 10 MB | 10.00 MB | 4.90 MB (49.0%) | 4.90 MB (49.0%) | 4.65 MB (46.5%) | 4.73 MB (47.3%) | 4.73 MB (47.3%) |
86
-
87
- > **Roxify matches 7z LZMA2 ultra-compression on text** (18.3% for both at 1 MB) and **beats LZMA2 on JSON** (21.4% vs 22.0%). On mixed data, Roxify is within 1 percentage point of LZMA2 while producing a shareable PNG/WAV instead of an archive.
88
-
89
- ### Encode and Decode Speed (CLI)
90
-
91
- | Dataset | Tool | Encode | Decode | Enc Throughput | Dec Throughput |
92
- | ---------- | -------------- | ---------- | ---------- | -------------- | -------------- |
93
- | Text 1 MB | zip -9 | 112 ms | 36 ms | 8.9 MB/s | 27.6 MB/s |
94
- | | gzip -9 | 146 ms | 38 ms | 6.9 MB/s | 26.0 MB/s |
95
- | | 7z LZMA -9 | 303 ms | 21 ms | 3.3 MB/s | 46.6 MB/s |
96
- | | **Roxify PNG** | **859 ms** | **577 ms** | **1.2 MB/s** | **1.7 MB/s** |
97
- | | **Roxify WAV** | **794 ms** | **480 ms** | **1.3 MB/s** | **2.1 MB/s** |
98
- | JSON 1 MB | zip -9 | 79 ms | 20 ms | 12.7 MB/s | 50.5 MB/s |
99
- | | 7z LZMA -9 | 197 ms | 26 ms | 5.1 MB/s | 37.9 MB/s |
100
- | | **Roxify PNG** | **1.14 s** | **755 ms** | **0.9 MB/s** | **1.3 MB/s** |
101
- | | **Roxify WAV** | **1.49 s** | **518 ms** | **0.7 MB/s** | **1.9 MB/s** |
102
- | Text 10 MB | zip -9 | 1.21 s | 70 ms | 8.2 MB/s | 143.8 MB/s |
103
- | | 7z LZMA -9 | 5.05 s | 99 ms | 2.0 MB/s | 100.8 MB/s |
104
- | | **Roxify PNG** | **9.05 s** | **4.53 s** | **1.1 MB/s** | **2.2 MB/s** |
105
- | | **Roxify WAV** | **9.22 s** | **2.59 s** | **1.1 MB/s** | **3.9 MB/s** |
106
-
107
- > Roxify CLI includes Node.js startup overhead (~400 ms). In the JS API (below), the same operations are significantly faster. WAV decode is consistently faster than PNG decode due to simpler container parsing.
108
-
109
- ### JavaScript API Throughput
110
-
111
- Direct API calls (no CLI startup overhead):
112
-
113
- | Size | Container | Encode | Decode | Enc Throughput | Dec Throughput | Output | Ratio | Integrity |
114
- | ------ | --------- | ------ | ------- | -------------- | -------------- | --------- | ------ | --------- |
115
- | 1 KB | PNG | 9 ms | 12 ms | 0.1 MB/s | 0.1 MB/s | 1.14 KB | 114.3% | ✓ |
116
- | 10 KB | PNG | 18 ms | 34 ms | 0.5 MB/s | 0.3 MB/s | 10.32 KB | 103.2% | ✓ |
117
- | 100 KB | PNG | 52 ms | 109 ms | 1.9 MB/s | 0.9 MB/s | 100.52 KB | 100.5% | ✓ |
118
- | 500 KB | PNG | 339 ms | 541 ms | 1.4 MB/s | 0.9 MB/s | 502.64 KB | 100.5% | ✓ |
119
- | 1 MB | PNG | 875 ms | 1.24 s | 1.1 MB/s | 0.8 MB/s | 1.00 MB | 100.3% | ✓ |
120
- | 5 MB | PNG | 3.39 s | 4.12 s | 1.5 MB/s | 1.2 MB/s | 5.01 MB | 100.2% | ✓ |
121
- | 10 MB | PNG | 6.84 s | 12.28 s | 1.5 MB/s | 0.8 MB/s | 10.01 MB | 100.1% | ✓ |
122
- | 1 KB | WAV | 2 ms | 2 ms | 0.6 MB/s | 0.6 MB/s | 1.08 KB | 107.5% | ✓ |
123
- | 10 KB | WAV | 4 ms | 5 ms | 2.3 MB/s | 1.8 MB/s | 10.08 KB | 100.8% | ✓ |
124
- | 100 KB | WAV | 39 ms | 28 ms | 2.5 MB/s | 3.5 MB/s | 100.08 KB | 100.1% | ✓ |
125
- | 500 KB | WAV | 172 ms | 190 ms | 2.8 MB/s | 2.6 MB/s | 500.09 KB | 100.0% | ✓ |
126
- | 1 MB | WAV | 452 ms | 276 ms | 2.2 MB/s | 3.6 MB/s | 1.00 MB | 100.0% | ✓ |
127
- | 5 MB | WAV | 2.70 s | 1.65 s | 1.8 MB/s | 3.0 MB/s | 5.00 MB | 100.0% | ✓ |
128
- | 10 MB | WAV | 4.81 s | 2.56 s | 2.1 MB/s | 3.9 MB/s | 10.00 MB | 100.0% | ✓ |
129
-
130
- > WAV container is **2–4× faster** than PNG for decoding at large sizes, and produces slightly smaller output thanks to simpler framing.
131
-
132
- ### Reed-Solomon ECC Throughput
133
-
134
- | Size | Encode | Decode | Enc Throughput | Dec Throughput | Overhead |
135
- | ------ | ------ | ------ | -------------- | -------------- | -------- |
136
- | 1 KB | 6 ms | 4 ms | 0.2 MB/s | 0.2 MB/s | 125.7% |
137
- | 10 KB | 7 ms | 6 ms | 1.3 MB/s | 1.5 MB/s | 119.6% |
138
- | 100 KB | 49 ms | 45 ms | 2.0 MB/s | 2.1 MB/s | 118.8% |
139
- | 1 MB | 483 ms | 377 ms | 2.1 MB/s | 2.7 MB/s | 118.6% |
140
-
141
- ### Lossy-Resilient Encoding
142
-
143
- #### Robust Image (QR-code-style, block size 4×4)
144
-
145
- | Data Size | Encode Time | Output (PNG) |
146
- | --------- | ----------- | ------------ |
147
- | 32 B | 32 ms | 122 KB |
148
- | 128 B | 39 ms | 122 KB |
149
- | 512 B | 76 ms | 316 KB |
150
- | 1 KB | 139 ms | 508 KB |
151
- | 2 KB | 251 ms | 986 KB |
152
-
153
- #### Robust Audio (MFSK 8-channel, medium ECC)
154
-
155
- | Data Size | Encode | Decode | Output (WAV) | Integrity |
156
- | --------- | ------ | ------ | ------------ | --------- |
157
- | 10 B | 33 ms | 44 ms | 1.35 MB | ✓ |
158
- | 32 B | 19 ms | 31 ms | 1.35 MB | ✓ |
159
- | 64 B | 22 ms | 24 ms | 1.35 MB | ✓ |
160
- | 128 B | 21 ms | 28 ms | 1.35 MB | ✓ |
161
- | 256 B | 40 ms | 45 ms | 2.59 MB | ✓ |
162
-
163
- ### Data Integrity Verification
164
-
165
- All encode/decode roundtrips produce bit-exact output, verified by SHA-256:
166
-
167
- | Test Case | PNG | WAV |
168
- | ----------------------- | --- | --- |
169
- | Empty buffer (0 B) | ✓ | ✓ |
170
- | Single byte (1 B) | ✓ | ✓ |
171
- | All byte values (256 B) | ✓ | ✓ |
172
- | 1 KB text | ✓ | ✓ |
173
- | 100 KB random | ✓ | ✓ |
174
- | 1 MB random | ✓ | ✓ |
175
- | 5 MB random | ✓ | ✓ |
176
-
177
- **14 / 14 integrity tests passed** across both containers.
178
-
179
- ### Key Observations
180
-
181
- - **Roxify matches LZMA2 ultra-compression** on text data (18.3%) and **outperforms it on JSON** (21.4% vs 22.0%), while producing a standard PNG or WAV file instead of an archive.
182
- - **WAV container decode is 2–4× faster** than PNG decode at large sizes (3.9 MB/s vs 0.8 MB/s for 10 MB).
183
- - **WAV encode for 1 KB data completes in 2 ms** — well under the sub-second target.
184
- - **Lossy-resilient audio** encode/decode completes in under 50 ms for data up to 256 bytes, with full integrity.
185
- - **100% data integrity** across all sizes and containers — every byte is recovered exactly.
186
- - The CLI overhead (~400 ms Node.js startup) is amortized on larger inputs. For programmatic use, the JS API eliminates this entirely.
187
- - On incompressible (random) data, all tools converge to ~100% as expected. No compression algorithm can shrink truly random data.
188
-
189
- ### Methodology
190
-
191
- Benchmarks were generated using `test/benchmark-detailed.cjs`. Datasets consist of procedurally generated text, JSON, and random binary data. Each tool was invoked with its maximum compression setting:
192
-
193
- | Tool | Command / Setting |
194
- | -------- | --------------------------- |
195
- | zip | `zip -r -q -9` |
196
- | tar/gzip | `tar -cf - \| gzip -9` |
197
- | 7z | `7z a -mx=9` (LZMA2 ultra) |
198
- | Roxify | Zstd level 19, compact mode |
199
-
200
- To reproduce:
60
+ All measurements taken on Linux x64 (Intel i7-6700K @ 4.0 GHz, 32 GB RAM). Roxify uses its native Rust CLI (`roxify_native`) with streaming Zstd L3 + multi-threaded + LDM + window_log(30). ZIP uses `zip -r -q -9` (maximum compression).
201
61
 
202
- ```bash
203
- node test/benchmark-detailed.cjs
204
- ```
62
+ ### Real-world directory encoding: Roxify vs ZIP
63
+
64
+ | Dataset | Original | ZIP -9 | Roxify PNG | ZIP time | Roxify time | Speedup |
65
+ | --- | --- | --- | --- | --- | --- | --- |
66
+ | Test A (19 638 files, 177 MB) | 177 MB | 87.7 MB (49.6%) | 54.9 MB (31.0%) | 17.6 s | 1.2 s | 14.7x |
67
+ | Test B (3 936 files, 1.4 GB) | 1.4 GB | 513 MB (36.7%) | 409 MB (29.2%) | 1 min 46 s | 6.7 s | 15.9x |
68
+
69
+ ### Decompression
70
+
71
+ | Dataset | unzip | Roxify decode | Speedup |
72
+ | --- | --- | --- | --- |
73
+ | Test A (177 MB) | 2.4 s | 1.8 s | 1.3x |
74
+ | Test B (1.4 GB) | 8.4 s | 2.2 s | 3.8x |
75
+
76
+ Roxify produces a valid PNG image instead of a ZIP archive. On these real-world datasets it compresses 20-37% smaller than ZIP -9 while encoding 15x faster, thanks to multi-threaded Zstd with long-distance matching.
77
+
78
+ ### Data integrity
79
+
80
+ 100% lossless roundtrip verified by byte-exact diff on all datasets. Start and end markers verified in every output PNG.
205
81
 
206
82
  ---
207
83
 
package/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ async function loadJsEngine() {
20
20
  VFSIndexEntry: undefined,
21
21
  };
22
22
  }
23
- const VERSION = '1.13.1';
23
+ const VERSION = '1.13.2';
24
24
  function getDirectorySize(dirPath) {
25
25
  let totalSize = 0;
26
26
  try {
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export * from './utils/optimization.js';
12
12
  export * from './utils/reconstitution.js';
13
13
  export * from './utils/robust-audio.js';
14
14
  export * from './utils/robust-image.js';
15
- export { decodeWithRustCLI, encodeWithRustCLI, havepassphraseWithRustCLI, isRustBinaryAvailable, listWithRustCLI, } from './utils/rust-cli-wrapper.js';
15
+ export { decodeWithRustCLI, encodeWithRustCLI, havepassphraseWithRustCLI, isRustBinaryAvailable, listWithRustCLI } from './utils/rust-cli-wrapper.js';
16
16
  export * from './utils/types.js';
17
17
  export * from './utils/zstd.js';
18
18
  export { packPaths, packPathsToParts, unpackBuffer } from './pack.js';
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ export * from './utils/optimization.js';
12
12
  export * from './utils/reconstitution.js';
13
13
  export * from './utils/robust-audio.js';
14
14
  export * from './utils/robust-image.js';
15
- export { decodeWithRustCLI, encodeWithRustCLI, havepassphraseWithRustCLI, isRustBinaryAvailable, listWithRustCLI, } from './utils/rust-cli-wrapper.js';
15
+ export { decodeWithRustCLI, encodeWithRustCLI, havepassphraseWithRustCLI, isRustBinaryAvailable, listWithRustCLI } from './utils/rust-cli-wrapper.js';
16
16
  export * from './utils/types.js';
17
17
  export * from './utils/zstd.js';
18
18
  export { packPaths, packPathsToParts, unpackBuffer } from './pack.js';
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -8,7 +8,7 @@ mod pool;
8
8
  mod hybrid;
9
9
 
10
10
  fn bench_roundtrip(name: &str, data: &[u8]) {
11
- let compressor = hybrid::HybridCompressor::new(false, 4);
11
+ let compressor = hybrid::HybridCompressor::new();
12
12
 
13
13
  let start = Instant::now();
14
14
  let (compressed, stats) = compressor.compress(data).unwrap();
package/native/hybrid.rs CHANGED
@@ -28,7 +28,7 @@ pub struct HybridCompressor {
28
28
  }
29
29
 
30
30
  impl HybridCompressor {
31
- pub fn new(_enable_gpu: bool, _pool_size: usize) -> Self {
31
+ pub fn new() -> Self {
32
32
  HybridCompressor {
33
33
  block_size: BLOCK_SIZE,
34
34
  }
@@ -36,7 +36,6 @@ impl HybridCompressor {
36
36
 
37
37
  pub fn compress(&self, data: &[u8]) -> Result<(Vec<u8>, CompressionStats)> {
38
38
  let original_size = data.len() as u64;
39
-
40
39
  let blocks: Vec<&[u8]> = data.chunks(self.block_size).collect();
41
40
  let blocks_count = blocks.len();
42
41
 
@@ -136,13 +135,11 @@ impl HybridCompressor {
136
135
  }
137
136
  }
138
137
 
139
- fn compress_block(block: &[u8]) -> Result<Vec<u8>> {
138
+ fn compress_block_with_entropy(block: &[u8], entropy: f32) -> Result<Vec<u8>> {
140
139
  if block.is_empty() {
141
140
  return Ok(vec![BLOCK_FLAG_STORE]);
142
141
  }
143
142
 
144
- let entropy = analyze_entropy(block);
145
-
146
143
  if entropy >= ENTROPY_THRESHOLD_STORE {
147
144
  let mut result = Vec::with_capacity(1 + block.len());
148
145
  result.push(BLOCK_FLAG_STORE);
@@ -165,6 +162,19 @@ fn compress_block(block: &[u8]) -> Result<Vec<u8>> {
165
162
  return Ok(result);
166
163
  }
167
164
 
165
+ try_bwt_or_zstd(block)
166
+ }
167
+
168
+ fn compress_block(block: &[u8]) -> Result<Vec<u8>> {
169
+ if block.is_empty() {
170
+ return Ok(vec![BLOCK_FLAG_STORE]);
171
+ }
172
+
173
+ let entropy = analyze_entropy(block);
174
+ compress_block_with_entropy(block, entropy)
175
+ }
176
+
177
+ fn try_bwt_or_zstd(block: &[u8]) -> Result<Vec<u8>> {
168
178
  let bwt = bwt_encode(block)?;
169
179
  let mtf_data = mtf_encode(&bwt.transformed);
170
180
  let rle_data = rle0_encode(&mtf_data);
@@ -277,11 +287,11 @@ fn decompress_block_v1(block: &[u8]) -> Result<Vec<u8>> {
277
287
  }
278
288
 
279
289
  pub fn compress_high_performance(data: &[u8]) -> Result<(Vec<u8>, CompressionStats)> {
280
- let compressor = HybridCompressor::new(false, 0);
290
+ let compressor = HybridCompressor::new();
281
291
  compressor.compress(data)
282
292
  }
283
293
 
284
294
  pub fn decompress_high_performance(data: &[u8]) -> Result<Vec<u8>> {
285
- let compressor = HybridCompressor::new(false, 0);
295
+ let compressor = HybridCompressor::new();
286
296
  compressor.decompress(data)
287
297
  }
package/native/lib.rs CHANGED
@@ -6,8 +6,6 @@ use napi_derive::napi;
6
6
  static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
7
7
 
8
8
  mod core;
9
- #[cfg(feature = "gpu")]
10
- mod gpu;
11
9
  mod rans;
12
10
  mod rans_byte;
13
11
  mod bwt;
@@ -27,15 +25,6 @@ mod archive;
27
25
  mod streaming;
28
26
 
29
27
  pub use core::*;
30
- #[cfg(feature = "gpu")]
31
- pub use gpu::*;
32
- #[cfg(not(feature = "gpu"))]
33
- mod gpu {
34
- pub fn gpu_available() -> bool {
35
- false
36
- }
37
- }
38
-
39
28
  pub use rans::*;
40
29
  pub use bwt::*;
41
30
  pub use context_mixing::*;
@@ -57,12 +46,6 @@ pub struct CompressionReport {
57
46
  pub blocks_count: u32,
58
47
  }
59
48
 
60
- #[napi(object)]
61
- pub struct GpuStatus {
62
- pub available: bool,
63
- pub adapter_info: Option<String>,
64
- }
65
-
66
49
  #[cfg(not(test))]
67
50
  #[napi]
68
51
  pub fn scan_pixels(buffer: Buffer, channels: u32, marker_bytes: Option<Buffer>) -> Result<ScanResult> {
@@ -122,15 +105,6 @@ pub fn native_zstd_decompress_with_dict(buffer: Buffer, dict: Buffer) -> Result<
122
105
  core::zstd_decompress_bytes(&buffer, Some(dict_slice)).map_err(|e| Error::from_reason(e))
123
106
  }
124
107
 
125
- #[cfg(not(test))]
126
- #[napi]
127
- pub fn check_gpu_status() -> GpuStatus {
128
- GpuStatus {
129
- available: gpu::gpu_available(),
130
- adapter_info: None,
131
- }
132
- }
133
-
134
108
  #[cfg(not(test))]
135
109
  #[napi]
136
110
  pub fn bwt_transform(buffer: Buffer) -> Result<Vec<u8>> {
package/native/main.rs CHANGED
@@ -17,6 +17,7 @@ mod archive;
17
17
  mod streaming;
18
18
  mod streaming_decode;
19
19
  mod streaming_encode;
20
+ mod progress;
20
21
 
21
22
  use crate::encoder::ImageFormat;
22
23
  use std::path::PathBuf;
@@ -157,13 +158,16 @@ fn main() -> anyhow::Result<()> {
157
158
  .or_else(|| input.file_name().and_then(|n| n.to_str()));
158
159
 
159
160
  if is_dir && dict.is_none() {
160
- streaming_encode::encode_dir_to_png_encrypted(
161
+ streaming_encode::encode_dir_to_png_encrypted_with_progress(
161
162
  &input,
162
163
  &output,
163
164
  level,
164
165
  file_name,
165
166
  passphrase.as_deref(),
166
167
  Some(&encrypt),
168
+ Some(Box::new(|current, total, step| {
169
+ eprintln!("PROGRESS:{}:{}:{}", current, total, step);
170
+ })),
167
171
  )?;
168
172
  println!("(TAR archive, rXFL chunk embedded)");
169
173
  return Ok(());
@@ -187,6 +191,7 @@ fn main() -> anyhow::Result<()> {
187
191
  };
188
192
 
189
193
  let use_streaming = payload.len() > 64 * 1024 * 1024;
194
+ eprintln!("PROGRESS:50:100:encoding");
190
195
 
191
196
  if use_streaming {
192
197
  streaming::encode_to_png_file(
@@ -227,6 +232,7 @@ fn main() -> anyhow::Result<()> {
227
232
  }
228
233
 
229
234
  if file_list_json.is_some() {
235
+ eprintln!("PROGRESS:100:100:done");
230
236
  if is_dir {
231
237
  println!("(TAR archive, rXFL chunk embedded)");
232
238
  } else {
@@ -339,8 +345,10 @@ fn main() -> anyhow::Result<()> {
339
345
 
340
346
  if is_png_file && files.is_none() && dict.is_none() && file_size > 100_000_000 {
341
347
  let out_dir = output.clone().unwrap_or_else(|| PathBuf::from("out.raw"));
348
+ eprintln!("PROGRESS:5:100:decoding");
342
349
  match streaming_decode::streaming_decode_to_dir_encrypted(&input, &out_dir, passphrase.as_deref()) {
343
350
  Ok(written) => {
351
+ eprintln!("PROGRESS:100:100:done");
344
352
  println!("Unpacked {} files (TAR)", written.len());
345
353
  return Ok(());
346
354
  }
@@ -351,6 +359,7 @@ fn main() -> anyhow::Result<()> {
351
359
  }
352
360
 
353
361
  let buf = read_all(&input)?;
362
+ eprintln!("PROGRESS:20:100:decompressing");
354
363
  let dict_bytes: Option<Vec<u8>> = match dict {
355
364
  Some(path) => Some(read_all(&path)?),
356
365
  None => None,
@@ -413,6 +422,7 @@ fn main() -> anyhow::Result<()> {
413
422
  let files_slice = file_list.as_ref().map(|v| v.as_slice());
414
423
 
415
424
  let written = packer::unpack_stream_to_dir(&mut reader, &out_dir, files_slice).map_err(|e| anyhow::anyhow!(e))?;
425
+ eprintln!("PROGRESS:100:100:done");
416
426
  println!("Unpacked {} files", written.len());
417
427
  } else {
418
428
  let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
@@ -511,6 +521,7 @@ fn main() -> anyhow::Result<()> {
511
521
  } else {
512
522
  write_all(&dest, &out_bytes)?;
513
523
  }
524
+ eprintln!("PROGRESS:100:100:done");
514
525
  }
515
526
  }
516
527
  Commands::Crc32 { input } => {
@@ -1,43 +1,142 @@
1
1
  use parking_lot::Mutex;
2
2
  use std::sync::Arc;
3
+ use std::time::Instant;
3
4
 
4
- pub struct ProgressBar {
5
+ #[derive(Clone, Debug)]
6
+ pub struct ProgressSnapshot {
7
+ pub current: u64,
8
+ pub total: u64,
9
+ pub percentage: f64,
10
+ pub elapsed_ms: u64,
11
+ pub eta_ms: Option<u64>,
12
+ pub speed_bytes_per_sec: f64,
13
+ pub step: String,
14
+ }
15
+
16
+ struct ProgressInner {
5
17
  total: u64,
6
- current: Arc<Mutex<u64>>,
7
- message: Arc<Mutex<String>>,
18
+ current: u64,
19
+ step: String,
20
+ start: Instant,
21
+ }
22
+
23
+ pub struct ProgressBar {
24
+ inner: Arc<Mutex<ProgressInner>>,
25
+ callback: Arc<Mutex<Option<Box<dyn Fn(ProgressSnapshot) + Send>>>>,
8
26
  }
9
27
 
10
28
  impl ProgressBar {
11
29
  pub fn new(total: u64) -> Self {
12
30
  Self {
13
- total,
14
- current: Arc::new(Mutex::new(0)),
15
- message: Arc::new(Mutex::new(String::new())),
31
+ inner: Arc::new(Mutex::new(ProgressInner {
32
+ total,
33
+ current: 0,
34
+ step: String::new(),
35
+ start: Instant::now(),
36
+ })),
37
+ callback: Arc::new(Mutex::new(None)),
38
+ }
39
+ }
40
+
41
+ pub fn with_callback<F: Fn(ProgressSnapshot) + Send + 'static>(total: u64, cb: F) -> Self {
42
+ Self {
43
+ inner: Arc::new(Mutex::new(ProgressInner {
44
+ total,
45
+ current: 0,
46
+ step: String::new(),
47
+ start: Instant::now(),
48
+ })),
49
+ callback: Arc::new(Mutex::new(Some(Box::new(cb)))),
16
50
  }
17
51
  }
18
52
 
19
53
  pub fn inc(&self, delta: u64) {
20
- let mut current = self.current.lock();
21
- *current += delta;
54
+ let snapshot = {
55
+ let mut inner = self.inner.lock();
56
+ inner.current += delta;
57
+ self.snapshot_inner(&inner)
58
+ };
59
+ self.emit(snapshot);
22
60
  }
23
61
 
24
62
  pub fn set(&self, value: u64) {
25
- let mut current = self.current.lock();
26
- *current = value;
63
+ let snapshot = {
64
+ let mut inner = self.inner.lock();
65
+ inner.current = value;
66
+ self.snapshot_inner(&inner)
67
+ };
68
+ self.emit(snapshot);
27
69
  }
28
70
 
29
- pub fn set_message(&self, msg: String) {
30
- let mut message = self.message.lock();
31
- *message = msg;
71
+ pub fn set_step(&self, step: &str) {
72
+ let snapshot = {
73
+ let mut inner = self.inner.lock();
74
+ inner.step = step.to_string();
75
+ self.snapshot_inner(&inner)
76
+ };
77
+ self.emit(snapshot);
32
78
  }
33
79
 
34
- pub fn get_progress(&self) -> (u64, u64) {
35
- let current = *self.current.lock();
36
- (current, self.total)
80
+ pub fn snapshot(&self) -> ProgressSnapshot {
81
+ let inner = self.inner.lock();
82
+ self.snapshot_inner(&inner)
83
+ }
84
+
85
+ fn snapshot_inner(&self, inner: &ProgressInner) -> ProgressSnapshot {
86
+ let elapsed = inner.start.elapsed();
87
+ let elapsed_ms = elapsed.as_millis() as u64;
88
+ let percentage = if inner.total > 0 {
89
+ (inner.current as f64 / inner.total as f64) * 100.0
90
+ } else {
91
+ 0.0
92
+ };
93
+
94
+ let elapsed_secs = elapsed.as_secs_f64();
95
+ let speed = if elapsed_secs > 0.01 {
96
+ inner.current as f64 / elapsed_secs
97
+ } else {
98
+ 0.0
99
+ };
100
+
101
+ let eta_ms = if speed > 0.0 && inner.current > 0 && inner.current < inner.total {
102
+ let remaining = inner.total - inner.current;
103
+ Some((remaining as f64 / speed * 1000.0) as u64)
104
+ } else {
105
+ None
106
+ };
107
+
108
+ ProgressSnapshot {
109
+ current: inner.current,
110
+ total: inner.total,
111
+ percentage,
112
+ elapsed_ms,
113
+ eta_ms,
114
+ speed_bytes_per_sec: speed,
115
+ step: inner.step.clone(),
116
+ }
117
+ }
118
+
119
+ fn emit(&self, snapshot: ProgressSnapshot) {
120
+ if let Some(ref cb) = *self.callback.lock() {
121
+ cb(snapshot);
122
+ }
37
123
  }
38
124
 
39
125
  pub fn finish(&self) {
40
- let mut current = self.current.lock();
41
- *current = self.total;
126
+ let snapshot = {
127
+ let mut inner = self.inner.lock();
128
+ inner.current = inner.total;
129
+ self.snapshot_inner(&inner)
130
+ };
131
+ self.emit(snapshot);
132
+ }
133
+
134
+ pub fn get_progress(&self) -> (u64, u64) {
135
+ let inner = self.inner.lock();
136
+ (inner.current, inner.total)
137
+ }
138
+
139
+ pub fn set_message(&self, msg: String) {
140
+ self.set_step(&msg);
42
141
  }
43
142
  }
@@ -11,6 +11,8 @@ const MARKER_END: [(u8, u8, u8); 3] = [(0, 0, 255), (0, 255, 0), (255, 0, 0)];
11
11
  const MARKER_ZSTD: (u8, u8, u8) = (0, 255, 0);
12
12
  const MAGIC: &[u8] = b"ROX1";
13
13
 
14
+ pub type ProgressCallback = Box<dyn Fn(u64, u64, &str) + Send>;
15
+
14
16
  pub fn encode_dir_to_png(
15
17
  dir_path: &Path,
16
18
  output_path: &Path,
@@ -27,10 +29,27 @@ pub fn encode_dir_to_png_encrypted(
27
29
  name: Option<&str>,
28
30
  passphrase: Option<&str>,
29
31
  encrypt_type: Option<&str>,
32
+ ) -> anyhow::Result<()> {
33
+ encode_dir_to_png_encrypted_with_progress(dir_path, output_path, compression_level, name, passphrase, encrypt_type, None)
34
+ }
35
+
36
+ pub fn encode_dir_to_png_encrypted_with_progress(
37
+ dir_path: &Path,
38
+ output_path: &Path,
39
+ compression_level: i32,
40
+ name: Option<&str>,
41
+ passphrase: Option<&str>,
42
+ encrypt_type: Option<&str>,
43
+ progress: Option<ProgressCallback>,
30
44
  ) -> anyhow::Result<()> {
31
45
  let tmp_zst = output_path.with_extension("tmp.zst");
32
46
 
33
- let file_list = compress_dir_to_zst(dir_path, &tmp_zst, compression_level)?;
47
+ let file_list = compress_dir_to_zst(dir_path, &tmp_zst, compression_level, &progress)?;
48
+
49
+ if let Some(ref cb) = progress {
50
+ cb(90, 100, "writing_png");
51
+ }
52
+
34
53
  let file_list_json = serde_json::to_string(&file_list)?;
35
54
 
36
55
  let result = write_png_from_zst(
@@ -38,6 +57,11 @@ pub fn encode_dir_to_png_encrypted(
38
57
  passphrase, encrypt_type,
39
58
  );
40
59
  let _ = std::fs::remove_file(&tmp_zst);
60
+
61
+ if let Some(ref cb) = progress {
62
+ cb(100, 100, "done");
63
+ }
64
+
41
65
  result
42
66
  }
43
67
 
@@ -45,6 +69,7 @@ fn compress_dir_to_zst(
45
69
  dir_path: &Path,
46
70
  zst_path: &Path,
47
71
  compression_level: i32,
72
+ progress: &Option<ProgressCallback>,
48
73
  ) -> anyhow::Result<Vec<serde_json::Value>> {
49
74
  let base = dir_path;
50
75
 
@@ -55,6 +80,8 @@ fn compress_dir_to_zst(
55
80
  .filter(|e| e.file_type().is_file())
56
81
  .collect();
57
82
 
83
+ let total_files = entries.len() as u64;
84
+
58
85
  let zst_file = File::create(zst_path)?;
59
86
  let buf_writer = BufWriter::with_capacity(16 * 1024 * 1024, zst_file);
60
87
 
@@ -74,7 +101,7 @@ fn compress_dir_to_zst(
74
101
  let mut file_list = Vec::new();
75
102
  {
76
103
  let mut tar_builder = Builder::new(&mut encoder);
77
- for entry in &entries {
104
+ for (idx, entry) in entries.iter().enumerate() {
78
105
  let full = entry.path();
79
106
  let rel = full.strip_prefix(base).unwrap_or(full);
80
107
  let rel_str = rel.to_string_lossy().replace('\\', "/");
@@ -103,6 +130,11 @@ fn compress_dir_to_zst(
103
130
  .map_err(|e| anyhow::anyhow!("tar append {}: {}", rel_str, e))?;
104
131
 
105
132
  file_list.push(serde_json::json!({"name": rel_str, "size": size}));
133
+
134
+ if let Some(ref cb) = progress {
135
+ let pct = ((idx as u64 + 1) * 85 / total_files.max(1)).min(85);
136
+ cb(pct, 100, "compressing");
137
+ }
106
138
  }
107
139
  tar_builder.finish().map_err(|e| anyhow::anyhow!("tar finish: {}", e))?;
108
140
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roxify",
3
- "version": "1.13.1",
3
+ "version": "1.13.2",
4
4
  "type": "module",
5
5
  "description": "Ultra-lightweight PNG steganography with native Rust acceleration. Encode binary data into PNG images with zstd compression.",
6
6
  "main": "dist/index.js",
package/dist/roxify-cli DELETED
Binary file
package/native/gpu.rs DELETED
@@ -1,116 +0,0 @@
1
- use anyhow::{anyhow, Result};
2
- use parking_lot::RwLock;
3
- use std::sync::Arc;
4
- use wgpu::*;
5
- use wgpu::util::DeviceExt;
6
-
7
- pub struct GpuDevice {
8
- device: Device,
9
- queue: Queue,
10
- supported: bool,
11
- adapter_info: String,
12
- }
13
-
14
- pub struct GpuContext {
15
- inner: Arc<RwLock<Option<GpuDevice>>>,
16
- }
17
-
18
- impl GpuContext {
19
- pub async fn new() -> Self {
20
- let instance = Instance::new(InstanceDescriptor {
21
- backends: Backends::all(),
22
- ..Default::default()
23
- });
24
-
25
- let adapter = instance.request_adapter(&RequestAdapterOptions {
26
- power_preference: PowerPreference::HighPerformance,
27
- compatible_surface: None,
28
- force_fallback_adapter: false,
29
- }).await;
30
-
31
- let device_info = if let Some(adapter) = adapter {
32
- match adapter.request_device(&DeviceDescriptor {
33
- label: Some("roxify-compute"),
34
- required_features: Features::empty(),
35
- required_limits: Limits::default(),
36
- }, None).await {
37
- Ok((device, queue)) => {
38
- let info = adapter.get_info();
39
- Some(GpuDevice {
40
- device,
41
- queue,
42
- supported: true,
43
- adapter_info: format!("{:?}", info.driver),
44
- })
45
- }
46
- Err(_) => None,
47
- }
48
- } else {
49
- None
50
- };
51
-
52
- GpuContext {
53
- inner: Arc::new(RwLock::new(device_info)),
54
- }
55
- }
56
-
57
- pub fn is_available(&self) -> bool {
58
- self.inner.read().is_some()
59
- }
60
-
61
- pub fn get_adapter_info(&self) -> Option<String> {
62
- self.inner.read().as_ref().map(|d| d.adapter_info.clone())
63
- }
64
-
65
- pub async fn create_compute_pipeline(
66
- &self,
67
- shader_src: &str,
68
- entry_point: &str,
69
- ) -> Result<ComputePipeline> {
70
- let gpu = self.inner.read();
71
- let gpu = gpu.as_ref().ok_or_else(|| anyhow!("No GPU device available"))?;
72
-
73
- let shader_module = gpu.device.create_shader_module(ShaderModuleDescriptor {
74
- label: Some("compute-shader"),
75
- source: ShaderSource::Wgsl(std::borrow::Cow::Borrowed(shader_src)),
76
- });
77
-
78
- let pipeline_layout = gpu.device.create_pipeline_layout(&PipelineLayoutDescriptor {
79
- label: Some("compute-layout"),
80
- bind_group_layouts: &[],
81
- push_constant_ranges: &[],
82
- });
83
-
84
- Ok(gpu.device.create_compute_pipeline(&ComputePipelineDescriptor {
85
- label: Some("compute-pipeline"),
86
- layout: Some(&pipeline_layout),
87
- module: &shader_module,
88
- entry_point,
89
- }))
90
- }
91
-
92
- pub fn create_buffer_init(&self, data: &[u8], usage: BufferUsages) -> Result<Buffer> {
93
- let gpu = self.inner.read();
94
- let gpu = gpu.as_ref().ok_or_else(|| anyhow!("No GPU device available"))?;
95
-
96
- Ok(gpu.device.create_buffer_init(&util::BufferInitDescriptor {
97
- label: None,
98
- contents: data,
99
- usage,
100
- }))
101
- }
102
- }
103
-
104
- pub fn gpu_available() -> bool {
105
- pollster::block_on(async {
106
- let instance = Instance::new(InstanceDescriptor {
107
- backends: Backends::all(),
108
- ..Default::default()
109
- });
110
- instance.request_adapter(&RequestAdapterOptions {
111
- power_preference: PowerPreference::HighPerformance,
112
- compatible_surface: None,
113
- force_fallback_adapter: false,
114
- }).await.is_some()
115
- })
116
- }