roxify 1.12.10 → 1.13.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 (37) hide show
  1. package/Cargo.toml +98 -98
  2. package/dist/cli.js +48 -48
  3. package/dist/pack.js +1 -1
  4. package/dist/rox-macos-universal +0 -0
  5. package/dist/roxify_native +0 -0
  6. package/dist/roxify_native-macos-arm64 +0 -0
  7. package/dist/roxify_native-macos-x64 +0 -0
  8. package/dist/roxify_native.exe +0 -0
  9. package/dist/utils/decoder.js +59 -7
  10. package/dist/utils/encoder.js +12 -23
  11. package/dist/utils/optimization.js +2 -24
  12. package/dist/utils/zstd.js +1 -1
  13. package/native/bwt.rs +56 -56
  14. package/native/context_mixing.rs +117 -117
  15. package/native/core.rs +423 -382
  16. package/native/encoder.rs +635 -629
  17. package/native/gpu.rs +116 -116
  18. package/native/hybrid.rs +287 -287
  19. package/native/lib.rs +489 -489
  20. package/native/main.rs +527 -534
  21. package/native/mtf.rs +106 -106
  22. package/native/packer.rs +447 -447
  23. package/native/png_utils.rs +538 -538
  24. package/native/rans_byte.rs +286 -286
  25. package/native/streaming.rs +212 -212
  26. package/native/streaming_encode.rs +13 -5
  27. package/native/test_small_bwt.rs +31 -31
  28. package/package.json +114 -114
  29. package/roxify_native-aarch64-apple-darwin.node +0 -0
  30. package/roxify_native-aarch64-pc-windows-msvc.node +0 -0
  31. package/roxify_native-aarch64-unknown-linux-gnu.node +0 -0
  32. package/roxify_native-i686-pc-windows-msvc.node +0 -0
  33. package/roxify_native-i686-unknown-linux-gnu.node +0 -0
  34. package/roxify_native-x86_64-apple-darwin.node +0 -0
  35. package/roxify_native-x86_64-pc-windows-msvc.node +0 -0
  36. package/roxify_native-x86_64-unknown-linux-gnu.node +0 -0
  37. package/libroxify_native-x86_64-unknown-linux-gnu.node +0 -0
