roxify 1.13.8 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/cli.js +23 -21
  2. package/dist/stub-progress.d.ts +4 -4
  3. package/dist/stub-progress.js +4 -4
  4. package/dist/utils/decoder.d.ts +10 -1
  5. package/dist/utils/decoder.js +111 -7
  6. package/dist/utils/ecc.js +0 -1
  7. package/dist/utils/inspection.d.ts +1 -1
  8. package/dist/utils/inspection.js +2 -2
  9. package/dist/utils/robust-audio.js +0 -13
  10. package/dist/utils/robust-image.js +0 -26
  11. package/package.json +12 -29
  12. package/roxify_native-aarch64-apple-darwin.node +0 -0
  13. package/roxify_native-aarch64-pc-windows-msvc.node +0 -0
  14. package/roxify_native-aarch64-unknown-linux-gnu.node +0 -0
  15. package/roxify_native-i686-pc-windows-msvc.node +0 -0
  16. package/roxify_native-i686-unknown-linux-gnu.node +0 -0
  17. package/{dist/rox-macos-universal → roxify_native-universal-apple-darwin.node} +0 -0
  18. package/roxify_native-x86_64-apple-darwin.node +0 -0
  19. package/roxify_native-x86_64-pc-windows-msvc.node +0 -0
  20. package/roxify_native-x86_64-unknown-linux-gnu.node +0 -0
  21. package/scripts/postinstall.cjs +23 -2
  22. package/Cargo.toml +0 -91
  23. package/dist/roxify_native +0 -0
  24. package/dist/roxify_native-macos-arm64 +0 -0
  25. package/dist/roxify_native-macos-x64 +0 -0
  26. package/dist/roxify_native.exe +0 -0
  27. package/native/archive.rs +0 -220
  28. package/native/audio.rs +0 -151
  29. package/native/bench_hybrid.rs +0 -145
  30. package/native/bwt.rs +0 -56
  31. package/native/context_mixing.rs +0 -117
  32. package/native/core.rs +0 -378
  33. package/native/crypto.rs +0 -209
  34. package/native/encoder.rs +0 -405
  35. package/native/hybrid.rs +0 -297
  36. package/native/image_utils.rs +0 -82
  37. package/native/io_advice.rs +0 -43
  38. package/native/io_ntfs_optimized.rs +0 -99
  39. package/native/lib.rs +0 -480
  40. package/native/main.rs +0 -842
  41. package/native/mtf.rs +0 -106
  42. package/native/packer.rs +0 -604
  43. package/native/png_chunk_writer.rs +0 -146
  44. package/native/png_utils.rs +0 -554
  45. package/native/pool.rs +0 -101
  46. package/native/progress.rs +0 -142
  47. package/native/rans.rs +0 -149
  48. package/native/rans_byte.rs +0 -286
  49. package/native/reconstitution.rs +0 -623
  50. package/native/streaming.rs +0 -189
  51. package/native/streaming_decode.rs +0 -625
  52. package/native/streaming_encode.rs +0 -684
  53. package/native/test_small_bwt.rs +0 -31
  54. package/native/test_stages.rs +0 -70
