roxify 1.14.0 → 1.14.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.
Files changed (42) hide show
  1. package/Cargo.toml +76 -0
  2. package/dist/cli.js +45 -22
  3. package/native/archive.rs +220 -0
  4. package/native/audio.rs +151 -0
  5. package/native/bench_hybrid.rs +145 -0
  6. package/native/bwt.rs +56 -0
  7. package/native/context_mixing.rs +117 -0
  8. package/native/core.rs +378 -0
  9. package/native/crypto.rs +209 -0
  10. package/native/encoder.rs +405 -0
  11. package/native/hybrid.rs +297 -0
  12. package/native/image_utils.rs +82 -0
  13. package/native/io_advice.rs +43 -0
  14. package/native/io_ntfs_optimized.rs +99 -0
  15. package/native/lib.rs +480 -0
  16. package/native/main.rs +939 -0
  17. package/native/mtf.rs +106 -0
  18. package/native/packer.rs +863 -0
  19. package/native/png_chunk_writer.rs +146 -0
  20. package/native/png_utils.rs +554 -0
  21. package/native/pool.rs +101 -0
  22. package/native/progress.rs +142 -0
  23. package/native/rans.rs +149 -0
  24. package/native/rans_byte.rs +286 -0
  25. package/native/reconstitution.rs +623 -0
  26. package/native/streaming.rs +189 -0
  27. package/native/streaming_decode.rs +720 -0
  28. package/native/streaming_encode.rs +684 -0
  29. package/native/test_small_bwt.rs +31 -0
  30. package/native/test_stages.rs +70 -0
  31. package/package.json +5 -3
  32. package/scripts/download-binary.cjs +259 -0
  33. package/scripts/postinstall.cjs +136 -110
  34. package/roxify_native-aarch64-apple-darwin.node +0 -0
  35. package/roxify_native-aarch64-pc-windows-msvc.node +0 -0
  36. package/roxify_native-aarch64-unknown-linux-gnu.node +0 -0
  37. package/roxify_native-i686-pc-windows-msvc.node +0 -0
  38. package/roxify_native-i686-unknown-linux-gnu.node +0 -0
  39. package/roxify_native-universal-apple-darwin.node +0 -0
  40. package/roxify_native-x86_64-apple-darwin.node +0 -0
  41. package/roxify_native-x86_64-pc-windows-msvc.node +0 -0
  42. package/roxify_native-x86_64-unknown-linux-gnu.node +0 -0