package/native/main.rs CHANGED
@@ -1,534 +1,527 @@
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 audio;
15
- mod reconstitution;
16
- mod archive;
17
- mod streaming;
18
- mod streaming_decode;
19
- mod streaming_encode;
20
-
21
- use crate::encoder::ImageFormat;
22
- use std::path::PathBuf;
23
-
24
- #[derive(Parser)]
25
- #[command(author, version)]
26
- struct Cli {
27
- #[command(subcommand)]
28
- command: Commands,
29
- }
30
-
31
- #[derive(Subcommand)]
32
- enum Commands {
33
- Encode {
34
- input: PathBuf,
35
- output: PathBuf,
36
- #[arg(short, long, default_value_t = 3)]
37
- level: i32,
38
- #[arg(short, long)]
39
- passphrase: Option<String>,
40
- #[arg(short, long, default_value = "aes")]
41
- encrypt: String,
42
- #[arg(short, long)]
43
- name: Option<String>,
44
- /// optional zstd dictionary file for payload compression
45
- #[arg(long, value_name = "FILE")]
46
- dict: Option<PathBuf>,
47
- },
48
-
49
- List {
50
- input: PathBuf,
51
- },
52
- Havepassphrase {
53
- input: PathBuf,
54
- },
55
- Scan {
56
- input: PathBuf,
57
- #[arg(short, long, value_name = "FILE")]
58
- #[arg(short, long, default_value_t = 4)]
59
- channels: usize,
60
- #[arg(short, long, value_delimiter = ',')]
61
- markers: Vec<String>,
62
- },
63
- DeltaEncode {
64
- input: PathBuf,
65
- output: Option<PathBuf>,
66
- },
67
- DeltaDecode {
68
- input: PathBuf,
69
- output: Option<PathBuf>,
70
- },
71
- Compress {
72
- input: PathBuf,
73
- output: Option<PathBuf>,
74
- #[arg(short, long, default_value_t = 19)]
75
- level: i32,
76
- /// optional zstd dictionary file to use for compression
77
- #[arg(long, value_name = "FILE")]
78
- dict: Option<PathBuf>,
79
- },
80
- TrainDict {
81
- /// sample files used to train the dictionary
82
- #[arg(short, long, value_name = "FILE", required = true)]
83
- samples: Vec<PathBuf>,
84
- /// desired dictionary size in bytes
85
- #[arg(short, long, default_value_t = 112640)]
86
- size: usize,
87
- /// output dictionary file
88
- output: PathBuf,
89
- },
90
- Decompress {
91
- input: PathBuf,
92
- output: Option<PathBuf>,
93
- #[arg(long)]
94
- files: Option<String>,
95
- #[arg(short, long)]
96
- passphrase: Option<String>,
97
- /// optional dictionary file used during decompression
98
- #[arg(long, value_name = "FILE")]
99
- dict: Option<PathBuf>,
100
- },
101
- Crc32 {
102
- input: PathBuf,
103
- },
104
- Adler32 {
105
- input: PathBuf,
106
- },
107
- }
108
-
109
- fn read_all(path: &PathBuf) -> anyhow::Result<Vec<u8>> {
110
- let metadata = std::fs::metadata(path)?;
111
- let size = metadata.len() as usize;
112
-
113
- if size > 256 * 1024 * 1024 {
114
- let file = File::open(path)?;
115
- let mmap = unsafe { memmap2::Mmap::map(&file)? };
116
- Ok(mmap.to_vec())
117
- } else {
118
- let mut f = File::open(path)?;
119
- let mut buf = Vec::with_capacity(size);
120
- f.read_to_end(&mut buf)?;
121
- Ok(buf)
122
- }
123
- }
124
-
125
- fn write_all(path: &PathBuf, data: &[u8]) -> anyhow::Result<()> {
126
- let f = File::create(path)?;
127
- let buf_size = if data.len() > 64 * 1024 * 1024 { 16 * 1024 * 1024 }
128
- else { (8 * 1024 * 1024).min(data.len().max(8192)) };
129
- let mut writer = std::io::BufWriter::with_capacity(buf_size, f);
130
- writer.write_all(data)?;
131
- writer.flush()?;
132
- Ok(())
133
- }
134
-
135
- fn parse_markers(v: &[String]) -> Option<Vec<u8>> {
136
- if v.is_empty() {
137
- return None;
138
- }
139
- let mut out = Vec::new();
140
- for s in v {
141
- let parts: Vec<&str> = s.split(|c| c == ':' || c == ',' ).collect();
142
- if parts.len() >= 3 {
143
- if let (Ok(r), Ok(g), Ok(b)) = (parts[0].parse::<u8>(), parts[1].parse::<u8>(), parts[2].parse::<u8>()) {
144
- out.push(r); out.push(g); out.push(b);
145
- }
146
- }
147
- }
148
- if out.is_empty() { None } else { Some(out) }
149
- }
150
-
151
- fn main() -> anyhow::Result<()> {
152
- let cli = Cli::parse();
153
- match cli.command {
154
- Commands::TrainDict { samples, size, output } => {
155
- let dict = core::train_zstd_dictionary(&samples, size)?;
156
- write_all(&output, &dict)?;
157
- println!("wrote {} bytes dictionary to {:?}", dict.len(), output);
158
- return Ok(());
159
- }
160
- Commands::Encode { input, output, level, passphrase, encrypt, name, dict } => {
161
- let is_dir = input.is_dir();
162
-
163
- let file_name = name.as_deref()
164
- .or_else(|| input.file_name().and_then(|n| n.to_str()));
165
-
166
- if is_dir && dict.is_none() {
167
- streaming_encode::encode_dir_to_png_encrypted(
168
- &input,
169
- &output,
170
- level,
171
- file_name,
172
- passphrase.as_deref(),
173
- Some(&encrypt),
174
- )?;
175
- println!("(TAR archive, rXFL chunk embedded)");
176
- return Ok(());
177
- }
178
-
179
- let (payload, file_list_json) = if is_dir {
180
- let result = archive::tar_pack_directory_with_list(&input)
181
- .map_err(|e| anyhow::anyhow!(e))?;
182
- let json_list: Vec<serde_json::Value> = result.file_list.iter()
183
- .map(|(name, size)| serde_json::json!({"name": name, "size": size}))
184
- .collect();
185
- (result.data, Some(serde_json::to_string(&json_list)?))
186
- } else {
187
- let pack_result = packer::pack_path_with_metadata(&input)?;
188
- (pack_result.data, pack_result.file_list_json)
189
- };
190
-
191
- let dict_bytes: Option<Vec<u8>> = match dict {
192
- Some(path) => Some(read_all(&path)?),
193
- None => None,
194
- };
195
-
196
- let use_streaming = payload.len() > 64 * 1024 * 1024;
197
-
198
- if use_streaming {
199
- streaming::encode_to_png_file(
200
- &payload,
201
- &output,
202
- level,
203
- passphrase.as_deref(),
204
- Some(&encrypt),
205
- file_name,
206
- file_list_json.as_deref(),
207
- dict_bytes.as_deref(),
208
- )?;
209
- } else {
210
- let png = if let Some(ref pass) = passphrase {
211
- encoder::encode_to_png_with_encryption_name_and_format_and_filelist(
212
- &payload,
213
- level,
214
- Some(pass),
215
- Some(&encrypt),
216
- ImageFormat::Png,
217
- file_name,
218
- file_list_json.as_deref(),
219
- dict_bytes.as_deref(),
220
- )?
221
- } else {
222
- encoder::encode_to_png_with_encryption_name_and_format_and_filelist(
223
- &payload,
224
- level,
225
- None,
226
- None,
227
- ImageFormat::Png,
228
- file_name,
229
- file_list_json.as_deref(),
230
- dict_bytes.as_deref(),
231
- )?
232
- };
233
- write_all(&output, &png)?;
234
- }
235
-
236
- if file_list_json.is_some() {
237
- if is_dir {
238
- println!("(TAR archive, rXFL chunk embedded)");
239
- } else {
240
- println!("(rXFL chunk embedded)");
241
- }
242
- }
243
- }
244
- Commands::List { input } => {
245
- let mut file = File::open(&input)?;
246
- let chunks = png_utils::extract_png_chunks_streaming(&mut file).map_err(|e| anyhow::anyhow!(e))?;
247
-
248
- if let Some(rxfl_chunk) = chunks.iter().find(|c| c.name == "rXFL") {
249
- println!("{}", String::from_utf8_lossy(&rxfl_chunk.data));
250
- return Ok(());
251
- }
252
-
253
- if let Some(meta_chunk) = chunks.iter().find(|c| c.name == "rOXm") {
254
- if let Some(pos) = meta_chunk.data.windows(4).position(|w| w == b"rXFL") {
255
- if pos + 8 <= meta_chunk.data.len() {
256
- let json_len = u32::from_be_bytes([
257
- meta_chunk.data[pos + 4],
258
- meta_chunk.data[pos + 5],
259
- meta_chunk.data[pos + 6],
260
- meta_chunk.data[pos + 7],
261
- ]) as usize;
262
-
263
- let json_start = pos + 8;
264
- let json_end = json_start + json_len;
265
-
266
- if json_end <= meta_chunk.data.len() {
267
- println!("{}", String::from_utf8_lossy(&meta_chunk.data[json_start..json_end]));
268
- return Ok(());
269
- }
270
- }
271
- }
272
- }
273
-
274
- let png_data = std::fs::read(&input)?;
275
- match png_utils::extract_file_list_from_pixels(&png_data) {
276
- Ok(json) => {
277
- println!("{}", json);
278
- return Ok(());
279
- }
280
- Err(_) => {}
281
- }
282
-
283
- eprintln!("No file list found in PNG");
284
- std::process::exit(1);
285
- }
286
- Commands::Havepassphrase { input } => {
287
- let buf = read_all(&input)?;
288
- let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
289
- if is_png {
290
- let payload = png_utils::extract_payload_from_png(&buf).map_err(|e| anyhow::anyhow!(e))?;
291
- if !payload.is_empty() && (payload[0] == 0x01 || payload[0] == 0x02 || payload[0] == 0x03) {
292
- println!("Passphrase detected.");
293
- } else {
294
- println!("No passphrase detected.");
295
- }
296
- } else {
297
- if !buf.is_empty() && (buf[0] == 0x01 || buf[0] == 0x02 || buf[0] == 0x03) {
298
- println!("Passphrase detected.");
299
- } else {
300
- println!("No passphrase detected.");
301
- }
302
- }
303
- }
304
- Commands::Scan { input, channels, markers } => {
305
- let buf = read_all(&input)?;
306
- let marker_bytes = parse_markers(&markers);
307
- let res = crate::core::scan_pixels_bytes(&buf, channels, marker_bytes.as_deref());
308
- println!("magic_positions: {:?}", res.magic_positions);
309
- println!("marker_positions: {:?}", res.marker_positions);
310
- }
311
- Commands::DeltaEncode { input, output } => {
312
- let buf = read_all(&input)?;
313
- let out = crate::core::delta_encode_bytes(&buf);
314
- let dest = output.unwrap_or_else(|| PathBuf::from("delta.bin"));
315
- write_all(&dest, &out)?;
316
- }
317
- Commands::DeltaDecode { input, output } => {
318
- let buf = read_all(&input)?;
319
- let out = crate::core::delta_decode_bytes(&buf);
320
- let dest = output.unwrap_or_else(|| PathBuf::from("raw.bin"));
321
- write_all(&dest, &out)?;
322
- }
323
- Commands::Compress { input, output, level, dict } => {
324
- let buf = read_all(&input)?;
325
- let dict_bytes: Option<Vec<u8>> = match dict {
326
- Some(path) => Some(read_all(&path)?),
327
- None => None,
328
- };
329
- let out = crate::core::zstd_compress_bytes(
330
- &buf,
331
- level,
332
- dict_bytes.as_deref(),
333
- )
334
- .map_err(|e: String| anyhow::anyhow!(e))?;
335
- let dest = output.unwrap_or_else(|| PathBuf::from("out.zst"));
336
- write_all(&dest, &out)?;
337
- }
338
- Commands::Decompress { input, output, files, passphrase, dict } => {
339
- let file_size = std::fs::metadata(&input).map(|m| m.len()).unwrap_or(0);
340
- let is_png_file = input.extension().map(|e| e == "png").unwrap_or(false)
341
- || (file_size >= 8 && {
342
- let mut sig = [0u8; 8];
343
- std::fs::File::open(&input).and_then(|mut f| { use std::io::Read; f.read_exact(&mut sig) }).is_ok()
344
- && sig == [137, 80, 78, 71, 13, 10, 26, 10]
345
- });
346
-
347
- if is_png_file && files.is_none() && dict.is_none() && file_size > 100_000_000 {
348
- let out_dir = output.clone().unwrap_or_else(|| PathBuf::from("out.raw"));
349
- match streaming_decode::streaming_decode_to_dir_encrypted(&input, &out_dir, passphrase.as_deref()) {
350
- Ok(written) => {
351
- println!("Unpacked {} files (TAR)", written.len());
352
- return Ok(());
353
- }
354
- Err(e) => {
355
- eprintln!("Streaming decode failed ({}), falling back to in-memory", e);
356
- }
357
- }
358
- }
359
-
360
- let buf = read_all(&input)?;
361
- let dict_bytes: Option<Vec<u8>> = match dict {
362
- Some(path) => Some(read_all(&path)?),
363
- None => None,
364
- };
365
- if let Some(files_str) = files {
366
- let file_list: Option<Vec<String>> = if files_str.trim_start().starts_with('[') {
367
- match serde_json::from_str::<Vec<String>>(&files_str) {
368
- Ok(v) => Some(v),
369
- Err(e) => {
370
- eprintln!("Invalid JSON for --files: {}", e);
371
- std::process::exit(1);
372
- }
373
- }
374
- } else {
375
- let list = files_str.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect::<Vec<_>>();
376
- Some(list)
377
- };
378
-
379
- let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
380
-
381
- use std::io::Cursor;
382
- let normalized: Vec<u8> = if is_png {
383
- let payload = png_utils::extract_payload_from_png(&buf).map_err(|e| anyhow::anyhow!(e))?;
384
- if payload.is_empty() { return Err(anyhow::anyhow!("Empty payload")); }
385
- if payload[0] == 0x00u8 {
386
- payload[1..].to_vec()
387
- } else {
388
- let pass = passphrase.as_ref().map(|s: &String| s.as_str());
389
- match crate::crypto::try_decrypt(&payload, pass) {
390
- Ok(v) => v,
391
- Err(e) => return Err(anyhow::anyhow!("Encrypted payload: {}", e)),
392
- }
393
- }
394
- } else {
395
- if buf[0] == 0x00u8 {
396
- buf[1..].to_vec()
397
- } else if buf.starts_with(b"ROX1") {
398
- buf[4..].to_vec()
399
- } else if buf[0] == 0x01u8 || buf[0] == 0x02u8 || buf[0] == 0x03u8 {
400
- let pass = passphrase.as_ref().map(|s: &String| s.as_str());
401
- match crate::crypto::try_decrypt(&buf, pass) {
402
- Ok(v) => v,
403
- Err(e) => return Err(anyhow::anyhow!("Encrypted payload: {}", e)),
404
- }
405
- } else {
406
- buf.to_vec()
407
- }
408
- };
409
-
410
- let mut reader: Box<dyn std::io::Read> = if normalized.starts_with(b"ROX1") {
411
- Box::new(Cursor::new(normalized[4..].to_vec()))
412
- } else {
413
- let mut dec = zstd::stream::Decoder::new(Cursor::new(normalized)).map_err(|e| anyhow::anyhow!("zstd decoder init: {}", e))?;
414
- dec.window_log_max(31).map_err(|e| anyhow::anyhow!("zstd window_log_max: {}", e))?;
415
- Box::new(dec)
416
- };
417
-
418
- let out_dir = output.unwrap_or_else(|| PathBuf::from("."));
419
- std::fs::create_dir_all(&out_dir).map_err(|e| anyhow::anyhow!("Cannot create output directory {:?}: {}", out_dir, e))?;
420
- let files_slice = file_list.as_ref().map(|v| v.as_slice());
421
-
422
- let written = packer::unpack_stream_to_dir(&mut reader, &out_dir, files_slice).map_err(|e| anyhow::anyhow!(e))?;
423
- println!("Unpacked {} files", written.len());
424
- } else {
425
- let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
426
- let out_bytes = if is_png {
427
- let payload = png_utils::extract_payload_from_png(&buf).map_err(|e| anyhow::anyhow!(e))?;
428
- if payload.is_empty() { return Err(anyhow::anyhow!("Empty payload")); }
429
- let first = payload[0];
430
- if first == 0x00u8 {
431
- let compressed = payload[1..].to_vec();
432
- match crate::core::zstd_decompress_bytes(&compressed, dict_bytes.as_deref()) {
433
- Ok(mut o) => {
434
- if o.starts_with(b"ROX1") { o = o[4..].to_vec(); }
435
- o
436
- }
437
- Err(e) => {
438
- if compressed.starts_with(b"ROX1") {
439
- eprintln!("⚠️ zstd decompress failed ({}), but payload starts with ROX1: falling back to raw pack", e);
440
- compressed[4..].to_vec()
441
- } else {
442
- return Err(anyhow::anyhow!("zstd decompress error: {}", e));
443
- }
444
- }
445
- }
446
- } else {
447
- let pass = passphrase.as_ref().map(|s| s.as_str());
448
- match crate::crypto::try_decrypt(&payload, pass) {
449
- Ok(v) => {
450
- let inner = if v.starts_with(b"ROX1") {
451
- v[4..].to_vec()
452
- } else {
453
- v
454
- };
455
- match crate::core::zstd_decompress_bytes(&inner, dict_bytes.as_deref()) {
456
- Ok(mut o) => {
457
- if o.starts_with(b"ROX1") { o = o[4..].to_vec(); }
458
- o
459
- }
460
- Err(_) => inner,
461
- }
462
- }
463
- Err(e) => return Err(anyhow::anyhow!("Encrypted payload: {}", e)),
464
- }
465
- }
466
- } else {
467
- match crate::core::zstd_decompress_bytes(&buf, dict_bytes.as_deref()) {
468
- Ok(mut x) => { if x.starts_with(b"ROX1") { x = x[4..].to_vec(); } x },
469
- Err(e) => {
470
- if buf.starts_with(b"ROX1") {
471
- eprintln!("⚠️ zstd decompress failed ({}), but input already starts with ROX1: using raw pack", e);
472
- buf[4..].to_vec()
473
- } else {
474
- return Err(anyhow::anyhow!("zstd decompress error: {}", e));
475
- }
476
- }
477
- }
478
- };
479
-
480
- let dest = output.unwrap_or_else(|| PathBuf::from("out.raw"));
481
-
482
- if archive::is_tar(&out_bytes) {
483
- let out_dir = if dest.extension().is_none() || dest.is_dir() {
484
- dest
485
- } else {
486
- PathBuf::from(".")
487
- };
488
- std::fs::create_dir_all(&out_dir)
489
- .map_err(|e| anyhow::anyhow!("mkdir {:?}: {}", out_dir, e))?;
490
- let written = archive::tar_unpack(&out_bytes, &out_dir)
491
- .map_err(|e| anyhow::anyhow!(e))?;
492
- println!("Unpacked {} files (TAR) to {:?}", written.len(), out_dir);
493
- } else if out_bytes.len() >= 4
494
- && (u32::from_be_bytes(out_bytes[0..4].try_into().unwrap()) == 0x524f5850u32
495
- || u32::from_be_bytes(out_bytes[0..4].try_into().unwrap()) == 0x524f5849u32)
496
- {
497
- let out_dir = if dest.extension().is_none() || dest.is_dir() {
498
- dest
499
- } else {
500
- PathBuf::from(".")
501
- };
502
- std::fs::create_dir_all(&out_dir)
503
- .map_err(|e| anyhow::anyhow!("mkdir {:?}: {}", out_dir, e))?;
504
- let written = packer::unpack_buffer_to_dir(&out_bytes, &out_dir, None)
505
- .map_err(|e| anyhow::anyhow!(e))?;
506
- println!("Unpacked {} files to {:?}", written.len(), out_dir);
507
- } else if dest.is_dir() {
508
- let fname = if is_png {
509
- png_utils::extract_name_from_png(&buf)
510
- } else {
511
- None
512
- }.unwrap_or_else(|| {
513
- input.file_stem()
514
- .map(|s| s.to_string_lossy().to_string())
515
- .unwrap_or_else(|| "out.raw".to_string())
516
- });
517
- write_all(&dest.join(&fname), &out_bytes)?;
518
- } else {
519
- write_all(&dest, &out_bytes)?;
520
- }
521
- }
522
- }
523
- Commands::Crc32 { input } => {
524
- let buf = read_all(&input)?;
525
- println!("crc32: {}", crate::core::crc32_bytes(&buf));
526
- }
527
- Commands::Adler32 { input } => {
528
- let buf = read_all(&input)?;
529
- println!("adler32: {}", crate::core::adler32_bytes(&buf));
530
- }
531
- }
532
- Ok(())
533
- }
534
-
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 audio;
15
+ mod reconstitution;
16
+ mod archive;
17
+ mod streaming;
18
+ mod streaming_decode;
19
+ mod streaming_encode;
20
+
21
+ use crate::encoder::ImageFormat;
22
+ use std::path::PathBuf;
23
+
24
+ #[derive(Parser)]
25
+ #[command(author, version)]
26
+ struct Cli {
27
+ #[command(subcommand)]
28
+ command: Commands,
29
+ }
30
+
31
+ #[derive(Subcommand)]
32
+ enum Commands {
33
+ Encode {
34
+ input: PathBuf,
35
+ output: PathBuf,
36
+ #[arg(short, long, default_value_t = 3)]
37
+ level: i32,
38
+ #[arg(short, long)]
39
+ passphrase: Option<String>,
40
+ #[arg(short, long, default_value = "aes")]
41
+ encrypt: String,
42
+ #[arg(short, long)]
43
+ name: Option<String>,
44
+ /// optional zstd dictionary file for payload compression
45
+ #[arg(long, value_name = "FILE")]
46
+ dict: Option<PathBuf>,
47
+ },
48
+
49
+ List {
50
+ input: PathBuf,
51
+ },
52
+ Havepassphrase {
53
+ input: PathBuf,
54
+ },
55
+ Scan {
56
+ input: PathBuf,
57
+ #[arg(short, long, value_name = "FILE")]
58
+ #[arg(short, long, default_value_t = 4)]
59
+ channels: usize,
60
+ #[arg(short, long, value_delimiter = ',')]
61
+ markers: Vec<String>,
62
+ },
63
+ DeltaEncode {
64
+ input: PathBuf,
65
+ output: Option<PathBuf>,
66
+ },
67
+ DeltaDecode {
68
+ input: PathBuf,
69
+ output: Option<PathBuf>,
70
+ },
71
+ Compress {
72
+ input: PathBuf,
73
+ output: Option<PathBuf>,
74
+ #[arg(short, long, default_value_t = 19)]
75
+ level: i32,
76
+ /// optional zstd dictionary file to use for compression
77
+ #[arg(long, value_name = "FILE")]
78
+ dict: Option<PathBuf>,
79
+ },
80
+ TrainDict {
81
+ /// sample files used to train the dictionary
82
+ #[arg(short, long, value_name = "FILE", required = true)]
83
+ samples: Vec<PathBuf>,
84
+ /// desired dictionary size in bytes
85
+ #[arg(short, long, default_value_t = 112640)]
86
+ size: usize,
87
+ /// output dictionary file
88
+ output: PathBuf,
89
+ },
90
+ Decompress {
91
+ input: PathBuf,
92
+ output: Option<PathBuf>,
93
+ #[arg(long)]
94
+ files: Option<String>,
95
+ #[arg(short, long)]
96
+ passphrase: Option<String>,
97
+ /// optional dictionary file used during decompression
98
+ #[arg(long, value_name = "FILE")]
99
+ dict: Option<PathBuf>,
100
+ },
101
+ Crc32 {
102
+ input: PathBuf,
103
+ },
104
+ Adler32 {
105
+ input: PathBuf,
106
+ },
107
+ }
108
+
109
+ fn read_all(path: &PathBuf) -> anyhow::Result<Vec<u8>> {
110
+ let metadata = std::fs::metadata(path)?;
111
+ let size = metadata.len() as usize;
112
+ let mut f = File::open(path)?;
113
+ let mut buf = Vec::with_capacity(size);
114
+ f.read_to_end(&mut buf)?;
115
+ Ok(buf)
116
+ }
117
+
118
+ fn write_all(path: &PathBuf, data: &[u8]) -> anyhow::Result<()> {
119
+ let f = File::create(path)?;
120
+ let buf_size = if data.len() > 64 * 1024 * 1024 { 16 * 1024 * 1024 }
121
+ else { (8 * 1024 * 1024).min(data.len().max(8192)) };
122
+ let mut writer = std::io::BufWriter::with_capacity(buf_size, f);
123
+ writer.write_all(data)?;
124
+ writer.flush()?;
125
+ Ok(())
126
+ }
127
+
128
+ fn parse_markers(v: &[String]) -> Option<Vec<u8>> {
129
+ if v.is_empty() {
130
+ return None;
131
+ }
132
+ let mut out = Vec::new();
133
+ for s in v {
134
+ let parts: Vec<&str> = s.split(|c| c == ':' || c == ',' ).collect();
135
+ if parts.len() >= 3 {
136
+ if let (Ok(r), Ok(g), Ok(b)) = (parts[0].parse::<u8>(), parts[1].parse::<u8>(), parts[2].parse::<u8>()) {
137
+ out.push(r); out.push(g); out.push(b);
138
+ }
139
+ }
140
+ }
141
+ if out.is_empty() { None } else { Some(out) }
142
+ }
143
+
144
+ fn main() -> anyhow::Result<()> {
145
+ let cli = Cli::parse();
146
+ match cli.command {
147
+ Commands::TrainDict { samples, size, output } => {
148
+ let dict = core::train_zstd_dictionary(&samples, size)?;
149
+ write_all(&output, &dict)?;
150
+ println!("wrote {} bytes dictionary to {:?}", dict.len(), output);
151
+ return Ok(());
152
+ }
153
+ Commands::Encode { input, output, level, passphrase, encrypt, name, dict } => {
154
+ let is_dir = input.is_dir();
155
+
156
+ let file_name = name.as_deref()
157
+ .or_else(|| input.file_name().and_then(|n| n.to_str()));
158
+
159
+ if is_dir && dict.is_none() {
160
+ streaming_encode::encode_dir_to_png_encrypted(
161
+ &input,
162
+ &output,
163
+ level,
164
+ file_name,
165
+ passphrase.as_deref(),
166
+ Some(&encrypt),
167
+ )?;
168
+ println!("(TAR archive, rXFL chunk embedded)");
169
+ return Ok(());
170
+ }
171
+
172
+ let (payload, file_list_json) = if is_dir {
173
+ let result = archive::tar_pack_directory_with_list(&input)
174
+ .map_err(|e| anyhow::anyhow!(e))?;
175
+ let json_list: Vec<serde_json::Value> = result.file_list.iter()
176
+ .map(|(name, size)| serde_json::json!({"name": name, "size": size}))
177
+ .collect();
178
+ (result.data, Some(serde_json::to_string(&json_list)?))
179
+ } else {
180
+ let pack_result = packer::pack_path_with_metadata(&input)?;
181
+ (pack_result.data, pack_result.file_list_json)
182
+ };
183
+
184
+ let dict_bytes: Option<Vec<u8>> = match dict {
185
+ Some(path) => Some(read_all(&path)?),
186
+ None => None,
187
+ };
188
+
189
+ let use_streaming = payload.len() > 64 * 1024 * 1024;
190
+
191
+ if use_streaming {
192
+ streaming::encode_to_png_file(
193
+ &payload,
194
+ &output,
195
+ level,
196
+ passphrase.as_deref(),
197
+ Some(&encrypt),
198
+ file_name,
199
+ file_list_json.as_deref(),
200
+ dict_bytes.as_deref(),
201
+ )?;
202
+ } else {
203
+ let png = if let Some(ref pass) = passphrase {
204
+ encoder::encode_to_png_with_encryption_name_and_format_and_filelist(
205
+ &payload,
206
+ level,
207
+ Some(pass),
208
+ Some(&encrypt),
209
+ ImageFormat::Png,
210
+ file_name,
211
+ file_list_json.as_deref(),
212
+ dict_bytes.as_deref(),
213
+ )?
214
+ } else {
215
+ encoder::encode_to_png_with_encryption_name_and_format_and_filelist(
216
+ &payload,
217
+ level,
218
+ None,
219
+ None,
220
+ ImageFormat::Png,
221
+ file_name,
222
+ file_list_json.as_deref(),
223
+ dict_bytes.as_deref(),
224
+ )?
225
+ };
226
+ write_all(&output, &png)?;
227
+ }
228
+
229
+ if file_list_json.is_some() {
230
+ if is_dir {
231
+ println!("(TAR archive, rXFL chunk embedded)");
232
+ } else {
233
+ println!("(rXFL chunk embedded)");
234
+ }
235
+ }
236
+ }
237
+ Commands::List { input } => {
238
+ let mut file = File::open(&input)?;
239
+ let chunks = png_utils::extract_png_chunks_streaming(&mut file).map_err(|e| anyhow::anyhow!(e))?;
240
+
241
+ if let Some(rxfl_chunk) = chunks.iter().find(|c| c.name == "rXFL") {
242
+ println!("{}", String::from_utf8_lossy(&rxfl_chunk.data));
243
+ return Ok(());
244
+ }
245
+
246
+ if let Some(meta_chunk) = chunks.iter().find(|c| c.name == "rOXm") {
247
+ if let Some(pos) = meta_chunk.data.windows(4).position(|w| w == b"rXFL") {
248
+ if pos + 8 <= meta_chunk.data.len() {
249
+ let json_len = u32::from_be_bytes([
250
+ meta_chunk.data[pos + 4],
251
+ meta_chunk.data[pos + 5],
252
+ meta_chunk.data[pos + 6],
253
+ meta_chunk.data[pos + 7],
254
+ ]) as usize;
255
+
256
+ let json_start = pos + 8;
257
+ let json_end = json_start + json_len;
258
+
259
+ if json_end <= meta_chunk.data.len() {
260
+ println!("{}", String::from_utf8_lossy(&meta_chunk.data[json_start..json_end]));
261
+ return Ok(());
262
+ }
263
+ }
264
+ }
265
+ }
266
+
267
+ let png_data = std::fs::read(&input)?;
268
+ match png_utils::extract_file_list_from_pixels(&png_data) {
269
+ Ok(json) => {
270
+ println!("{}", json);
271
+ return Ok(());
272
+ }
273
+ Err(_) => {}
274
+ }
275
+
276
+ eprintln!("No file list found in PNG");
277
+ std::process::exit(1);
278
+ }
279
+ Commands::Havepassphrase { input } => {
280
+ let buf = read_all(&input)?;
281
+ let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
282
+ if is_png {
283
+ let payload = png_utils::extract_payload_from_png(&buf).map_err(|e| anyhow::anyhow!(e))?;
284
+ if !payload.is_empty() && (payload[0] == 0x01 || payload[0] == 0x02 || payload[0] == 0x03) {
285
+ println!("Passphrase detected.");
286
+ } else {
287
+ println!("No passphrase detected.");
288
+ }
289
+ } else {
290
+ if !buf.is_empty() && (buf[0] == 0x01 || buf[0] == 0x02 || buf[0] == 0x03) {
291
+ println!("Passphrase detected.");
292
+ } else {
293
+ println!("No passphrase detected.");
294
+ }
295
+ }
296
+ }
297
+ Commands::Scan { input, channels, markers } => {
298
+ let buf = read_all(&input)?;
299
+ let marker_bytes = parse_markers(&markers);
300
+ let res = crate::core::scan_pixels_bytes(&buf, channels, marker_bytes.as_deref());
301
+ println!("magic_positions: {:?}", res.magic_positions);
302
+ println!("marker_positions: {:?}", res.marker_positions);
303
+ }
304
+ Commands::DeltaEncode { input, output } => {
305
+ let buf = read_all(&input)?;
306
+ let out = crate::core::delta_encode_bytes(&buf);
307
+ let dest = output.unwrap_or_else(|| PathBuf::from("delta.bin"));
308
+ write_all(&dest, &out)?;
309
+ }
310
+ Commands::DeltaDecode { input, output } => {
311
+ let buf = read_all(&input)?;
312
+ let out = crate::core::delta_decode_bytes(&buf);
313
+ let dest = output.unwrap_or_else(|| PathBuf::from("raw.bin"));
314
+ write_all(&dest, &out)?;
315
+ }
316
+ Commands::Compress { input, output, level, dict } => {
317
+ let buf = read_all(&input)?;
318
+ let dict_bytes: Option<Vec<u8>> = match dict {
319
+ Some(path) => Some(read_all(&path)?),
320
+ None => None,
321
+ };
322
+ let out = crate::core::zstd_compress_bytes(
323
+ &buf,
324
+ level,
325
+ dict_bytes.as_deref(),
326
+ )
327
+ .map_err(|e: String| anyhow::anyhow!(e))?;
328
+ let dest = output.unwrap_or_else(|| PathBuf::from("out.zst"));
329
+ write_all(&dest, &out)?;
330
+ }
331
+ Commands::Decompress { input, output, files, passphrase, dict } => {
332
+ let file_size = std::fs::metadata(&input).map(|m| m.len()).unwrap_or(0);
333
+ let is_png_file = input.extension().map(|e| e == "png").unwrap_or(false)
334
+ || (file_size >= 8 && {
335
+ let mut sig = [0u8; 8];
336
+ std::fs::File::open(&input).and_then(|mut f| { use std::io::Read; f.read_exact(&mut sig) }).is_ok()
337
+ && sig == [137, 80, 78, 71, 13, 10, 26, 10]
338
+ });
339
+
340
+ if is_png_file && files.is_none() && dict.is_none() && file_size > 100_000_000 {
341
+ let out_dir = output.clone().unwrap_or_else(|| PathBuf::from("out.raw"));
342
+ match streaming_decode::streaming_decode_to_dir_encrypted(&input, &out_dir, passphrase.as_deref()) {
343
+ Ok(written) => {
344
+ println!("Unpacked {} files (TAR)", written.len());
345
+ return Ok(());
346
+ }
347
+ Err(e) => {
348
+ eprintln!("Streaming decode failed ({}), falling back to in-memory", e);
349
+ }
350
+ }
351
+ }
352
+
353
+ let buf = read_all(&input)?;
354
+ let dict_bytes: Option<Vec<u8>> = match dict {
355
+ Some(path) => Some(read_all(&path)?),
356
+ None => None,
357
+ };
358
+ if let Some(files_str) = files {
359
+ let file_list: Option<Vec<String>> = if files_str.trim_start().starts_with('[') {
360
+ match serde_json::from_str::<Vec<String>>(&files_str) {
361
+ Ok(v) => Some(v),
362
+ Err(e) => {
363
+ eprintln!("Invalid JSON for --files: {}", e);
364
+ std::process::exit(1);
365
+ }
366
+ }
367
+ } else {
368
+ let list = files_str.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect::<Vec<_>>();
369
+ Some(list)
370
+ };
371
+
372
+ let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
373
+
374
+ use std::io::Cursor;
375
+ let normalized: Vec<u8> = if is_png {
376
+ let payload = png_utils::extract_payload_from_png(&buf).map_err(|e| anyhow::anyhow!(e))?;
377
+ if payload.is_empty() { return Err(anyhow::anyhow!("Empty payload")); }
378
+ if payload[0] == 0x00u8 {
379
+ payload[1..].to_vec()
380
+ } else {
381
+ let pass = passphrase.as_ref().map(|s: &String| s.as_str());
382
+ match crate::crypto::try_decrypt(&payload, pass) {
383
+ Ok(v) => v,
384
+ Err(e) => return Err(anyhow::anyhow!("Encrypted payload: {}", e)),
385
+ }
386
+ }
387
+ } else {
388
+ if buf[0] == 0x00u8 {
389
+ buf[1..].to_vec()
390
+ } else if buf.starts_with(b"ROX1") {
391
+ buf[4..].to_vec()
392
+ } else if buf[0] == 0x01u8 || buf[0] == 0x02u8 || buf[0] == 0x03u8 {
393
+ let pass = passphrase.as_ref().map(|s: &String| s.as_str());
394
+ match crate::crypto::try_decrypt(&buf, pass) {
395
+ Ok(v) => v,
396
+ Err(e) => return Err(anyhow::anyhow!("Encrypted payload: {}", e)),
397
+ }
398
+ } else {
399
+ buf.to_vec()
400
+ }
401
+ };
402
+
403
+ let mut reader: Box<dyn std::io::Read> = if normalized.starts_with(b"ROX1") {
404
+ Box::new(Cursor::new(normalized[4..].to_vec()))
405
+ } else {
406
+ let mut dec = zstd::stream::Decoder::new(Cursor::new(normalized)).map_err(|e| anyhow::anyhow!("zstd decoder init: {}", e))?;
407
+ dec.window_log_max(31).map_err(|e| anyhow::anyhow!("zstd window_log_max: {}", e))?;
408
+ Box::new(dec)
409
+ };
410
+
411
+ let out_dir = output.unwrap_or_else(|| PathBuf::from("."));
412
+ std::fs::create_dir_all(&out_dir).map_err(|e| anyhow::anyhow!("Cannot create output directory {:?}: {}", out_dir, e))?;
413
+ let files_slice = file_list.as_ref().map(|v| v.as_slice());
414
+
415
+ let written = packer::unpack_stream_to_dir(&mut reader, &out_dir, files_slice).map_err(|e| anyhow::anyhow!(e))?;
416
+ println!("Unpacked {} files", written.len());
417
+ } else {
418
+ let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
419
+ let out_bytes = if is_png {
420
+ let payload = png_utils::extract_payload_from_png(&buf).map_err(|e| anyhow::anyhow!(e))?;
421
+ if payload.is_empty() { return Err(anyhow::anyhow!("Empty payload")); }
422
+ let first = payload[0];
423
+ if first == 0x00u8 {
424
+ let compressed = payload[1..].to_vec();
425
+ match crate::core::zstd_decompress_bytes(&compressed, dict_bytes.as_deref()) {
426
+ Ok(mut o) => {
427
+ if o.starts_with(b"ROX1") { o = o[4..].to_vec(); }
428
+ o
429
+ }
430
+ Err(e) => {
431
+ if compressed.starts_with(b"ROX1") {
432
+ eprintln!("⚠️ zstd decompress failed ({}), but payload starts with ROX1: falling back to raw pack", e);
433
+ compressed[4..].to_vec()
434
+ } else {
435
+ return Err(anyhow::anyhow!("zstd decompress error: {}", e));
436
+ }
437
+ }
438
+ }
439
+ } else {
440
+ let pass = passphrase.as_ref().map(|s| s.as_str());
441
+ match crate::crypto::try_decrypt(&payload, pass) {
442
+ Ok(v) => {
443
+ let inner = if v.starts_with(b"ROX1") {
444
+ v[4..].to_vec()
445
+ } else {
446
+ v
447
+ };
448
+ match crate::core::zstd_decompress_bytes(&inner, dict_bytes.as_deref()) {
449
+ Ok(mut o) => {
450
+ if o.starts_with(b"ROX1") { o = o[4..].to_vec(); }
451
+ o
452
+ }
453
+ Err(_) => inner,
454
+ }
455
+ }
456
+ Err(e) => return Err(anyhow::anyhow!("Encrypted payload: {}", e)),
457
+ }
458
+ }
459
+ } else {
460
+ match crate::core::zstd_decompress_bytes(&buf, dict_bytes.as_deref()) {
461
+ Ok(mut x) => { if x.starts_with(b"ROX1") { x = x[4..].to_vec(); } x },
462
+ Err(e) => {
463
+ if buf.starts_with(b"ROX1") {
464
+ eprintln!("⚠️ zstd decompress failed ({}), but input already starts with ROX1: using raw pack", e);
465
+ buf[4..].to_vec()
466
+ } else {
467
+ return Err(anyhow::anyhow!("zstd decompress error: {}", e));
468
+ }
469
+ }
470
+ }
471
+ };
472
+
473
+ let dest = output.unwrap_or_else(|| PathBuf::from("out.raw"));
474
+
475
+ if archive::is_tar(&out_bytes) {
476
+ let out_dir = if dest.extension().is_none() || dest.is_dir() {
477
+ dest
478
+ } else {
479
+ PathBuf::from(".")
480
+ };
481
+ std::fs::create_dir_all(&out_dir)
482
+ .map_err(|e| anyhow::anyhow!("mkdir {:?}: {}", out_dir, e))?;
483
+ let written = archive::tar_unpack(&out_bytes, &out_dir)
484
+ .map_err(|e| anyhow::anyhow!(e))?;
485
+ println!("Unpacked {} files (TAR) to {:?}", written.len(), out_dir);
486
+ } else if out_bytes.len() >= 4
487
+ && (u32::from_be_bytes(out_bytes[0..4].try_into().unwrap()) == 0x524f5850u32
488
+ || u32::from_be_bytes(out_bytes[0..4].try_into().unwrap()) == 0x524f5849u32)
489
+ {
490
+ let out_dir = if dest.extension().is_none() || dest.is_dir() {
491
+ dest
492
+ } else {
493
+ PathBuf::from(".")
494
+ };
495
+ std::fs::create_dir_all(&out_dir)
496
+ .map_err(|e| anyhow::anyhow!("mkdir {:?}: {}", out_dir, e))?;
497
+ let written = packer::unpack_buffer_to_dir(&out_bytes, &out_dir, None)
498
+ .map_err(|e| anyhow::anyhow!(e))?;
499
+ println!("Unpacked {} files to {:?}", written.len(), out_dir);
500
+ } else if dest.is_dir() {
501
+ let fname = if is_png {
502
+ png_utils::extract_name_from_png(&buf)
503
+ } else {
504
+ None
505
+ }.unwrap_or_else(|| {
506
+ input.file_stem()
507
+ .map(|s| s.to_string_lossy().to_string())
508
+ .unwrap_or_else(|| "out.raw".to_string())
509
+ });
510
+ write_all(&dest.join(&fname), &out_bytes)?;
511
+ } else {
512
+ write_all(&dest, &out_bytes)?;
513
+ }
514
+ }
515
+ }
516
+ Commands::Crc32 { input } => {
517
+ let buf = read_all(&input)?;
518
+ println!("crc32: {}", crate::core::crc32_bytes(&buf));
519
+ }
520
+ Commands::Adler32 { input } => {
521
+ let buf = read_all(&input)?;
522
+ println!("adler32: {}", crate::core::adler32_bytes(&buf));
523
+ }
524
+ }
525
+ Ok(())
526
+ }
527
+