roxify 1.13.2 → 1.13.4

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.
@@ -4,17 +4,39 @@ use cipher::{KeyIvInit, StreamCipher};
4
4
 
5
5
  const PIXEL_MAGIC: &[u8] = b"PXL1";
6
6
  const MARKER_BYTES: usize = 12;
7
+ const PACK_MAGIC: [u8; 4] = 0x524f5850u32.to_be_bytes();
7
8
 
8
9
  type Aes256Ctr = ctr::Ctr64BE<aes::Aes256>;
9
10
 
11
+ pub type DecodeProgressCallback = Box<dyn Fn(u64, u64, &str) + Send>;
12
+
10
13
  pub fn streaming_decode_to_dir(png_path: &Path, out_dir: &Path) -> Result<Vec<String>, String> {
11
- streaming_decode_to_dir_encrypted(png_path, out_dir, None)
14
+ streaming_decode_selected_to_dir_encrypted_with_progress(png_path, out_dir, None, None, None)
12
15
  }
13
16
 
14
17
  pub fn streaming_decode_to_dir_encrypted(
15
18
  png_path: &Path,
16
19
  out_dir: &Path,
17
20
  passphrase: Option<&str>,
21
+ ) -> Result<Vec<String>, String> {
22
+ streaming_decode_selected_to_dir_encrypted_with_progress(png_path, out_dir, None, passphrase, None)
23
+ }
24
+
25
+ pub fn streaming_decode_to_dir_encrypted_with_progress(
26
+ png_path: &Path,
27
+ out_dir: &Path,
28
+ passphrase: Option<&str>,
29
+ progress: Option<DecodeProgressCallback>,
30
+ ) -> Result<Vec<String>, String> {
31
+ streaming_decode_selected_to_dir_encrypted_with_progress(png_path, out_dir, None, passphrase, progress)
32
+ }
33
+
34
+ pub fn streaming_decode_selected_to_dir_encrypted_with_progress(
35
+ png_path: &Path,
36
+ out_dir: &Path,
37
+ files_opt: Option<&[String]>,
38
+ passphrase: Option<&str>,
39
+ progress: Option<DecodeProgressCallback>,
18
40
  ) -> Result<Vec<String>, String> {
19
41
  let file = std::fs::File::open(png_path).map_err(|e| format!("open: {}", e))?;
20
42
  let mmap = unsafe { memmap2::Mmap::map(&file).map_err(|e| format!("mmap: {}", e))? };
@@ -24,7 +46,16 @@ pub fn streaming_decode_to_dir_encrypted(
24
46
  return Err("Not a PNG file".into());
25
47
  }
26
48
 
49
+ if let Some(ref cb) = progress {
50
+ cb(2, 100, "parsing_png");
51
+ }
52
+
27
53
  let (width, height, idat_data_start, idat_data_end) = parse_png_header(data)?;
54
+ let total_expected = parse_rxfl_total_bytes(data).unwrap_or(0);
55
+
56
+ if let Some(ref cb) = progress {
57
+ cb(5, 100, "reading_header");
58
+ }
28
59
 
29
60
  let mut reader = DeflatePixelReader::new(data, width, height, idat_data_start, idat_data_end);
30
61
 
@@ -51,6 +82,10 @@ pub fn streaming_decode_to_dir_encrypted(
51
82
  reader.read_exact(&mut plen_buf).map_err(|e| format!("read payload_len: {}", e))?;
52
83
  let payload_len = u32::from_be_bytes(plen_buf) as u64;
53
84
 
85
+ if let Some(ref cb) = progress {
86
+ cb(8, 100, "decrypting");
87
+ }
88
+
54
89
  let payload_reader = reader.take(payload_len);
55
90
 
56
91
  let first_byte_reader = FirstByteReader::new(payload_reader);
@@ -58,10 +93,13 @@ pub fn streaming_decode_to_dir_encrypted(
58
93
 
59
94
  match enc_byte {
60
95
  0x00 => {
96
+ if let Some(ref cb) = progress {
97
+ cb(10, 100, "decompressing");
98
+ }
61
99
  let mut decoder = zstd::stream::Decoder::new(remaining_reader)
62
100
  .map_err(|e| format!("zstd decoder: {}", e))?;
63
101
  decoder.window_log_max(31).map_err(|e| format!("zstd window_log_max: {}", e))?;
64
- read_rox1_and_untar(decoder, out_dir)
102
+ read_rox1_and_unpack_with_progress(decoder, out_dir, files_opt, progress, total_expected)
65
103
  }
66
104
  0x03 => {
67
105
  let pass = passphrase.ok_or("Passphrase required for AES-CTR decryption")?;
@@ -79,23 +117,42 @@ pub fn streaming_decode_to_dir_encrypted(
79
117
  let encrypted_data_len = payload_len - 1 - 16 - 16 - hmac_size;
80
118
  let ctr_reader = CtrDecryptReader::new(r.take(encrypted_data_len), cipher);
81
119
 
120
+ if let Some(ref cb) = progress {
121
+ cb(10, 100, "decompressing");
122
+ }
82
123
  let mut decoder = zstd::stream::Decoder::new(ctr_reader)
83
124
  .map_err(|e| format!("zstd decoder: {}", e))?;
84
125
  decoder.window_log_max(31).map_err(|e| format!("zstd window_log_max: {}", e))?;
85
- read_rox1_and_untar(decoder, out_dir)
126
+ read_rox1_and_unpack_with_progress(decoder, out_dir, files_opt, progress, total_expected)
86
127
  }
87
128
  _ => Err(format!("Unsupported encryption (enc=0x{:02x}) in streaming decode", enc_byte)),
88
129
  }
89
130
  }
90
131
 
91
- fn read_rox1_and_untar<R: Read>(mut decoder: R, out_dir: &Path) -> Result<Vec<String>, String> {
132
+ fn read_rox1_and_unpack_with_progress<R: Read>(
133
+ mut decoder: R,
134
+ out_dir: &Path,
135
+ files_opt: Option<&[String]>,
136
+ progress: Option<DecodeProgressCallback>,
137
+ total_expected: u64,
138
+ ) -> Result<Vec<String>, String> {
92
139
  let mut magic = [0u8; 4];
93
140
  decoder.read_exact(&mut magic).map_err(|e| format!("read ROX1: {}", e))?;
94
141
  if &magic != b"ROX1" {
95
142
  return Err(format!("Expected ROX1, got {:?}", magic));
96
143
  }
97
144
  std::fs::create_dir_all(out_dir).map_err(|e| format!("mkdir: {}", e))?;
98
- tar_unpack_from_reader(decoder, out_dir)
145
+
146
+ let mut prefix = [0u8; 4];
147
+ decoder.read_exact(&mut prefix).map_err(|e| format!("read payload magic: {}", e))?;
148
+ let mut chained = std::io::Cursor::new(prefix).chain(decoder);
149
+
150
+ if prefix == PACK_MAGIC {
151
+ crate::packer::unpack_stream_to_dir(&mut chained, out_dir, files_opt, progress.as_deref(), total_expected)
152
+ .map_err(|e| format!("pack unpack: {}", e))
153
+ } else {
154
+ tar_unpack_from_reader_with_progress(chained, out_dir, files_opt, progress, total_expected)
155
+ }
99
156
  }
100
157
 
101
158
  fn parse_png_header(data: &[u8]) -> Result<(usize, usize, usize, usize), String> {
@@ -147,6 +204,30 @@ fn parse_png_header(data: &[u8]) -> Result<(usize, usize, usize, usize), String>
147
204
  Ok((width, height, idat_start, idat_end))
148
205
  }
149
206
 
207
+ fn parse_rxfl_total_bytes(data: &[u8]) -> Option<u64> {
208
+ let mut pos = 8;
209
+ while pos + 12 <= data.len() {
210
+ let chunk_len = u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
211
+ let chunk_type = &data[pos + 4..pos + 8];
212
+ let chunk_data_start = pos + 8;
213
+
214
+ if chunk_type == b"rXFL" && chunk_data_start + chunk_len <= data.len() {
215
+ let json_bytes = &data[chunk_data_start..chunk_data_start + chunk_len];
216
+ if let Ok(entries) = serde_json::from_slice::<Vec<serde_json::Value>>(json_bytes) {
217
+ let total: u64 = entries.iter()
218
+ .filter_map(|e| e.get("size").and_then(|s| s.as_u64()))
219
+ .sum();
220
+ return Some(total);
221
+ }
222
+ } else if chunk_type == b"IEND" {
223
+ break;
224
+ }
225
+
226
+ pos = chunk_data_start + chunk_len + 4;
227
+ }
228
+ None
229
+ }
230
+
150
231
  struct DeflatePixelReader<'a> {
151
232
  data: &'a [u8],
152
233
  height: usize,
@@ -298,16 +379,29 @@ impl<R: Read> Read for CtrDecryptReader<R> {
298
379
  }
299
380
  }
300
381
 
301
- fn tar_unpack_from_reader<R: Read>(reader: R, output_dir: &Path) -> Result<Vec<String>, String> {
382
+ fn tar_unpack_from_reader_with_progress<R: Read>(
383
+ reader: R,
384
+ output_dir: &Path,
385
+ files_opt: Option<&[String]>,
386
+ progress: Option<DecodeProgressCallback>,
387
+ total_expected: u64,
388
+ ) -> Result<Vec<String>, String> {
302
389
  let buf_reader = std::io::BufReader::with_capacity(8 * 1024 * 1024, reader);
303
390
  let mut archive = tar::Archive::new(buf_reader);
304
391
  let mut written = Vec::new();
305
392
  let mut created_dirs = std::collections::HashSet::new();
393
+ let mut bytes_extracted: u64 = 0;
394
+ let mut last_pct: u64 = 10;
395
+ let files_filter: Option<std::collections::HashSet<&str>> = files_opt.map(|files| files.iter().map(|file| file.as_str()).collect());
396
+ let mut remaining = files_filter.as_ref().map(|files| files.len()).unwrap_or(usize::MAX);
306
397
 
307
398
  let entries = archive.entries().map_err(|e| format!("tar entries: {}", e))?;
308
399
  for entry in entries {
309
400
  let mut entry = entry.map_err(|e| format!("tar entry: {}", e))?;
401
+ let entry_size = entry.size();
310
402
  let path = entry.path().map_err(|e| format!("tar path: {}", e))?.to_path_buf();
403
+ let logical_path = path.to_string_lossy().replace('\\', "/");
404
+ let should_write = files_filter.as_ref().map(|files| files.contains(logical_path.as_str())).unwrap_or(true);
311
405
 
312
406
  let mut safe = std::path::PathBuf::new();
313
407
  for comp in path.components() {
@@ -319,6 +413,23 @@ fn tar_unpack_from_reader<R: Read>(reader: R, output_dir: &Path) -> Result<Vec<S
319
413
  continue;
320
414
  }
321
415
 
416
+ if !should_write {
417
+ std::io::copy(&mut entry, &mut std::io::sink()).map_err(|e| format!("skip {:?}: {}", safe, e))?;
418
+ bytes_extracted += entry_size;
419
+ if let Some(ref cb) = progress {
420
+ let pct = if total_expected > 0 {
421
+ 10 + (bytes_extracted * 89 / total_expected).min(89)
422
+ } else {
423
+ (10 + (bytes_extracted / (1024 * 1024))).min(99)
424
+ };
425
+ if pct > last_pct {
426
+ last_pct = pct;
427
+ cb(pct, 100, "extracting");
428
+ }
429
+ }
430
+ continue;
431
+ }
432
+
322
433
  let dest = output_dir.join(&safe);
323
434
  if let Some(parent) = dest.parent() {
324
435
  if created_dirs.insert(parent.to_path_buf()) {
@@ -327,11 +438,34 @@ fn tar_unpack_from_reader<R: Read>(reader: R, output_dir: &Path) -> Result<Vec<S
327
438
  }
328
439
 
329
440
  let mut f = std::io::BufWriter::with_capacity(
330
- (entry.size() as usize).min(4 * 1024 * 1024).max(8192),
441
+ (entry_size as usize).min(4 * 1024 * 1024).max(8192),
331
442
  std::fs::File::create(&dest).map_err(|e| format!("create {:?}: {}", dest, e))?,
332
443
  );
333
444
  std::io::copy(&mut entry, &mut f).map_err(|e| format!("write {:?}: {}", dest, e))?;
334
445
  written.push(safe.to_string_lossy().to_string());
446
+ if files_filter.is_some() {
447
+ remaining = remaining.saturating_sub(1);
448
+ }
449
+
450
+ bytes_extracted += entry_size;
451
+ if let Some(ref cb) = progress {
452
+ let pct = if total_expected > 0 {
453
+ 10 + (bytes_extracted * 89 / total_expected).min(89)
454
+ } else {
455
+ (10 + (bytes_extracted / (1024 * 1024))).min(99)
456
+ };
457
+ if pct > last_pct {
458
+ last_pct = pct;
459
+ cb(pct, 100, "extracting");
460
+ }
461
+ }
462
+ if remaining == 0 {
463
+ break;
464
+ }
465
+ }
466
+
467
+ if let Some(ref cb) = progress {
468
+ cb(99, 100, "finishing");
335
469
  }
336
470
 
337
471
  Ok(written)