@@ -0,0 +1,720 @@
1
+ use cipher::{KeyIvInit, StreamCipher};
2
+ use std::fs::File;
3
+ use std::io::{Read, Seek, SeekFrom};
4
+ use std::path::Path;
5
+
6
+ const PIXEL_MAGIC: &[u8] = b"PXL1";
7
+ const MARKER_BYTES: usize = 12;
8
+ const HEADER_VERSION_V1: u8 = 1;
9
+ const HEADER_VERSION_V2: u8 = 2;
10
+
11
+ type Aes256Ctr = ctr::Ctr64BE<aes::Aes256>;
12
+
13
+ pub type DecodeProgressCallback = Box<dyn Fn(u64, u64, &str) + Send>;
14
+
15
+ pub fn streaming_decode_to_dir(png_path: &Path, out_dir: &Path) -> Result<Vec<String>, String> {
16
+ streaming_decode_selected_to_dir_encrypted_with_progress(png_path, out_dir, None, None, None)
17
+ }
18
+
19
+ pub fn streaming_decode_to_dir_encrypted(
20
+ png_path: &Path,
21
+ out_dir: &Path,
22
+ passphrase: Option<&str>,
23
+ ) -> Result<Vec<String>, String> {
24
+ streaming_decode_selected_to_dir_encrypted_with_progress(
25
+ png_path, out_dir, None, passphrase, None,
26
+ )
27
+ }
28
+
29
+ pub fn streaming_decode_to_dir_encrypted_with_progress(
30
+ png_path: &Path,
31
+ out_dir: &Path,
32
+ passphrase: Option<&str>,
33
+ progress: Option<DecodeProgressCallback>,
34
+ ) -> Result<Vec<String>, String> {
35
+ streaming_decode_selected_to_dir_encrypted_with_progress(
36
+ png_path, out_dir, None, passphrase, progress,
37
+ )
38
+ }
39
+
40
+ pub fn streaming_decode_selected_to_dir_encrypted_with_progress(
41
+ png_path: &Path,
42
+ out_dir: &Path,
43
+ files_opt: Option<&[String]>,
44
+ passphrase: Option<&str>,
45
+ progress: Option<DecodeProgressCallback>,
46
+ ) -> Result<Vec<String>, String> {
47
+ let mut meta_file = File::open(png_path).map_err(|e| format!("open: {}", e))?;
48
+ let (width, height, idat_ranges, total_expected) = parse_png_metadata(&mut meta_file)?;
49
+
50
+ if let Some(ref cb) = progress {
51
+ cb(2, 100, "parsing_png");
52
+ }
53
+
54
+ if let Some(ref cb) = progress {
55
+ cb(5, 100, "reading_header");
56
+ }
57
+
58
+ let data_file = File::open(png_path).map_err(|e| format!("open data: {}", e))?;
59
+ let mut reader = DeflatePixelReader::new(data_file, width, height, idat_ranges)
60
+ .map_err(|e| format!("init deflate reader: {}", e))?;
61
+
62
+ let mut marker_buf = [0u8; MARKER_BYTES];
63
+ reader
64
+ .read_exact(&mut marker_buf)
65
+ .map_err(|e| format!("read markers: {}", e))?;
66
+
67
+ let mut pxl1 = [0u8; 4];
68
+ reader
69
+ .read_exact(&mut pxl1)
70
+ .map_err(|e| format!("read PXL1: {}", e))?;
71
+ if &pxl1 != PIXEL_MAGIC {
72
+ return Err(format!("Expected PXL1, got {:?}", pxl1));
73
+ }
74
+
75
+ let mut hdr = [0u8; 2];
76
+ reader
77
+ .read_exact(&mut hdr)
78
+ .map_err(|e| format!("read hdr: {}", e))?;
79
+ let _version = hdr[0];
80
+ let name_len = hdr[1] as usize;
81
+
82
+ if name_len > 0 {
83
+ let mut name_buf = vec![0u8; name_len];
84
+ reader
85
+ .read_exact(&mut name_buf)
86
+ .map_err(|e| format!("read name: {}", e))?;
87
+ }
88
+
89
+ let payload_len = read_payload_len(&mut reader, hdr[0])?;
90
+
91
+ if let Some(ref cb) = progress {
92
+ cb(8, 100, "decrypting");
93
+ }
94
+
95
+ let mut enc_byte = [0u8; 1];
96
+ reader
97
+ .read_exact(&mut enc_byte)
98
+ .map_err(|e| format!("read first byte: {}", e))?;
99
+ let remaining_payload_len = payload_len.saturating_sub(1);
100
+
101
+ match enc_byte[0] {
102
+ 0x00 => {
103
+ if let Some(ref cb) = progress {
104
+ cb(10, 100, "decompressing");
105
+ }
106
+ let mut decoder =
107
+ zstd::stream::Decoder::new(reader).map_err(|e| format!("zstd decoder: {}", e))?;
108
+ decoder
109
+ .window_log_max(31)
110
+ .map_err(|e| format!("zstd window_log_max: {}", e))?;
111
+ read_rox1_and_unpack_with_progress(
112
+ decoder,
113
+ out_dir,
114
+ files_opt,
115
+ progress,
116
+ total_expected,
117
+ )
118
+ }
119
+ 0x03 => {
120
+ let pass = passphrase.ok_or("Passphrase required for AES-CTR decryption")?;
121
+ let mut salt = [0u8; 16];
122
+ let mut iv = [0u8; 16];
123
+ let mut r = reader.take(remaining_payload_len);
124
+ r.read_exact(&mut salt)
125
+ .map_err(|e| format!("read salt: {}", e))?;
126
+ r.read_exact(&mut iv)
127
+ .map_err(|e| format!("read iv: {}", e))?;
128
+
129
+ let key = crate::crypto::derive_aes_ctr_key(pass, &salt);
130
+ let cipher = Aes256Ctr::new_from_slices(&key, &iv)
131
+ .map_err(|e| format!("AES-CTR init: {}", e))?;
132
+
133
+ let hmac_size = 32u64;
134
+ let encrypted_data_len = remaining_payload_len - 16 - 16 - hmac_size;
135
+ let ctr_reader = CtrDecryptReader::new(r.take(encrypted_data_len), cipher);
136
+
137
+ if let Some(ref cb) = progress {
138
+ cb(10, 100, "decompressing");
139
+ }
140
+ let mut decoder = zstd::stream::Decoder::new(ctr_reader)
141
+ .map_err(|e| format!("zstd decoder: {}", e))?;
142
+ decoder
143
+ .window_log_max(31)
144
+ .map_err(|e| format!("zstd window_log_max: {}", e))?;
145
+ read_rox1_and_unpack_with_progress(
146
+ decoder,
147
+ out_dir,
148
+ files_opt,
149
+ progress,
150
+ total_expected,
151
+ )
152
+ }
153
+ _ => Err(format!(
154
+ "Unsupported encryption (enc=0x{:02x}) in streaming decode",
155
+ enc_byte[0]
156
+ )),
157
+ }
158
+ }
159
+
160
+ fn read_payload_len<R: Read>(reader: &mut R, version: u8) -> Result<u64, String> {
161
+ match version {
162
+ HEADER_VERSION_V1 => {
163
+ let mut plen_buf = [0u8; 4];
164
+ reader
165
+ .read_exact(&mut plen_buf)
166
+ .map_err(|e| format!("read payload_len: {}", e))?;
167
+ Ok(u32::from_be_bytes(plen_buf) as u64)
168
+ }
169
+ HEADER_VERSION_V2 => {
170
+ let mut plen_buf = [0u8; 8];
171
+ reader
172
+ .read_exact(&mut plen_buf)
173
+ .map_err(|e| format!("read payload_len64: {}", e))?;
174
+ Ok(u64::from_be_bytes(plen_buf))
175
+ }
176
+ other => Err(format!("Unsupported header version {}", other)),
177
+ }
178
+ }
179
+
180
+ fn read_rox1_and_unpack_with_progress<R: Read>(
181
+ mut decoder: R,
182
+ out_dir: &Path,
183
+ files_opt: Option<&[String]>,
184
+ progress: Option<DecodeProgressCallback>,
185
+ total_expected: u64,
186
+ ) -> Result<Vec<String>, String> {
187
+ let mut magic = [0u8; 4];
188
+ decoder
189
+ .read_exact(&mut magic)
190
+ .map_err(|e| format!("read ROX1: {}", e))?;
191
+ if &magic != b"ROX1" {
192
+ return Err(format!("Expected ROX1, got {:?}", magic));
193
+ }
194
+ std::fs::create_dir_all(out_dir).map_err(|e| format!("mkdir: {}", e))?;
195
+
196
+ // Reconstruct the stream with ROX1 prefix for packer (it expects ROX1/ROXI/ROXP sequence)
197
+ let mut chained = std::io::Cursor::new(magic).chain(decoder);
198
+ crate::packer::unpack_stream_to_dir(
199
+ &mut chained,
200
+ out_dir,
201
+ files_opt,
202
+ progress.as_deref(),
203
+ total_expected,
204
+ )
205
+ .map_err(|e| format!("pack unpack: {}", e))
206
+ }
207
+
208
+ fn parse_png_metadata(file: &mut File) -> Result<(usize, usize, Vec<(u64, u64)>, u64), String> {
209
+ let mut sig = [0u8; 8];
210
+ file.read_exact(&mut sig)
211
+ .map_err(|e| format!("read sig: {}", e))?;
212
+ if sig != [137, 80, 78, 71, 13, 10, 26, 10] {
213
+ return Err("Not a PNG file".into());
214
+ }
215
+
216
+ let mut width = 0usize;
217
+ let mut height = 0usize;
218
+ let mut idat_ranges = Vec::new();
219
+ let mut total_expected = 0u64;
220
+
221
+ loop {
222
+ let mut header = [0u8; 8];
223
+ match file.read_exact(&mut header) {
224
+ Ok(()) => {}
225
+ Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => break,
226
+ Err(err) => return Err(format!("read chunk header: {}", err)),
227
+ }
228
+
229
+ let chunk_len = u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as u64;
230
+ let chunk_type = &header[4..8];
231
+ let chunk_data_start = file
232
+ .stream_position()
233
+ .map_err(|e| format!("stream position: {}", e))?;
234
+ let chunk_data_end = chunk_data_start
235
+ .checked_add(chunk_len)
236
+ .ok_or_else(|| "PNG chunk length overflow".to_string())?;
237
+
238
+ if chunk_type == b"IHDR" {
239
+ if chunk_len < 13 {
240
+ return Err("Invalid IHDR".into());
241
+ }
242
+ let mut ihdr = [0u8; 13];
243
+ file.read_exact(&mut ihdr)
244
+ .map_err(|e| format!("read IHDR: {}", e))?;
245
+ width = u32::from_be_bytes([ihdr[0], ihdr[1], ihdr[2], ihdr[3]]) as usize;
246
+ height = u32::from_be_bytes([ihdr[4], ihdr[5], ihdr[6], ihdr[7]]) as usize;
247
+ if chunk_len > 13 {
248
+ file.seek(SeekFrom::Current((chunk_len - 13) as i64))
249
+ .map_err(|e| format!("seek IHDR: {}", e))?;
250
+ }
251
+ } else if chunk_type == b"IDAT" {
252
+ idat_ranges.push((chunk_data_start, chunk_data_end));
253
+ file.seek(SeekFrom::Current(chunk_len as i64))
254
+ .map_err(|e| format!("seek IDAT: {}", e))?;
255
+ } else if chunk_type == b"rXFL" {
256
+ let json_len = usize::try_from(chunk_len).map_err(|_| "rXFL too large".to_string())?;
257
+ let mut json = vec![0u8; json_len];
258
+ file.read_exact(&mut json)
259
+ .map_err(|e| format!("read rXFL: {}", e))?;
260
+ total_expected = parse_rxfl_total_bytes(&json).unwrap_or(total_expected);
261
+ } else if chunk_type == b"IEND" {
262
+ break;
263
+ } else {
264
+ file.seek(SeekFrom::Current(chunk_len as i64))
265
+ .map_err(|e| format!("seek chunk: {}", e))?;
266
+ }
267
+
268
+ file.seek(SeekFrom::Current(4))
269
+ .map_err(|e| format!("seek crc: {}", e))?;
270
+ }
271
+
272
+ if width == 0 || height == 0 {
273
+ return Err("IHDR not found".into());
274
+ }
275
+ if idat_ranges.is_empty() {
276
+ return Err("IDAT not found".into());
277
+ }
278
+
279
+ Ok((width, height, idat_ranges, total_expected))
280
+ }
281
+
282
+ fn parse_rxfl_total_bytes(json_bytes: &[u8]) -> Option<u64> {
283
+ if let Ok(entries) = serde_json::from_slice::<Vec<serde_json::Value>>(json_bytes) {
284
+ return Some(
285
+ entries
286
+ .iter()
287
+ .filter_map(|e| e.get("size").and_then(|s| s.as_u64()))
288
+ .sum(),
289
+ );
290
+ }
291
+ None
292
+ }
293
+
294
+ struct DeflatePixelReader {
295
+ file: File,
296
+ height: usize,
297
+ idat_ranges: Vec<(u64, u64)>,
298
+ range_index: usize,
299
+ offset: u64,
300
+ range_end: u64,
301
+ dropped_until: u64,
302
+ block_remaining: usize,
303
+ current_row: usize,
304
+ col_in_row: usize,
305
+ scanline_filter_pending: bool,
306
+ row_bytes: usize,
307
+ }
308
+
309
+ impl DeflatePixelReader {
310
+ fn new(
311
+ mut file: File,
312
+ width: usize,
313
+ height: usize,
314
+ idat_ranges: Vec<(u64, u64)>,
315
+ ) -> Result<Self, String> {
316
+ let Some(&(offset, range_end)) = idat_ranges.first() else {
317
+ return Err("IDAT not found".to_string());
318
+ };
319
+ crate::io_advice::advise_file_sequential(&file);
320
+ file.seek(SeekFrom::Start(offset))
321
+ .map_err(|e| format!("seek first IDAT: {}", e))?;
322
+ let row_bytes = width * 3;
323
+ let mut reader = Self {
324
+ file,
325
+ height,
326
+ idat_ranges,
327
+ range_index: 0,
328
+ offset,
329
+ range_end,
330
+ dropped_until: offset,
331
+ block_remaining: 0,
332
+ current_row: 0,
333
+ col_in_row: 0,
334
+ scanline_filter_pending: true,
335
+ row_bytes,
336
+ };
337
+ reader
338
+ .skip_stream_bytes(2)
339
+ .map_err(|e| format!("read zlib header: {}", e))?;
340
+ Ok(reader)
341
+ }
342
+
343
+ fn advance_range(&mut self) -> Result<bool, std::io::Error> {
344
+ while self.offset >= self.range_end {
345
+ crate::io_advice::advise_drop(
346
+ &self.file,
347
+ self.dropped_until,
348
+ self.range_end.saturating_sub(self.dropped_until),
349
+ );
350
+ self.dropped_until = self.range_end;
351
+ self.range_index += 1;
352
+ let Some(&(offset, end)) = self.idat_ranges.get(self.range_index) else {
353
+ return Ok(false);
354
+ };
355
+ self.file.seek(SeekFrom::Start(offset))?;
356
+ self.offset = offset;
357
+ self.range_end = end;
358
+ self.dropped_until = offset;
359
+ }
360
+ Ok(true)
361
+ }
362
+
363
+ fn maybe_drop_consumed(&mut self) {
364
+ let consumed = self.offset.saturating_sub(self.dropped_until);
365
+ if consumed >= crate::io_advice::INPUT_DROP_GRANULARITY {
366
+ crate::io_advice::advise_drop(&self.file, self.dropped_until, consumed);
367
+ self.dropped_until = self.offset;
368
+ }
369
+ }
370
+
371
+ fn read_stream_bytes(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
372
+ let mut written = 0;
373
+ while written < buf.len() {
374
+ if !self.advance_range()? {
375
+ break;
376
+ }
377
+ let available =
378
+ usize::try_from(self.range_end - self.offset).unwrap_or(buf.len() - written);
379
+ if available == 0 {
380
+ continue;
381
+ }
382
+ let take = available.min(buf.len() - written);
383
+ let read = self.file.read(&mut buf[written..written + take])?;
384
+ if read == 0 {
385
+ return Err(std::io::Error::new(
386
+ std::io::ErrorKind::UnexpectedEof,
387
+ "failed to fill whole buffer",
388
+ ));
389
+ }
390
+ self.offset += read as u64;
391
+ written += read;
392
+ self.maybe_drop_consumed();
393
+ }
394
+ Ok(written)
395
+ }
396
+
397
+ fn read_stream_exact(&mut self, buf: &mut [u8]) -> Result<(), std::io::Error> {
398
+ let got = self.read_stream_bytes(buf)?;
399
+ if got == buf.len() {
400
+ return Ok(());
401
+ }
402
+ Err(std::io::Error::new(
403
+ std::io::ErrorKind::UnexpectedEof,
404
+ "failed to fill whole buffer",
405
+ ))
406
+ }
407
+
408
+ fn skip_stream_bytes(&mut self, count: usize) -> Result<(), std::io::Error> {
409
+ let mut remaining = count;
410
+ while remaining > 0 {
411
+ if !self.advance_range()? {
412
+ return Err(std::io::Error::new(
413
+ std::io::ErrorKind::UnexpectedEof,
414
+ "failed to fill whole buffer",
415
+ ));
416
+ }
417
+ let available = usize::try_from(self.range_end - self.offset).unwrap_or(remaining);
418
+ if available == 0 {
419
+ continue;
420
+ }
421
+ let take = available.min(remaining);
422
+ self.file.seek(SeekFrom::Current(take as i64))?;
423
+ self.offset += take as u64;
424
+ remaining -= take;
425
+ self.maybe_drop_consumed();
426
+ }
427
+ Ok(())
428
+ }
429
+
430
+ fn ensure_block(&mut self) -> Result<(), std::io::Error> {
431
+ if self.block_remaining > 0 {
432
+ return Ok(());
433
+ }
434
+
435
+ let mut header = [0u8; 5];
436
+ self.read_stream_exact(&mut header)?;
437
+
438
+ let len_lo = header[1] as usize;
439
+ let len_hi = header[2] as usize;
440
+
441
+ self.block_remaining = len_lo | (len_hi << 8);
442
+ Ok(())
443
+ }
444
+
445
+ fn copy_raw_bytes(&mut self, buf: &mut [u8], count: usize) -> Result<usize, std::io::Error> {
446
+ let mut written = 0;
447
+ while written < count {
448
+ self.ensure_block()?;
449
+ let avail = self.block_remaining.min(count - written);
450
+ if avail == 0 {
451
+ break;
452
+ }
453
+ let got = self.read_stream_bytes(&mut buf[written..written + avail])?;
454
+ if got == 0 {
455
+ break;
456
+ }
457
+ self.block_remaining -= got;
458
+ written += got;
459
+ }
460
+ Ok(written)
461
+ }
462
+
463
+ fn skip_raw_bytes(&mut self, count: usize) -> Result<(), std::io::Error> {
464
+ let mut remaining = count;
465
+ while remaining > 0 {
466
+ self.ensure_block()?;
467
+ let skip = self.block_remaining.min(remaining);
468
+ if skip == 0 {
469
+ break;
470
+ }
471
+ self.skip_stream_bytes(skip)?;
472
+ self.block_remaining -= skip;
473
+ remaining -= skip;
474
+ }
475
+ Ok(())
476
+ }
477
+ }
478
+
479
+ impl Read for DeflatePixelReader {
480
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
481
+ let mut filled = 0;
482
+
483
+ while filled < buf.len() {
484
+ if self.current_row >= self.height {
485
+ break;
486
+ }
487
+
488
+ if self.scanline_filter_pending {
489
+ self.skip_raw_bytes(1)?;
490
+ self.scanline_filter_pending = false;
491
+ self.col_in_row = 0;
492
+ }
493
+
494
+ if self.col_in_row >= self.row_bytes {
495
+ self.current_row += 1;
496
+ self.scanline_filter_pending = true;
497
+ continue;
498
+ }
499
+
500
+ let remaining_in_row = self.row_bytes - self.col_in_row;
501
+ let remaining_in_buf = buf.len() - filled;
502
+ let to_read = remaining_in_row.min(remaining_in_buf);
503
+
504
+ let got = self.copy_raw_bytes(&mut buf[filled..filled + to_read], to_read)?;
505
+ filled += got;
506
+ self.col_in_row += got;
507
+ if got == 0 {
508
+ break;
509
+ }
510
+ }
511
+
512
+ Ok(filled)
513
+ }
514
+ }
515
+
516
+ struct CtrDecryptReader<R: Read> {
517
+ inner: R,
518
+ cipher: Aes256Ctr,
519
+ }
520
+
521
+ impl<R: Read> CtrDecryptReader<R> {
522
+ fn new(inner: R, cipher: Aes256Ctr) -> Self {
523
+ Self { inner, cipher }
524
+ }
525
+ }
526
+
527
+ impl<R: Read> Read for CtrDecryptReader<R> {
528
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
529
+ let n = self.inner.read(buf)?;
530
+ if n > 0 {
531
+ self.cipher.apply_keystream(&mut buf[..n]);
532
+ }
533
+ Ok(n)
534
+ }
535
+ }
536
+
537
+ fn tar_unpack_from_reader_with_progress<R: Read>(
538
+ reader: R,
539
+ output_dir: &Path,
540
+ files_opt: Option<&[String]>,
541
+ progress: Option<DecodeProgressCallback>,
542
+ total_expected: u64,
543
+ ) -> Result<Vec<String>, String> {
544
+ let buf_reader = std::io::BufReader::with_capacity(8 * 1024 * 1024, reader);
545
+ let mut archive = tar::Archive::new(buf_reader);
546
+ let mut written = Vec::new();
547
+ let mut created_dirs = std::collections::HashSet::new();
548
+ let mut bytes_extracted: u64 = 0;
549
+ let mut last_pct: u64 = 10;
550
+ let files_filter: Option<std::collections::HashSet<&str>> =
551
+ files_opt.map(|files| files.iter().map(|file| file.as_str()).collect());
552
+ let mut remaining = files_filter
553
+ .as_ref()
554
+ .map(|files| files.len())
555
+ .unwrap_or(usize::MAX);
556
+
557
+ let entries = archive
558
+ .entries()
559
+ .map_err(|e| format!("tar entries: {}", e))?;
560
+ for entry in entries {
561
+ let mut entry = entry.map_err(|e| format!("tar entry: {}", e))?;
562
+ let entry_size = entry.size();
563
+ let path = entry
564
+ .path()
565
+ .map_err(|e| format!("tar path: {}", e))?
566
+ .to_path_buf();
567
+ let logical_path = path.to_string_lossy().replace('\\', "/");
568
+ let should_write = files_filter
569
+ .as_ref()
570
+ .map(|files| files.contains(logical_path.as_str()))
571
+ .unwrap_or(true);
572
+
573
+ let mut safe = std::path::PathBuf::new();
574
+ for comp in path.components() {
575
+ if let std::path::Component::Normal(osstr) = comp {
576
+ safe.push(osstr);
577
+ }
578
+ }
579
+ if safe.as_os_str().is_empty() {
580
+ continue;
581
+ }
582
+
583
+ if !should_write {
584
+ std::io::copy(&mut entry, &mut std::io::sink())
585
+ .map_err(|e| format!("skip {:?}: {}", safe, e))?;
586
+ bytes_extracted += entry_size;
587
+ if let Some(ref cb) = progress {
588
+ let pct = if total_expected > 0 {
589
+ 10 + (bytes_extracted * 89 / total_expected).min(89)
590
+ } else {
591
+ (10 + (bytes_extracted / (1024 * 1024))).min(99)
592
+ };
593
+ if pct > last_pct {
594
+ last_pct = pct;
595
+ cb(pct, 100, "extracting");
596
+ }
597
+ }
598
+ continue;
599
+ }
600
+
601
+ let dest = output_dir.join(&safe);
602
+ if let Some(parent) = dest.parent() {
603
+ if created_dirs.insert(parent.to_path_buf()) {
604
+ std::fs::create_dir_all(parent)
605
+ .map_err(|e| format!("mkdir {:?}: {}", parent, e))?;
606
+ }
607
+ }
608
+
609
+ // NTFS optimization: larger buffer reduces syscalls (16MB max, 256KB min)
610
+ let buffer_size = (entry_size as usize)
611
+ .min(16 * 1024 * 1024) // 16MB max buffer
612
+ .max(256 * 1024); // 256KB min buffer for NTFS
613
+ let mut f = std::io::BufWriter::with_capacity(
614
+ buffer_size,
615
+ std::fs::File::create(&dest).map_err(|e| format!("create {:?}: {}", dest, e))?,
616
+ );
617
+ std::io::copy(&mut entry, &mut f).map_err(|e| format!("write {:?}: {}", dest, e))?;
618
+ let file = f
619
+ .into_inner()
620
+ .map_err(|e| format!("flush {:?}: {}", dest, e.error()))?;
621
+ crate::io_advice::sync_and_drop(&file, entry_size);
622
+ written.push(safe.to_string_lossy().to_string());
623
+ if files_filter.is_some() {
624
+ remaining = remaining.saturating_sub(1);
625
+ }
626
+
627
+ bytes_extracted += entry_size;
628
+ if let Some(ref cb) = progress {
629
+ let pct = if total_expected > 0 {
630
+ 10 + (bytes_extracted * 89 / total_expected).min(89)
631
+ } else {
632
+ (10 + (bytes_extracted / (1024 * 1024))).min(99)
633
+ };
634
+ if pct > last_pct {
635
+ last_pct = pct;
636
+ cb(pct, 100, "extracting");
637
+ }
638
+ }
639
+ if remaining == 0 {
640
+ break;
641
+ }
642
+ }
643
+
644
+ if let Some(ref cb) = progress {
645
+ cb(99, 100, "finishing");
646
+ }
647
+
648
+ Ok(written)
649
+ }
650
+
651
+ #[cfg(test)]
652
+ mod tests {
653
+ use super::*;
654
+ use std::time::{SystemTime, UNIX_EPOCH};
655
+
656
+ #[test]
657
+ fn parse_png_header_collects_all_idat_ranges() {
658
+ let mut png = Vec::new();
659
+ png.extend_from_slice(&[137, 80, 78, 71, 13, 10, 26, 10]);
660
+
661
+ let mut ihdr = [0u8; 13];
662
+ ihdr[0..4].copy_from_slice(&1u32.to_be_bytes());
663
+ ihdr[4..8].copy_from_slice(&1u32.to_be_bytes());
664
+ ihdr[8] = 8;
665
+ ihdr[9] = 2;
666
+
667
+ crate::png_chunk_writer::write_png_chunk(&mut png, b"IHDR", &ihdr).unwrap();
668
+ crate::png_chunk_writer::write_png_chunk(&mut png, b"IDAT", &[1, 2, 3]).unwrap();
669
+ crate::png_chunk_writer::write_png_chunk(&mut png, b"IDAT", &[4, 5]).unwrap();
670
+ crate::png_chunk_writer::write_png_chunk(&mut png, b"IEND", &[]).unwrap();
671
+
672
+ let ms = SystemTime::now()
673
+ .duration_since(UNIX_EPOCH)
674
+ .unwrap()
675
+ .as_millis();
676
+ let path = std::env::temp_dir().join(format!("rox_png_header_test_{}.png", ms));
677
+ std::fs::write(&path, &png).unwrap();
678
+
679
+ let mut file = File::open(&path).unwrap();
680
+ let (_, _, ranges, _) = parse_png_metadata(&mut file).unwrap();
681
+
682
+ assert_eq!(ranges.len(), 2);
683
+ assert_eq!(&png[ranges[0].0 as usize..ranges[0].1 as usize], &[1, 2, 3]);
684
+ assert_eq!(&png[ranges[1].0 as usize..ranges[1].1 as usize], &[4, 5]);
685
+
686
+ let _ = std::fs::remove_file(path);
687
+ }
688
+
689
+ #[test]
690
+ fn deflate_reader_reads_across_idat_boundaries() {
691
+ let scanline = [0u8, 10, 11, 12, 13, 14, 15];
692
+ let mut deflate = vec![0x78, 0x01, 0x01, 0x07, 0x00, 0xF8, 0xFF];
693
+ deflate.extend_from_slice(&scanline);
694
+ deflate.extend_from_slice(&crate::core::adler32_bytes(&scanline).to_be_bytes());
695
+
696
+ let mut data = Vec::new();
697
+ data.extend_from_slice(&deflate[0..4]);
698
+ data.extend_from_slice(&[200, 201, 202]);
699
+ data.extend_from_slice(&deflate[4..12]);
700
+ data.extend_from_slice(&[203, 204]);
701
+ data.extend_from_slice(&deflate[12..]);
702
+
703
+ let ms = SystemTime::now()
704
+ .duration_since(UNIX_EPOCH)
705
+ .unwrap()
706
+ .as_millis();
707
+ let path = std::env::temp_dir().join(format!("rox_deflate_reader_test_{}.bin", ms));
708
+ std::fs::write(&path, &data).unwrap();
709
+
710
+ let ranges = vec![(0, 4), (7, 15), (17, 23)];
711
+ let file = File::open(&path).unwrap();
712
+ let mut reader = DeflatePixelReader::new(file, 2, 1, ranges).unwrap();
713
+ let mut out = Vec::new();
714
+ reader.read_to_end(&mut out).unwrap();
715
+
716
+ assert_eq!(out, vec![10, 11, 12, 13, 14, 15]);
717
+
718
+ let _ = std::fs::remove_file(path);
719
+ }
720
+ }