package/native/main.rs DELETED
@@ -1,842 +0,0 @@
1
- #![allow(dead_code)]
2
- use clap::{Parser, Subcommand};
3
- use std::fs::File;
4
- use std::io::{Read, Write};
5
-
6
- #[global_allocator]
7
- static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
8
-
9
- mod core;
10
- mod encoder;
11
- mod packer;
12
- mod crypto;
13
- mod png_utils;
14
- mod png_chunk_writer;
15
- mod io_advice;
16
- mod audio;
17
- mod reconstitution;
18
- mod archive;
19
- mod streaming;
20
- mod streaming_decode;
21
- mod streaming_encode;
22
- mod progress;
23
-
24
- use crate::encoder::ImageFormat;
25
- use std::path::PathBuf;
26
-
27
- const MB_U64: u64 = 1024 * 1024;
28
- const GB_U64: u64 = 1024 * MB_U64;
29
-
30
- // Adaptive RAM Budget Configuration
31
- const MIN_RAM_BUDGET_MB: u64 = 512;
32
- const DEFAULT_RAM_BUDGET_MB: u64 = 2048;
33
- const RESERVED_RAM_MB: u64 = 1024;
34
- const MIN_WRITE_BUFFER_BYTES: usize = 16 * 1024 * 1024;
35
- const MAX_WRITE_BUFFER_BYTES: usize = 256 * 1024 * 1024;
36
-
37
- // Performance Targets (under 10 seconds)
38
- const TARGET_ENCODE_TIME_SECS: u64 = 10;
39
- const TARGET_DECODE_TIME_SECS: u64 = 10;
40
- const FAST_COMPRESSION_THRESHOLD_MB: u64 = 100;
41
- const STREAMING_THRESHOLD_MB: u64 = 500;
42
-
43
- // Adaptive Compression Levels
44
- const COMPRESSION_ULTRA_FAST: i32 = 1; // < 100MB files, lots of RAM
45
- const COMPRESSION_FAST: i32 = 2; // < 500MB files
46
- const COMPRESSION_BALANCED: i32 = 3; // Default, 500MB-2GB
47
- const COMPRESSION_SMALL_FILES: i32 = 5; // Many small files
48
-
49
- // RAM Usage Tiers
50
- const RAM_TIER_ULTRA: u64 = 16384; // 16GB+ - Aggressive optimization
51
- const RAM_TIER_HIGH: u64 = 8192; // 8GB+ - Fast mode
52
- const RAM_TIER_MEDIUM: u64 = 4096; // 4GB+ - Balanced
53
- const RAM_TIER_LOW: u64 = 2048; // 2GB+ - Conservative
54
-
55
- #[derive(Parser)]
56
- #[command(author, version)]
57
- struct Cli {
58
- #[command(subcommand)]
59
- command: Commands,
60
- }
61
-
62
- #[derive(Subcommand)]
63
- enum Commands {
64
- Encode {
65
- input: PathBuf,
66
- output: PathBuf,
67
- #[arg(short, long, default_value_t = 3)]
68
- level: i32,
69
- #[arg(short, long)]
70
- passphrase: Option<String>,
71
- #[arg(short, long, default_value = "aes")]
72
- encrypt: String,
73
- #[arg(short, long)]
74
- name: Option<String>,
75
- /// optional zstd dictionary file for payload compression
76
- #[arg(long, value_name = "FILE")]
77
- dict: Option<PathBuf>,
78
- #[arg(long, value_name = "MB")]
79
- ram_budget_mb: Option<u64>,
80
- },
81
-
82
- List {
83
- input: PathBuf,
84
- },
85
- Havepassphrase {
86
- input: PathBuf,
87
- },
88
- Scan {
89
- input: PathBuf,
90
- #[arg(short, long, value_name = "FILE")]
91
- #[arg(short, long, default_value_t = 4)]
92
- channels: usize,
93
- #[arg(short, long, value_delimiter = ',')]
94
- markers: Vec<String>,
95
- },
96
- DeltaEncode {
97
- input: PathBuf,
98
- output: Option<PathBuf>,
99
- },
100
- DeltaDecode {
101
- input: PathBuf,
102
- output: Option<PathBuf>,
103
- },
104
- Compress {
105
- input: PathBuf,
106
- output: Option<PathBuf>,
107
- #[arg(short, long, default_value_t = 19)]
108
- level: i32,
109
- /// optional zstd dictionary file to use for compression
110
- #[arg(long, value_name = "FILE")]
111
- dict: Option<PathBuf>,
112
- },
113
- TrainDict {
114
- /// sample files used to train the dictionary
115
- #[arg(short, long, value_name = "FILE", required = true)]
116
- samples: Vec<PathBuf>,
117
- /// desired dictionary size in bytes
118
- #[arg(short, long, default_value_t = 112640)]
119
- size: usize,
120
- /// output dictionary file
121
- output: PathBuf,
122
- },
123
- Decompress {
124
- input: PathBuf,
125
- output: Option<PathBuf>,
126
- #[arg(long)]
127
- files: Option<String>,
128
- #[arg(short, long)]
129
- passphrase: Option<String>,
130
- /// optional dictionary file used during decompression
131
- #[arg(long, value_name = "FILE")]
132
- dict: Option<PathBuf>,
133
- #[arg(long, value_name = "MB")]
134
- ram_budget_mb: Option<u64>,
135
- },
136
- Crc32 {
137
- input: PathBuf,
138
- },
139
- Adler32 {
140
- input: PathBuf,
141
- },
142
- }
143
-
144
- fn read_all(path: &PathBuf) -> anyhow::Result<Vec<u8>> {
145
- let metadata = std::fs::metadata(path)?;
146
- let size = metadata.len() as usize;
147
- let mut f = File::open(path)?;
148
- let mut buf = Vec::with_capacity(size);
149
- f.read_to_end(&mut buf)?;
150
- Ok(buf)
151
- }
152
-
153
- pub fn parse_linux_mem_available_mb() -> Option<u64> {
154
- let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
155
- let line = meminfo.lines().find(|l| l.starts_with("MemAvailable:"))?;
156
- let kb = line
157
- .split_whitespace()
158
- .nth(1)
159
- .and_then(|v| v.parse::<u64>().ok())?;
160
- Some(kb / 1024)
161
- }
162
-
163
- fn parse_total_ram_mb() -> Option<u64> {
164
- let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
165
- let line = meminfo.lines().find(|l| l.starts_with("MemTotal:"))?;
166
- let kb = line
167
- .split_whitespace()
168
- .nth(1)
169
- .and_then(|v| v.parse::<u64>().ok())?;
170
- Some(kb / 1024)
171
- }
172
-
173
- fn get_cpu_cores() -> usize {
174
- std::thread::available_parallelism()
175
- .map(|p| p.get())
176
- .unwrap_or(4)
177
- }
178
-
179
- /// Determine RAM tier based on available memory
180
- fn get_ram_tier(available_mb: u64) -> &'static str {
181
- match available_mb {
182
- x if x >= RAM_TIER_ULTRA => "ultra",
183
- x if x >= RAM_TIER_HIGH => "high",
184
- x if x >= RAM_TIER_MEDIUM => "medium",
185
- _ => "low",
186
- }
187
- }
188
-
189
- /// Calculate adaptive RAM budget with aggressive optimization for Pyxelze
190
- fn auto_ram_budget_mb() -> u64 {
191
- let total_mb = parse_total_ram_mb().unwrap_or(4096);
192
- let available_mb = parse_linux_mem_available_mb().unwrap_or(total_mb / 2);
193
- let cpu_cores = get_cpu_cores();
194
-
195
- // Base calculation: use up to 85% of available RAM for ultra-fast mode
196
- let base_budget = available_mb.saturating_mul(85) / 100;
197
-
198
- // Adjust based on RAM tier and CPU cores
199
- let tier_multiplier = match get_ram_tier(total_mb) {
200
- "ultra" if cpu_cores >= 8 => 90, // 16GB+ with 8+ cores: 90%
201
- "ultra" => 85, // 16GB+ with fewer cores
202
- "high" if cpu_cores >= 6 => 80, // 8GB+ with 6+ cores
203
- "high" => 75, // 8GB+
204
- "medium" => 70, // 4GB+
205
- _ => 65, // 2GB+
206
- };
207
-
208
- let budget = available_mb.saturating_mul(tier_multiplier) / 100;
209
- let budget = budget.max(MIN_RAM_BUDGET_MB);
210
-
211
- // Cap at reasonable maximum to prevent OOM
212
- let max_budget = (total_mb / 2).max(8192);
213
- budget.min(max_budget)
214
- }
215
-
216
- /// Auto-select optimal compression level based on file size and available RAM
217
- fn auto_compression_level(file_size_mb: u64, ram_budget_mb: u64) -> i32 {
218
- // Ultra-fast for small files with lots of RAM
219
- if file_size_mb < FAST_COMPRESSION_THRESHOLD_MB && ram_budget_mb >= RAM_TIER_HIGH {
220
- return COMPRESSION_ULTRA_FAST;
221
- }
222
-
223
- // Fast mode for medium files
224
- if file_size_mb < STREAMING_THRESHOLD_MB {
225
- return COMPRESSION_FAST;
226
- }
227
-
228
- // Balanced for large files
229
- if ram_budget_mb >= RAM_TIER_MEDIUM {
230
- return COMPRESSION_BALANCED;
231
- }
232
-
233
- // Conservative for low RAM
234
- COMPRESSION_SMALL_FILES
235
- }
236
-
237
- /// Determine if streaming mode should be used
238
- fn should_use_streaming(file_size_mb: u64, ram_budget_mb: u64) -> bool {
239
- // Force streaming for very large files
240
- if file_size_mb >= 2048 { // 2GB+
241
- return true;
242
- }
243
-
244
- // Use streaming when file is larger than 60% of RAM budget
245
- let threshold = ram_budget_mb.saturating_mul(60) / 100;
246
- file_size_mb >= threshold
247
- }
248
-
249
- /// Get optimal thread count for zstd based on RAM and CPU
250
- fn optimal_zstd_threads(file_size_mb: u64, ram_budget_mb: u64) -> i32 {
251
- let cpu_cores = get_cpu_cores();
252
- let ram_tier = get_ram_tier(parse_total_ram_mb().unwrap_or(4096));
253
-
254
- match ram_tier {
255
- "ultra" => cpu_cores.min(16) as i32,
256
- "high" => cpu_cores.min(8) as i32,
257
- _ if file_size_mb > 1000 => cpu_cores.min(4) as i32,
258
- _ => cpu_cores.min(2) as i32,
259
- }
260
- }
261
-
262
- fn resolve_ram_budget_mb(cli_value: Option<u64>) -> u64 {
263
- if let Some(v) = cli_value {
264
- return v.max(MIN_RAM_BUDGET_MB);
265
- }
266
-
267
- if let Ok(v) = std::env::var("ROX_RAM_BUDGET_MB") {
268
- if let Ok(parsed) = v.trim().parse::<u64>() {
269
- return parsed.max(MIN_RAM_BUDGET_MB);
270
- }
271
- }
272
-
273
- auto_ram_budget_mb()
274
- }
275
-
276
- fn effective_ram_budget_mb() -> u64 {
277
- if let Ok(v) = std::env::var("ROX_RAM_BUDGET_MB_EFFECTIVE") {
278
- if let Ok(parsed) = v.trim().parse::<u64>() {
279
- return parsed.max(MIN_RAM_BUDGET_MB);
280
- }
281
- }
282
- resolve_ram_budget_mb(None)
283
- }
284
-
285
- fn ram_budget_bytes(ram_budget_mb: u64) -> u64 {
286
- ram_budget_mb.saturating_mul(MB_U64)
287
- }
288
-
289
- fn streaming_preference_threshold_bytes(ram_budget_mb: u64) -> u64 {
290
- ram_budget_bytes(ram_budget_mb)
291
- .saturating_mul(60)
292
- .saturating_div(100)
293
- .max(64 * MB_U64)
294
- }
295
-
296
- fn normalize_png_archive_bytes(png_data: &[u8], passphrase: Option<&str>) -> anyhow::Result<Vec<u8>> {
297
- let payload = png_utils::extract_payload_from_png(png_data).map_err(|e| anyhow::anyhow!(e))?;
298
- if payload.is_empty() {
299
- return Err(anyhow::anyhow!("Empty payload"));
300
- }
301
-
302
- if payload[0] == 0x00u8 {
303
- Ok(payload[1..].to_vec())
304
- } else {
305
- let decrypted = crate::crypto::try_decrypt(&payload, passphrase)
306
- .map_err(|e| anyhow::anyhow!("Encrypted payload: {}", e))?;
307
- Ok(decrypted)
308
- }
309
- }
310
-
311
- fn unpack_archive_bytes(
312
- normalized: Vec<u8>,
313
- out_dir: &std::path::Path,
314
- files_slice: Option<&[String]>,
315
- progress: Option<&(dyn Fn(u64, u64, &str) + Send)>,
316
- ) -> anyhow::Result<Vec<String>> {
317
- let mut reader: Box<dyn std::io::Read> = if normalized.starts_with(b"ROX1") {
318
- Box::new(std::io::Cursor::new(normalized[4..].to_vec()))
319
- } else {
320
- let mut dec = zstd::stream::Decoder::new(std::io::Cursor::new(normalized))
321
- .map_err(|e| anyhow::anyhow!("zstd decoder init: {}", e))?;
322
- dec.window_log_max(31)
323
- .map_err(|e| anyhow::anyhow!("zstd window_log_max: {}", e))?;
324
- Box::new(dec)
325
- };
326
-
327
- std::fs::create_dir_all(out_dir)
328
- .map_err(|e| anyhow::anyhow!("Cannot create output directory {:?}: {}", out_dir, e))?;
329
- packer::unpack_stream_to_dir(&mut reader, out_dir, files_slice, progress, 0)
330
- .map_err(|e| anyhow::anyhow!(e))
331
- }
332
-
333
- fn write_all(path: &PathBuf, data: &[u8]) -> anyhow::Result<()> {
334
- let f = File::create(path)?;
335
- let budget_bytes = ram_budget_bytes(effective_ram_budget_mb());
336
- let dynamic_cap = (budget_bytes / 24)
337
- .clamp(MIN_WRITE_BUFFER_BYTES as u64, MAX_WRITE_BUFFER_BYTES as u64) as usize;
338
- let buf_size = dynamic_cap.min(data.len().max(8192));
339
- let mut writer = std::io::BufWriter::with_capacity(buf_size, f);
340
- writer.write_all(data)?;
341
- writer.flush()?;
342
- Ok(())
343
- }
344
-
345
- fn parse_markers(v: &[String]) -> Option<Vec<u8>> {
346
- if v.is_empty() {
347
- return None;
348
- }
349
- let mut out = Vec::new();
350
- for s in v {
351
- let parts: Vec<&str> = s.split(|c| c == ':' || c == ',' ).collect();
352
- if parts.len() >= 3 {
353
- if let (Ok(r), Ok(g), Ok(b)) = (parts[0].parse::<u8>(), parts[1].parse::<u8>(), parts[2].parse::<u8>()) {
354
- out.push(r); out.push(g); out.push(b);
355
- }
356
- }
357
- }
358
- if out.is_empty() { None } else { Some(out) }
359
- }
360
-
361
- fn main() -> anyhow::Result<()> {
362
- let cli = Cli::parse();
363
-
364
- fn parse_requested_files(files: &str) -> anyhow::Result<Vec<String>> {
365
- if files.trim_start().starts_with('[') {
366
- serde_json::from_str::<Vec<String>>(files)
367
- .map_err(|e| anyhow::anyhow!("Invalid JSON for --files: {}", e))
368
- } else {
369
- Ok(files
370
- .split(',')
371
- .map(|file| file.trim().to_string())
372
- .filter(|file| !file.is_empty())
373
- .collect())
374
- }
375
- }
376
- match cli.command {
377
- Commands::TrainDict { samples, size, output } => {
378
- let dict = core::train_zstd_dictionary(&samples, size)?;
379
- write_all(&output, &dict)?;
380
- println!("wrote {} bytes dictionary to {:?}", dict.len(), output);
381
- return Ok(());
382
- }
383
- Commands::Encode { input, output, level, passphrase, encrypt, name, dict, ram_budget_mb } => {
384
- let ram_budget_mb = resolve_ram_budget_mb(ram_budget_mb);
385
- std::env::set_var("ROX_RAM_BUDGET_MB_EFFECTIVE", ram_budget_mb.to_string());
386
- let is_dir = input.is_dir();
387
-
388
- let file_name = name.as_deref()
389
- .or_else(|| input.file_name().and_then(|n| n.to_str()));
390
-
391
- if is_dir && dict.is_none() {
392
- streaming_encode::encode_dir_to_png_encrypted_with_progress(
393
- &input,
394
- &output,
395
- level,
396
- file_name,
397
- passphrase.as_deref(),
398
- Some(&encrypt),
399
- Some(Box::new(|current, total, step| {
400
- eprintln!("PROGRESS:{}:{}:{}", current, total, step);
401
- })),
402
- )?;
403
- println!("(directory payload, rXFL chunk embedded)");
404
- return Ok(());
405
- }
406
-
407
- let (payload, file_list_json) = if is_dir {
408
- let result = archive::tar_pack_directory_with_list(&input)
409
- .map_err(|e| anyhow::anyhow!(e))?;
410
- let json_list: Vec<serde_json::Value> = result.file_list.iter()
411
- .map(|(name, size)| serde_json::json!({"name": name, "size": size}))
412
- .collect();
413
- (result.data, Some(serde_json::to_string(&json_list)?))
414
- } else {
415
- let pack_result = packer::pack_path_with_metadata(&input)?;
416
- (pack_result.data, pack_result.file_list_json)
417
- };
418
-
419
- let dict_bytes: Option<Vec<u8>> = match dict {
420
- Some(path) => Some(read_all(&path)?),
421
- None => None,
422
- };
423
-
424
- let use_streaming = (payload.len() as u64) > streaming_preference_threshold_bytes(ram_budget_mb);
425
- eprintln!("PROGRESS:50:100:encoding");
426
-
427
- if use_streaming {
428
- streaming::encode_to_png_file(
429
- &payload,
430
- &output,
431
- level,
432
- passphrase.as_deref(),
433
- Some(&encrypt),
434
- file_name,
435
- file_list_json.as_deref(),
436
- dict_bytes.as_deref(),
437
- )?;
438
- } else {
439
- let png = if let Some(ref pass) = passphrase {
440
- encoder::encode_to_png_with_encryption_name_and_format_and_filelist(
441
- &payload,
442
- level,
443
- Some(pass),
444
- Some(&encrypt),
445
- ImageFormat::Png,
446
- file_name,
447
- file_list_json.as_deref(),
448
- dict_bytes.as_deref(),
449
- )?
450
- } else {
451
- encoder::encode_to_png_with_encryption_name_and_format_and_filelist(
452
- &payload,
453
- level,
454
- None,
455
- None,
456
- ImageFormat::Png,
457
- file_name,
458
- file_list_json.as_deref(),
459
- dict_bytes.as_deref(),
460
- )?
461
- };
462
- write_all(&output, &png)?;
463
- }
464
-
465
- if file_list_json.is_some() {
466
- eprintln!("PROGRESS:100:100:done");
467
- if is_dir {
468
- println!("(directory payload, rXFL chunk embedded)");
469
- } else {
470
- println!("(rXFL chunk embedded)");
471
- }
472
- }
473
- }
474
- Commands::List { input } => {
475
- let mut file = File::open(&input)?;
476
- let mut chunk_scan_error: Option<anyhow::Error> = None;
477
-
478
- match png_utils::extract_png_chunks_streaming(&mut file) {
479
- Ok(chunks) => {
480
- if let Some(rxfl_chunk) = chunks.iter().find(|c| c.name == "rXFL") {
481
- println!("{}", String::from_utf8_lossy(&rxfl_chunk.data));
482
- return Ok(());
483
- }
484
-
485
- if let Some(meta_chunk) = chunks.iter().find(|c| c.name == "rOXm") {
486
- if let Some(pos) = meta_chunk.data.windows(4).position(|w| w == b"rXFL") {
487
- if pos + 8 <= meta_chunk.data.len() {
488
- let json_len = u32::from_be_bytes([
489
- meta_chunk.data[pos + 4],
490
- meta_chunk.data[pos + 5],
491
- meta_chunk.data[pos + 6],
492
- meta_chunk.data[pos + 7],
493
- ]) as usize;
494
-
495
- let json_start = pos + 8;
496
- let json_end = json_start + json_len;
497
-
498
- if json_end <= meta_chunk.data.len() {
499
- println!("{}", String::from_utf8_lossy(&meta_chunk.data[json_start..json_end]));
500
- return Ok(());
501
- }
502
- }
503
- }
504
- }
505
- }
506
- Err(err) => {
507
- chunk_scan_error = Some(anyhow::anyhow!(err));
508
- }
509
- }
510
-
511
- let png_data = std::fs::read(&input)?;
512
- match png_utils::extract_file_list_from_pixels(&png_data) {
513
- Ok(json) => {
514
- println!("{}", json);
515
- return Ok(());
516
- }
517
- Err(pixel_err) => {
518
- if let Some(chunk_err) = chunk_scan_error {
519
- return Err(anyhow::anyhow!("chunk scan: {}; pixel scan: {}", chunk_err, pixel_err));
520
- }
521
- }
522
- }
523
-
524
- return Err(anyhow::anyhow!("No file list found in PNG"));
525
- }
526
- Commands::Havepassphrase { input } => {
527
- let buf = read_all(&input)?;
528
- let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
529
- if is_png {
530
- let payload = png_utils::extract_payload_from_png(&buf).map_err(|e| anyhow::anyhow!(e))?;
531
- if !payload.is_empty() && (payload[0] == 0x01 || payload[0] == 0x02 || payload[0] == 0x03) {
532
- println!("Passphrase detected.");
533
- } else {
534
- println!("No passphrase detected.");
535
- }
536
- } else {
537
- if !buf.is_empty() && (buf[0] == 0x01 || buf[0] == 0x02 || buf[0] == 0x03) {
538
- println!("Passphrase detected.");
539
- } else {
540
- println!("No passphrase detected.");
541
- }
542
- }
543
- }
544
- Commands::Scan { input, channels, markers } => {
545
- let buf = read_all(&input)?;
546
- let marker_bytes = parse_markers(&markers);
547
- let res = crate::core::scan_pixels_bytes(&buf, channels, marker_bytes.as_deref());
548
- println!("magic_positions: {:?}", res.magic_positions);
549
- println!("marker_positions: {:?}", res.marker_positions);
550
- }
551
- Commands::DeltaEncode { input, output } => {
552
- let buf = read_all(&input)?;
553
- let out = crate::core::delta_encode_bytes(&buf);
554
- let dest = output.unwrap_or_else(|| PathBuf::from("delta.bin"));
555
- write_all(&dest, &out)?;
556
- }
557
- Commands::DeltaDecode { input, output } => {
558
- let buf = read_all(&input)?;
559
- let out = crate::core::delta_decode_bytes(&buf);
560
- let dest = output.unwrap_or_else(|| PathBuf::from("raw.bin"));
561
- write_all(&dest, &out)?;
562
- }
563
- Commands::Compress { input, output, level, dict } => {
564
- let buf = read_all(&input)?;
565
- let dict_bytes: Option<Vec<u8>> = match dict {
566
- Some(path) => Some(read_all(&path)?),
567
- None => None,
568
- };
569
- let out = crate::core::zstd_compress_bytes(
570
- &buf,
571
- level,
572
- dict_bytes.as_deref(),
573
- )
574
- .map_err(|e: String| anyhow::anyhow!(e))?;
575
- let dest = output.unwrap_or_else(|| PathBuf::from("out.zst"));
576
- write_all(&dest, &out)?;
577
- }
578
- Commands::Decompress { input, output, files, passphrase, dict, ram_budget_mb } => {
579
- let ram_budget_mb = resolve_ram_budget_mb(ram_budget_mb);
580
- std::env::set_var("ROX_RAM_BUDGET_MB_EFFECTIVE", ram_budget_mb.to_string());
581
- let file_size = std::fs::metadata(&input).map(|m| m.len()).unwrap_or(0);
582
- let is_png_file = input.extension().map(|e| e == "png").unwrap_or(false)
583
- || (file_size >= 8 && {
584
- let mut sig = [0u8; 8];
585
- std::fs::File::open(&input).and_then(|mut f| { use std::io::Read; f.read_exact(&mut sig) }).is_ok()
586
- && sig == [137, 80, 78, 71, 13, 10, 26, 10]
587
- });
588
-
589
- let requested_files = match files.as_deref() {
590
- Some(files_str) => Some(parse_requested_files(files_str)?),
591
- None => None,
592
- };
593
-
594
- if is_png_file && dict.is_none() {
595
- let out_dir = if requested_files.is_some() {
596
- output.clone().unwrap_or_else(|| PathBuf::from("."))
597
- } else {
598
- output.clone().unwrap_or_else(|| PathBuf::from("out.raw"))
599
- };
600
-
601
- let should_try_streaming = file_size >= streaming_preference_threshold_bytes(ram_budget_mb);
602
-
603
- if should_try_streaming {
604
- let streaming_result = if let Some(ref selected) = requested_files {
605
- streaming_decode::streaming_decode_selected_to_dir_encrypted_with_progress(
606
- &input,
607
- &out_dir,
608
- Some(selected.as_slice()),
609
- passphrase.as_deref(),
610
- Some(Box::new(|current, total, step| {
611
- eprintln!("PROGRESS:{}:{}:{}", current, total, step);
612
- })),
613
- )
614
- } else {
615
- streaming_decode::streaming_decode_to_dir_encrypted_with_progress(
616
- &input,
617
- &out_dir,
618
- passphrase.as_deref(),
619
- Some(Box::new(|current, total, step| {
620
- eprintln!("PROGRESS:{}:{}:{}", current, total, step);
621
- })),
622
- )
623
- };
624
-
625
- match streaming_result {
626
- Ok(written) => {
627
- eprintln!("PROGRESS:100:100:done");
628
- println!("Unpacked {} files", written.len());
629
- return Ok(());
630
- }
631
- Err(e) => {
632
- eprintln!("PROGRESS:12:100:streaming_fallback");
633
- eprintln!("Streaming decode failed, falling back to reconstruction: {}", e);
634
- }
635
- }
636
- }
637
-
638
- if file_size > ram_budget_bytes(ram_budget_mb) {
639
- return Err(anyhow::anyhow!(
640
- "PNG fallback requires in-memory reconstruction ({} MB file > {} MB RAM budget). Increase --ram-budget-mb.",
641
- file_size / MB_U64,
642
- ram_budget_mb
643
- ));
644
- }
645
-
646
- let buf = read_all(&input)?;
647
-
648
- eprintln!("PROGRESS:20:100:decompressing");
649
- let progress_cb = |current: u64, total: u64, step: &str| {
650
- eprintln!("PROGRESS:{}:{}:{}", current, total, step);
651
- };
652
-
653
- let normalized = normalize_png_archive_bytes(&buf, passphrase.as_deref())?;
654
- let written = unpack_archive_bytes(
655
- normalized,
656
- &out_dir,
657
- requested_files.as_deref(),
658
- Some(&progress_cb),
659
- )?;
660
- eprintln!("PROGRESS:100:100:done");
661
- println!("Unpacked {} files", written.len());
662
- return Ok(());
663
- }
664
-
665
- if file_size > ram_budget_bytes(ram_budget_mb) {
666
- return Err(anyhow::anyhow!(
667
- "Input is {} MB but RAM budget is {} MB. Increase --ram-budget-mb for non-streaming decode paths.",
668
- file_size / MB_U64,
669
- ram_budget_mb
670
- ));
671
- }
672
-
673
- let buf = read_all(&input)?;
674
-
675
- eprintln!("PROGRESS:20:100:decompressing");
676
- let dict_bytes: Option<Vec<u8>> = match dict {
677
- Some(path) => Some(read_all(&path)?),
678
- None => None,
679
- };
680
- if requested_files.is_some() {
681
- let file_list = requested_files;
682
-
683
- let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
684
-
685
- use std::io::Cursor;
686
- let normalized: Vec<u8> = if is_png {
687
- let payload = png_utils::extract_payload_from_png(&buf).map_err(|e| anyhow::anyhow!(e))?;
688
- if payload.is_empty() { return Err(anyhow::anyhow!("Empty payload")); }
689
- if payload[0] == 0x00u8 {
690
- payload[1..].to_vec()
691
- } else {
692
- let pass = passphrase.as_ref().map(|s: &String| s.as_str());
693
- match crate::crypto::try_decrypt(&payload, pass) {
694
- Ok(v) => v,
695
- Err(e) => return Err(anyhow::anyhow!("Encrypted payload: {}", e)),
696
- }
697
- }
698
- } else {
699
- if buf[0] == 0x00u8 {
700
- buf[1..].to_vec()
701
- } else if buf.starts_with(b"ROX1") {
702
- buf[4..].to_vec()
703
- } else if buf[0] == 0x01u8 || buf[0] == 0x02u8 || buf[0] == 0x03u8 {
704
- let pass = passphrase.as_ref().map(|s: &String| s.as_str());
705
- match crate::crypto::try_decrypt(&buf, pass) {
706
- Ok(v) => v,
707
- Err(e) => return Err(anyhow::anyhow!("Encrypted payload: {}", e)),
708
- }
709
- } else {
710
- buf.to_vec()
711
- }
712
- };
713
-
714
- let mut reader: Box<dyn std::io::Read> = if normalized.starts_with(b"ROX1") {
715
- Box::new(Cursor::new(normalized[4..].to_vec()))
716
- } else {
717
- let mut dec = zstd::stream::Decoder::new(Cursor::new(normalized)).map_err(|e| anyhow::anyhow!("zstd decoder init: {}", e))?;
718
- dec.window_log_max(31).map_err(|e| anyhow::anyhow!("zstd window_log_max: {}", e))?;
719
- Box::new(dec)
720
- };
721
-
722
- let out_dir = output.unwrap_or_else(|| PathBuf::from("."));
723
- std::fs::create_dir_all(&out_dir).map_err(|e| anyhow::anyhow!("Cannot create output directory {:?}: {}", out_dir, e))?;
724
- let files_slice = file_list.as_ref().map(|v| v.as_slice());
725
-
726
- let written = packer::unpack_stream_to_dir(&mut reader, &out_dir, files_slice, Some(&|current, total, step| {
727
- eprintln!("PROGRESS:{}:{}:{}", current, total, step);
728
- }), 0).map_err(|e| anyhow::anyhow!(e))?;
729
- eprintln!("PROGRESS:100:100:done");
730
- println!("Unpacked {} files", written.len());
731
- } else {
732
- let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
733
- let out_bytes = if is_png {
734
- let payload = png_utils::extract_payload_from_png(&buf).map_err(|e| anyhow::anyhow!(e))?;
735
- if payload.is_empty() { return Err(anyhow::anyhow!("Empty payload")); }
736
- let first = payload[0];
737
- if first == 0x00u8 {
738
- let compressed = payload[1..].to_vec();
739
- match crate::core::zstd_decompress_bytes(&compressed, dict_bytes.as_deref()) {
740
- Ok(mut o) => {
741
- if o.starts_with(b"ROX1") { o = o[4..].to_vec(); }
742
- o
743
- }
744
- Err(e) => {
745
- if compressed.starts_with(b"ROX1") {
746
- eprintln!("⚠️ zstd decompress failed ({}), but payload starts with ROX1: falling back to raw pack", e);
747
- compressed[4..].to_vec()
748
- } else {
749
- return Err(anyhow::anyhow!("zstd decompress error: {}", e));
750
- }
751
- }
752
- }
753
- } else {
754
- let pass = passphrase.as_ref().map(|s| s.as_str());
755
- match crate::crypto::try_decrypt(&payload, pass) {
756
- Ok(v) => {
757
- let inner = if v.starts_with(b"ROX1") {
758
- v[4..].to_vec()
759
- } else {
760
- v
761
- };
762
- match crate::core::zstd_decompress_bytes(&inner, dict_bytes.as_deref()) {
763
- Ok(mut o) => {
764
- if o.starts_with(b"ROX1") { o = o[4..].to_vec(); }
765
- o
766
- }
767
- Err(_) => inner,
768
- }
769
- }
770
- Err(e) => return Err(anyhow::anyhow!("Encrypted payload: {}", e)),
771
- }
772
- }
773
- } else {
774
- match crate::core::zstd_decompress_bytes(&buf, dict_bytes.as_deref()) {
775
- Ok(mut x) => { if x.starts_with(b"ROX1") { x = x[4..].to_vec(); } x },
776
- Err(e) => {
777
- if buf.starts_with(b"ROX1") {
778
- eprintln!("⚠️ zstd decompress failed ({}), but input already starts with ROX1: using raw pack", e);
779
- buf[4..].to_vec()
780
- } else {
781
- return Err(anyhow::anyhow!("zstd decompress error: {}", e));
782
- }
783
- }
784
- }
785
- };
786
-
787
- let dest = output.unwrap_or_else(|| PathBuf::from("out.raw"));
788
-
789
- if archive::is_tar(&out_bytes) {
790
- let out_dir = if dest.extension().is_none() || dest.is_dir() {
791
- dest
792
- } else {
793
- PathBuf::from(".")
794
- };
795
- std::fs::create_dir_all(&out_dir)
796
- .map_err(|e| anyhow::anyhow!("mkdir {:?}: {}", out_dir, e))?;
797
- let written = archive::tar_unpack(&out_bytes, &out_dir)
798
- .map_err(|e| anyhow::anyhow!(e))?;
799
- println!("Unpacked {} files to {:?}", written.len(), out_dir);
800
- } else if out_bytes.len() >= 4
801
- && (u32::from_be_bytes(out_bytes[0..4].try_into().unwrap()) == 0x524f5850u32
802
- || u32::from_be_bytes(out_bytes[0..4].try_into().unwrap()) == 0x524f5849u32)
803
- {
804
- let out_dir = if dest.extension().is_none() || dest.is_dir() {
805
- dest
806
- } else {
807
- PathBuf::from(".")
808
- };
809
- std::fs::create_dir_all(&out_dir)
810
- .map_err(|e| anyhow::anyhow!("mkdir {:?}: {}", out_dir, e))?;
811
- let written = packer::unpack_buffer_to_dir(&out_bytes, &out_dir, None)
812
- .map_err(|e| anyhow::anyhow!(e))?;
813
- println!("Unpacked {} files to {:?}", written.len(), out_dir);
814
- } else if dest.is_dir() {
815
- let fname = if is_png {
816
- png_utils::extract_name_from_png(&buf)
817
- } else {
818
- None
819
- }.unwrap_or_else(|| {
820
- input.file_stem()
821
- .map(|s| s.to_string_lossy().to_string())
822
- .unwrap_or_else(|| "out.raw".to_string())
823
- });
824
- write_all(&dest.join(&fname), &out_bytes)?;
825
- } else {
826
- write_all(&dest, &out_bytes)?;
827
- }
828
- eprintln!("PROGRESS:100:100:done");
829
- }
830
- }
831
- Commands::Crc32 { input } => {
832
- let buf = read_all(&input)?;
833
- println!("crc32: {}", crate::core::crc32_bytes(&buf));
834
- }
835
- Commands::Adler32 { input } => {
836
- let buf = read_all(&input)?;
837
- println!("adler32: {}", crate::core::adler32_bytes(&buf));
838
- }
839
- }
840
- Ok(())
841
- }
842
-