roxify 1.13.3 → 1.13.5

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.
@@ -1,16 +1,20 @@
1
- use std::io::Read;
1
+ use std::fs::File;
2
+ use std::io::{Read, Seek, SeekFrom};
2
3
  use std::path::Path;
3
4
  use cipher::{KeyIvInit, StreamCipher};
4
5
 
5
6
  const PIXEL_MAGIC: &[u8] = b"PXL1";
6
7
  const MARKER_BYTES: usize = 12;
8
+ const PACK_MAGIC: [u8; 4] = 0x524f5850u32.to_be_bytes();
9
+ const HEADER_VERSION_V1: u8 = 1;
10
+ const HEADER_VERSION_V2: u8 = 2;
7
11
 
8
12
  type Aes256Ctr = ctr::Ctr64BE<aes::Aes256>;
9
13
 
10
14
  pub type DecodeProgressCallback = Box<dyn Fn(u64, u64, &str) + Send>;
11
15
 
12
16
  pub fn streaming_decode_to_dir(png_path: &Path, out_dir: &Path) -> Result<Vec<String>, String> {
13
- streaming_decode_to_dir_encrypted_with_progress(png_path, out_dir, None, None)
17
+ streaming_decode_selected_to_dir_encrypted_with_progress(png_path, out_dir, None, None, None)
14
18
  }
15
19
 
16
20
  pub fn streaming_decode_to_dir_encrypted(
@@ -18,7 +22,7 @@ pub fn streaming_decode_to_dir_encrypted(
18
22
  out_dir: &Path,
19
23
  passphrase: Option<&str>,
20
24
  ) -> Result<Vec<String>, String> {
21
- streaming_decode_to_dir_encrypted_with_progress(png_path, out_dir, passphrase, None)
25
+ streaming_decode_selected_to_dir_encrypted_with_progress(png_path, out_dir, None, passphrase, None)
22
26
  }
23
27
 
24
28
  pub fn streaming_decode_to_dir_encrypted_with_progress(
@@ -27,26 +31,30 @@ pub fn streaming_decode_to_dir_encrypted_with_progress(
27
31
  passphrase: Option<&str>,
28
32
  progress: Option<DecodeProgressCallback>,
29
33
  ) -> Result<Vec<String>, String> {
30
- let file = std::fs::File::open(png_path).map_err(|e| format!("open: {}", e))?;
31
- let mmap = unsafe { memmap2::Mmap::map(&file).map_err(|e| format!("mmap: {}", e))? };
32
- let data = &mmap[..];
34
+ streaming_decode_selected_to_dir_encrypted_with_progress(png_path, out_dir, None, passphrase, progress)
35
+ }
33
36
 
34
- if data.len() < 8 || &data[0..8] != &[137, 80, 78, 71, 13, 10, 26, 10] {
35
- return Err("Not a PNG file".into());
36
- }
37
+ pub fn streaming_decode_selected_to_dir_encrypted_with_progress(
38
+ png_path: &Path,
39
+ out_dir: &Path,
40
+ files_opt: Option<&[String]>,
41
+ passphrase: Option<&str>,
42
+ progress: Option<DecodeProgressCallback>,
43
+ ) -> Result<Vec<String>, String> {
44
+ let mut meta_file = File::open(png_path).map_err(|e| format!("open: {}", e))?;
45
+ let (width, height, idat_ranges, total_expected) = parse_png_metadata(&mut meta_file)?;
37
46
 
38
47
  if let Some(ref cb) = progress {
39
48
  cb(2, 100, "parsing_png");
40
49
  }
41
50
 
42
- let (width, height, idat_data_start, idat_data_end) = parse_png_header(data)?;
43
- let total_expected = parse_rxfl_total_bytes(data).unwrap_or(0);
44
-
45
51
  if let Some(ref cb) = progress {
46
52
  cb(5, 100, "reading_header");
47
53
  }
48
54
 
49
- let mut reader = DeflatePixelReader::new(data, width, height, idat_data_start, idat_data_end);
55
+ let data_file = File::open(png_path).map_err(|e| format!("open data: {}", e))?;
56
+ let mut reader = DeflatePixelReader::new(data_file, width, height, idat_ranges)
57
+ .map_err(|e| format!("init deflate reader: {}", e))?;
50
58
 
51
59
  let mut marker_buf = [0u8; MARKER_BYTES];
52
60
  reader.read_exact(&mut marker_buf).map_err(|e| format!("read markers: {}", e))?;
@@ -67,34 +75,31 @@ pub fn streaming_decode_to_dir_encrypted_with_progress(
67
75
  reader.read_exact(&mut name_buf).map_err(|e| format!("read name: {}", e))?;
68
76
  }
69
77
 
70
- let mut plen_buf = [0u8; 4];
71
- reader.read_exact(&mut plen_buf).map_err(|e| format!("read payload_len: {}", e))?;
72
- let payload_len = u32::from_be_bytes(plen_buf) as u64;
78
+ let payload_len = read_payload_len(&mut reader, hdr[0])?;
73
79
 
74
80
  if let Some(ref cb) = progress {
75
81
  cb(8, 100, "decrypting");
76
82
  }
77
83
 
78
- let payload_reader = reader.take(payload_len);
84
+ let mut enc_byte = [0u8; 1];
85
+ reader.read_exact(&mut enc_byte).map_err(|e| format!("read first byte: {}", e))?;
86
+ let remaining_payload_len = payload_len.saturating_sub(1);
79
87
 
80
- let first_byte_reader = FirstByteReader::new(payload_reader);
81
- let (enc_byte, remaining_reader) = first_byte_reader.into_parts()?;
82
-
83
- match enc_byte {
88
+ match enc_byte[0] {
84
89
  0x00 => {
85
90
  if let Some(ref cb) = progress {
86
91
  cb(10, 100, "decompressing");
87
92
  }
88
- let mut decoder = zstd::stream::Decoder::new(remaining_reader)
93
+ let mut decoder = zstd::stream::Decoder::new(reader)
89
94
  .map_err(|e| format!("zstd decoder: {}", e))?;
90
95
  decoder.window_log_max(31).map_err(|e| format!("zstd window_log_max: {}", e))?;
91
- read_rox1_and_untar_with_progress(decoder, out_dir, progress, total_expected)
96
+ read_rox1_and_unpack_with_progress(decoder, out_dir, files_opt, progress, total_expected)
92
97
  }
93
98
  0x03 => {
94
99
  let pass = passphrase.ok_or("Passphrase required for AES-CTR decryption")?;
95
100
  let mut salt = [0u8; 16];
96
101
  let mut iv = [0u8; 16];
97
- let mut r = remaining_reader;
102
+ let mut r = reader.take(remaining_payload_len);
98
103
  r.read_exact(&mut salt).map_err(|e| format!("read salt: {}", e))?;
99
104
  r.read_exact(&mut iv).map_err(|e| format!("read iv: {}", e))?;
100
105
 
@@ -103,7 +108,7 @@ pub fn streaming_decode_to_dir_encrypted_with_progress(
103
108
  .map_err(|e| format!("AES-CTR init: {}", e))?;
104
109
 
105
110
  let hmac_size = 32u64;
106
- let encrypted_data_len = payload_len - 1 - 16 - 16 - hmac_size;
111
+ let encrypted_data_len = remaining_payload_len - 16 - 16 - hmac_size;
107
112
  let ctr_reader = CtrDecryptReader::new(r.take(encrypted_data_len), cipher);
108
113
 
109
114
  if let Some(ref cb) = progress {
@@ -112,15 +117,32 @@ pub fn streaming_decode_to_dir_encrypted_with_progress(
112
117
  let mut decoder = zstd::stream::Decoder::new(ctr_reader)
113
118
  .map_err(|e| format!("zstd decoder: {}", e))?;
114
119
  decoder.window_log_max(31).map_err(|e| format!("zstd window_log_max: {}", e))?;
115
- read_rox1_and_untar_with_progress(decoder, out_dir, progress, total_expected)
120
+ read_rox1_and_unpack_with_progress(decoder, out_dir, files_opt, progress, total_expected)
121
+ }
122
+ _ => Err(format!("Unsupported encryption (enc=0x{:02x}) in streaming decode", enc_byte[0])),
123
+ }
124
+ }
125
+
126
+ fn read_payload_len<R: Read>(reader: &mut R, version: u8) -> Result<u64, String> {
127
+ match version {
128
+ HEADER_VERSION_V1 => {
129
+ let mut plen_buf = [0u8; 4];
130
+ reader.read_exact(&mut plen_buf).map_err(|e| format!("read payload_len: {}", e))?;
131
+ Ok(u32::from_be_bytes(plen_buf) as u64)
116
132
  }
117
- _ => Err(format!("Unsupported encryption (enc=0x{:02x}) in streaming decode", enc_byte)),
133
+ HEADER_VERSION_V2 => {
134
+ let mut plen_buf = [0u8; 8];
135
+ reader.read_exact(&mut plen_buf).map_err(|e| format!("read payload_len64: {}", e))?;
136
+ Ok(u64::from_be_bytes(plen_buf))
137
+ }
138
+ other => Err(format!("Unsupported header version {}", other)),
118
139
  }
119
140
  }
120
141
 
121
- fn read_rox1_and_untar_with_progress<R: Read>(
142
+ fn read_rox1_and_unpack_with_progress<R: Read>(
122
143
  mut decoder: R,
123
144
  out_dir: &Path,
145
+ files_opt: Option<&[String]>,
124
146
  progress: Option<DecodeProgressCallback>,
125
147
  total_expected: u64,
126
148
  ) -> Result<Vec<String>, String> {
@@ -130,87 +152,101 @@ fn read_rox1_and_untar_with_progress<R: Read>(
130
152
  return Err(format!("Expected ROX1, got {:?}", magic));
131
153
  }
132
154
  std::fs::create_dir_all(out_dir).map_err(|e| format!("mkdir: {}", e))?;
133
- tar_unpack_from_reader_with_progress(decoder, out_dir, progress, total_expected)
155
+
156
+ let mut prefix = [0u8; 4];
157
+ decoder.read_exact(&mut prefix).map_err(|e| format!("read payload magic: {}", e))?;
158
+ let mut chained = std::io::Cursor::new(prefix).chain(decoder);
159
+
160
+ if prefix == PACK_MAGIC {
161
+ crate::packer::unpack_stream_to_dir(&mut chained, out_dir, files_opt, progress.as_deref(), total_expected)
162
+ .map_err(|e| format!("pack unpack: {}", e))
163
+ } else {
164
+ tar_unpack_from_reader_with_progress(chained, out_dir, files_opt, progress, total_expected)
165
+ }
134
166
  }
135
167
 
136
- fn parse_png_header(data: &[u8]) -> Result<(usize, usize, usize, usize), String> {
137
- let mut pos = 8;
168
+ fn parse_png_metadata(file: &mut File) -> Result<(usize, usize, Vec<(u64, u64)>, u64), String> {
169
+ let mut sig = [0u8; 8];
170
+ file.read_exact(&mut sig).map_err(|e| format!("read sig: {}", e))?;
171
+ if sig != [137, 80, 78, 71, 13, 10, 26, 10] {
172
+ return Err("Not a PNG file".into());
173
+ }
138
174
 
139
175
  let mut width = 0usize;
140
176
  let mut height = 0usize;
141
- let mut idat_start = 0usize;
142
- let mut idat_end = 0usize;
177
+ let mut idat_ranges = Vec::new();
178
+ let mut total_expected = 0u64;
179
+
180
+ loop {
181
+ let mut header = [0u8; 8];
182
+ match file.read_exact(&mut header) {
183
+ Ok(()) => {}
184
+ Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => break,
185
+ Err(err) => return Err(format!("read chunk header: {}", err)),
186
+ }
143
187
 
144
- while pos + 12 <= data.len() {
145
- let chunk_len = u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
146
- let chunk_type = &data[pos + 4..pos + 8];
147
- let chunk_data_start = pos + 8;
188
+ let chunk_len = u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as u64;
189
+ let chunk_type = &header[4..8];
190
+ let chunk_data_start = file.stream_position().map_err(|e| format!("stream position: {}", e))?;
191
+ let chunk_data_end = chunk_data_start
192
+ .checked_add(chunk_len)
193
+ .ok_or_else(|| "PNG chunk length overflow".to_string())?;
148
194
 
149
195
  if chunk_type == b"IHDR" {
150
196
  if chunk_len < 13 {
151
197
  return Err("Invalid IHDR".into());
152
198
  }
153
- width = u32::from_be_bytes([
154
- data[chunk_data_start],
155
- data[chunk_data_start + 1],
156
- data[chunk_data_start + 2],
157
- data[chunk_data_start + 3],
158
- ]) as usize;
159
- height = u32::from_be_bytes([
160
- data[chunk_data_start + 4],
161
- data[chunk_data_start + 5],
162
- data[chunk_data_start + 6],
163
- data[chunk_data_start + 7],
164
- ]) as usize;
199
+ let mut ihdr = [0u8; 13];
200
+ file.read_exact(&mut ihdr).map_err(|e| format!("read IHDR: {}", e))?;
201
+ width = u32::from_be_bytes([ihdr[0], ihdr[1], ihdr[2], ihdr[3]]) as usize;
202
+ height = u32::from_be_bytes([ihdr[4], ihdr[5], ihdr[6], ihdr[7]]) as usize;
203
+ if chunk_len > 13 {
204
+ file.seek(SeekFrom::Current((chunk_len - 13) as i64)).map_err(|e| format!("seek IHDR: {}", e))?;
205
+ }
165
206
  } else if chunk_type == b"IDAT" {
166
- idat_start = chunk_data_start;
167
- idat_end = chunk_data_start + chunk_len;
207
+ idat_ranges.push((chunk_data_start, chunk_data_end));
208
+ file.seek(SeekFrom::Current(chunk_len as i64)).map_err(|e| format!("seek IDAT: {}", e))?;
209
+ } else if chunk_type == b"rXFL" {
210
+ let json_len = usize::try_from(chunk_len).map_err(|_| "rXFL too large".to_string())?;
211
+ let mut json = vec![0u8; json_len];
212
+ file.read_exact(&mut json).map_err(|e| format!("read rXFL: {}", e))?;
213
+ total_expected = parse_rxfl_total_bytes(&json).unwrap_or(total_expected);
168
214
  } else if chunk_type == b"IEND" {
169
215
  break;
216
+ } else {
217
+ file.seek(SeekFrom::Current(chunk_len as i64)).map_err(|e| format!("seek chunk: {}", e))?;
170
218
  }
171
219
 
172
- pos = chunk_data_start + chunk_len + 4;
220
+ file.seek(SeekFrom::Current(4)).map_err(|e| format!("seek crc: {}", e))?;
173
221
  }
174
222
 
175
223
  if width == 0 || height == 0 {
176
224
  return Err("IHDR not found".into());
177
225
  }
178
- if idat_start == 0 {
226
+ if idat_ranges.is_empty() {
179
227
  return Err("IDAT not found".into());
180
228
  }
181
229
 
182
- Ok((width, height, idat_start, idat_end))
230
+ Ok((width, height, idat_ranges, total_expected))
183
231
  }
184
232
 
185
- fn parse_rxfl_total_bytes(data: &[u8]) -> Option<u64> {
186
- let mut pos = 8;
187
- while pos + 12 <= data.len() {
188
- let chunk_len = u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
189
- let chunk_type = &data[pos + 4..pos + 8];
190
- let chunk_data_start = pos + 8;
191
-
192
- if chunk_type == b"rXFL" && chunk_data_start + chunk_len <= data.len() {
193
- let json_bytes = &data[chunk_data_start..chunk_data_start + chunk_len];
194
- if let Ok(entries) = serde_json::from_slice::<Vec<serde_json::Value>>(json_bytes) {
195
- let total: u64 = entries.iter()
196
- .filter_map(|e| e.get("size").and_then(|s| s.as_u64()))
197
- .sum();
198
- return Some(total);
199
- }
200
- } else if chunk_type == b"IEND" {
201
- break;
202
- }
203
-
204
- pos = chunk_data_start + chunk_len + 4;
233
+ fn parse_rxfl_total_bytes(json_bytes: &[u8]) -> Option<u64> {
234
+ if let Ok(entries) = serde_json::from_slice::<Vec<serde_json::Value>>(json_bytes) {
235
+ return Some(entries.iter()
236
+ .filter_map(|e| e.get("size").and_then(|s| s.as_u64()))
237
+ .sum());
205
238
  }
206
239
  None
207
240
  }
208
241
 
209
- struct DeflatePixelReader<'a> {
210
- data: &'a [u8],
242
+ struct DeflatePixelReader {
243
+ file: File,
211
244
  height: usize,
212
- offset: usize,
213
- idat_end: usize,
245
+ idat_ranges: Vec<(u64, u64)>,
246
+ range_index: usize,
247
+ offset: u64,
248
+ range_end: u64,
249
+ dropped_until: u64,
214
250
  block_remaining: usize,
215
251
  current_row: usize,
216
252
  col_in_row: usize,
@@ -218,20 +254,104 @@ struct DeflatePixelReader<'a> {
218
254
  row_bytes: usize,
219
255
  }
220
256
 
221
- impl<'a> DeflatePixelReader<'a> {
222
- fn new(data: &'a [u8], width: usize, height: usize, idat_data_start: usize, idat_data_end: usize) -> Self {
257
+ impl DeflatePixelReader {
258
+ fn new(mut file: File, width: usize, height: usize, idat_ranges: Vec<(u64, u64)>) -> Result<Self, String> {
259
+ let Some(&(offset, range_end)) = idat_ranges.first() else {
260
+ return Err("IDAT not found".to_string());
261
+ };
262
+ crate::io_advice::advise_file_sequential(&file);
263
+ file.seek(SeekFrom::Start(offset)).map_err(|e| format!("seek first IDAT: {}", e))?;
223
264
  let row_bytes = width * 3;
224
- Self {
225
- data,
265
+ let mut reader = Self {
266
+ file,
226
267
  height,
227
- offset: idat_data_start + 2,
228
- idat_end: idat_data_end,
268
+ idat_ranges,
269
+ range_index: 0,
270
+ offset,
271
+ range_end,
272
+ dropped_until: offset,
229
273
  block_remaining: 0,
230
274
  current_row: 0,
231
275
  col_in_row: 0,
232
276
  scanline_filter_pending: true,
233
277
  row_bytes,
278
+ };
279
+ reader.skip_stream_bytes(2)
280
+ .map_err(|e| format!("read zlib header: {}", e))?;
281
+ Ok(reader)
282
+ }
283
+
284
+ fn advance_range(&mut self) -> Result<bool, std::io::Error> {
285
+ while self.offset >= self.range_end {
286
+ crate::io_advice::advise_drop(&self.file, self.dropped_until, self.range_end.saturating_sub(self.dropped_until));
287
+ self.dropped_until = self.range_end;
288
+ self.range_index += 1;
289
+ let Some(&(offset, end)) = self.idat_ranges.get(self.range_index) else {
290
+ return Ok(false);
291
+ };
292
+ self.file.seek(SeekFrom::Start(offset))?;
293
+ self.offset = offset;
294
+ self.range_end = end;
295
+ self.dropped_until = offset;
296
+ }
297
+ Ok(true)
298
+ }
299
+
300
+ fn maybe_drop_consumed(&mut self) {
301
+ let consumed = self.offset.saturating_sub(self.dropped_until);
302
+ if consumed >= crate::io_advice::INPUT_DROP_GRANULARITY {
303
+ crate::io_advice::advise_drop(&self.file, self.dropped_until, consumed);
304
+ self.dropped_until = self.offset;
305
+ }
306
+ }
307
+
308
+ fn read_stream_bytes(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
309
+ let mut written = 0;
310
+ while written < buf.len() {
311
+ if !self.advance_range()? {
312
+ break;
313
+ }
314
+ let available = usize::try_from(self.range_end - self.offset).unwrap_or(buf.len() - written);
315
+ if available == 0 {
316
+ continue;
317
+ }
318
+ let take = available.min(buf.len() - written);
319
+ let read = self.file.read(&mut buf[written..written + take])?;
320
+ if read == 0 {
321
+ return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "failed to fill whole buffer"));
322
+ }
323
+ self.offset += read as u64;
324
+ written += read;
325
+ self.maybe_drop_consumed();
326
+ }
327
+ Ok(written)
328
+ }
329
+
330
+ fn read_stream_exact(&mut self, buf: &mut [u8]) -> Result<(), std::io::Error> {
331
+ let got = self.read_stream_bytes(buf)?;
332
+ if got == buf.len() {
333
+ return Ok(());
334
+ }
335
+ Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "failed to fill whole buffer"))
336
+ }
337
+
338
+ fn skip_stream_bytes(&mut self, count: usize) -> Result<(), std::io::Error> {
339
+ let mut remaining = count;
340
+ while remaining > 0 {
341
+ if !self.advance_range()? {
342
+ return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "failed to fill whole buffer"));
343
+ }
344
+ let available = usize::try_from(self.range_end - self.offset).unwrap_or(remaining);
345
+ if available == 0 {
346
+ continue;
347
+ }
348
+ let take = available.min(remaining);
349
+ self.file.seek(SeekFrom::Current(take as i64))?;
350
+ self.offset += take as u64;
351
+ remaining -= take;
352
+ self.maybe_drop_consumed();
234
353
  }
354
+ Ok(())
235
355
  }
236
356
 
237
357
  fn ensure_block(&mut self) -> Result<(), std::io::Error> {
@@ -239,13 +359,11 @@ impl<'a> DeflatePixelReader<'a> {
239
359
  return Ok(());
240
360
  }
241
361
 
242
- if self.offset + 5 > self.idat_end {
243
- return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "No more deflate blocks"));
244
- }
362
+ let mut header = [0u8; 5];
363
+ self.read_stream_exact(&mut header)?;
245
364
 
246
- let len_lo = self.data[self.offset + 1] as usize;
247
- let len_hi = self.data[self.offset + 2] as usize;
248
- self.offset += 5;
365
+ let len_lo = header[1] as usize;
366
+ let len_hi = header[2] as usize;
249
367
 
250
368
  self.block_remaining = len_lo | (len_hi << 8);
251
369
  Ok(())
@@ -255,14 +373,16 @@ impl<'a> DeflatePixelReader<'a> {
255
373
  let mut written = 0;
256
374
  while written < count {
257
375
  self.ensure_block()?;
258
- let avail = self.block_remaining.min(count - written).min(self.idat_end - self.offset);
376
+ let avail = self.block_remaining.min(count - written);
259
377
  if avail == 0 {
260
378
  break;
261
379
  }
262
- buf[written..written + avail].copy_from_slice(&self.data[self.offset..self.offset + avail]);
263
- self.offset += avail;
264
- self.block_remaining -= avail;
265
- written += avail;
380
+ let got = self.read_stream_bytes(&mut buf[written..written + avail])?;
381
+ if got == 0 {
382
+ break;
383
+ }
384
+ self.block_remaining -= got;
385
+ written += got;
266
386
  }
267
387
  Ok(written)
268
388
  }
@@ -271,11 +391,11 @@ impl<'a> DeflatePixelReader<'a> {
271
391
  let mut remaining = count;
272
392
  while remaining > 0 {
273
393
  self.ensure_block()?;
274
- let skip = self.block_remaining.min(remaining).min(self.idat_end - self.offset);
394
+ let skip = self.block_remaining.min(remaining);
275
395
  if skip == 0 {
276
396
  break;
277
397
  }
278
- self.offset += skip;
398
+ self.skip_stream_bytes(skip)?;
279
399
  self.block_remaining -= skip;
280
400
  remaining -= skip;
281
401
  }
@@ -283,7 +403,7 @@ impl<'a> DeflatePixelReader<'a> {
283
403
  }
284
404
  }
285
405
 
286
- impl<'a> Read for DeflatePixelReader<'a> {
406
+ impl Read for DeflatePixelReader {
287
407
  fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
288
408
  let mut filled = 0;
289
409
 
@@ -320,22 +440,6 @@ impl<'a> Read for DeflatePixelReader<'a> {
320
440
  }
321
441
  }
322
442
 
323
- struct FirstByteReader<R: Read> {
324
- inner: R,
325
- }
326
-
327
- impl<R: Read> FirstByteReader<R> {
328
- fn new(inner: R) -> Self {
329
- Self { inner }
330
- }
331
-
332
- fn into_parts(mut self) -> Result<(u8, impl Read), String> {
333
- let mut byte = [0u8; 1];
334
- self.inner.read_exact(&mut byte).map_err(|e| format!("read first byte: {}", e))?;
335
- Ok((byte[0], self.inner))
336
- }
337
- }
338
-
339
443
  struct CtrDecryptReader<R: Read> {
340
444
  inner: R,
341
445
  cipher: Aes256Ctr,
@@ -360,6 +464,7 @@ impl<R: Read> Read for CtrDecryptReader<R> {
360
464
  fn tar_unpack_from_reader_with_progress<R: Read>(
361
465
  reader: R,
362
466
  output_dir: &Path,
467
+ files_opt: Option<&[String]>,
363
468
  progress: Option<DecodeProgressCallback>,
364
469
  total_expected: u64,
365
470
  ) -> Result<Vec<String>, String> {
@@ -369,12 +474,16 @@ fn tar_unpack_from_reader_with_progress<R: Read>(
369
474
  let mut created_dirs = std::collections::HashSet::new();
370
475
  let mut bytes_extracted: u64 = 0;
371
476
  let mut last_pct: u64 = 10;
477
+ let files_filter: Option<std::collections::HashSet<&str>> = files_opt.map(|files| files.iter().map(|file| file.as_str()).collect());
478
+ let mut remaining = files_filter.as_ref().map(|files| files.len()).unwrap_or(usize::MAX);
372
479
 
373
480
  let entries = archive.entries().map_err(|e| format!("tar entries: {}", e))?;
374
481
  for entry in entries {
375
482
  let mut entry = entry.map_err(|e| format!("tar entry: {}", e))?;
376
483
  let entry_size = entry.size();
377
484
  let path = entry.path().map_err(|e| format!("tar path: {}", e))?.to_path_buf();
485
+ let logical_path = path.to_string_lossy().replace('\\', "/");
486
+ let should_write = files_filter.as_ref().map(|files| files.contains(logical_path.as_str())).unwrap_or(true);
378
487
 
379
488
  let mut safe = std::path::PathBuf::new();
380
489
  for comp in path.components() {
@@ -386,6 +495,23 @@ fn tar_unpack_from_reader_with_progress<R: Read>(
386
495
  continue;
387
496
  }
388
497
 
498
+ if !should_write {
499
+ std::io::copy(&mut entry, &mut std::io::sink()).map_err(|e| format!("skip {:?}: {}", safe, e))?;
500
+ bytes_extracted += entry_size;
501
+ if let Some(ref cb) = progress {
502
+ let pct = if total_expected > 0 {
503
+ 10 + (bytes_extracted * 89 / total_expected).min(89)
504
+ } else {
505
+ (10 + (bytes_extracted / (1024 * 1024))).min(99)
506
+ };
507
+ if pct > last_pct {
508
+ last_pct = pct;
509
+ cb(pct, 100, "extracting");
510
+ }
511
+ }
512
+ continue;
513
+ }
514
+
389
515
  let dest = output_dir.join(&safe);
390
516
  if let Some(parent) = dest.parent() {
391
517
  if created_dirs.insert(parent.to_path_buf()) {
@@ -398,7 +524,12 @@ fn tar_unpack_from_reader_with_progress<R: Read>(
398
524
  std::fs::File::create(&dest).map_err(|e| format!("create {:?}: {}", dest, e))?,
399
525
  );
400
526
  std::io::copy(&mut entry, &mut f).map_err(|e| format!("write {:?}: {}", dest, e))?;
527
+ let file = f.into_inner().map_err(|e| format!("flush {:?}: {}", dest, e.error()))?;
528
+ crate::io_advice::sync_and_drop(&file, entry_size);
401
529
  written.push(safe.to_string_lossy().to_string());
530
+ if files_filter.is_some() {
531
+ remaining = remaining.saturating_sub(1);
532
+ }
402
533
 
403
534
  bytes_extracted += entry_size;
404
535
  if let Some(ref cb) = progress {
@@ -412,6 +543,9 @@ fn tar_unpack_from_reader_with_progress<R: Read>(
412
543
  cb(pct, 100, "extracting");
413
544
  }
414
545
  }
546
+ if remaining == 0 {
547
+ break;
548
+ }
415
549
  }
416
550
 
417
551
  if let Some(ref cb) = progress {
@@ -420,3 +554,68 @@ fn tar_unpack_from_reader_with_progress<R: Read>(
420
554
 
421
555
  Ok(written)
422
556
  }
557
+
558
+ #[cfg(test)]
559
+ mod tests {
560
+ use super::*;
561
+ use std::time::{SystemTime, UNIX_EPOCH};
562
+
563
+ #[test]
564
+ fn parse_png_header_collects_all_idat_ranges() {
565
+ let mut png = Vec::new();
566
+ png.extend_from_slice(&[137, 80, 78, 71, 13, 10, 26, 10]);
567
+
568
+ let mut ihdr = [0u8; 13];
569
+ ihdr[0..4].copy_from_slice(&1u32.to_be_bytes());
570
+ ihdr[4..8].copy_from_slice(&1u32.to_be_bytes());
571
+ ihdr[8] = 8;
572
+ ihdr[9] = 2;
573
+
574
+ crate::png_chunk_writer::write_png_chunk(&mut png, b"IHDR", &ihdr).unwrap();
575
+ crate::png_chunk_writer::write_png_chunk(&mut png, b"IDAT", &[1, 2, 3]).unwrap();
576
+ crate::png_chunk_writer::write_png_chunk(&mut png, b"IDAT", &[4, 5]).unwrap();
577
+ crate::png_chunk_writer::write_png_chunk(&mut png, b"IEND", &[]).unwrap();
578
+
579
+ let ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis();
580
+ let path = std::env::temp_dir().join(format!("rox_png_header_test_{}.png", ms));
581
+ std::fs::write(&path, &png).unwrap();
582
+
583
+ let mut file = File::open(&path).unwrap();
584
+ let (_, _, ranges, _) = parse_png_metadata(&mut file).unwrap();
585
+
586
+ assert_eq!(ranges.len(), 2);
587
+ assert_eq!(&png[ranges[0].0 as usize..ranges[0].1 as usize], &[1, 2, 3]);
588
+ assert_eq!(&png[ranges[1].0 as usize..ranges[1].1 as usize], &[4, 5]);
589
+
590
+ let _ = std::fs::remove_file(path);
591
+ }
592
+
593
+ #[test]
594
+ fn deflate_reader_reads_across_idat_boundaries() {
595
+ let scanline = [0u8, 10, 11, 12, 13, 14, 15];
596
+ let mut deflate = vec![0x78, 0x01, 0x01, 0x07, 0x00, 0xF8, 0xFF];
597
+ deflate.extend_from_slice(&scanline);
598
+ deflate.extend_from_slice(&crate::core::adler32_bytes(&scanline).to_be_bytes());
599
+
600
+ let mut data = Vec::new();
601
+ data.extend_from_slice(&deflate[0..4]);
602
+ data.extend_from_slice(&[200, 201, 202]);
603
+ data.extend_from_slice(&deflate[4..12]);
604
+ data.extend_from_slice(&[203, 204]);
605
+ data.extend_from_slice(&deflate[12..]);
606
+
607
+ let ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis();
608
+ let path = std::env::temp_dir().join(format!("rox_deflate_reader_test_{}.bin", ms));
609
+ std::fs::write(&path, &data).unwrap();
610
+
611
+ let ranges = vec![(0, 4), (7, 15), (17, 23)];
612
+ let file = File::open(&path).unwrap();
613
+ let mut reader = DeflatePixelReader::new(file, 2, 1, ranges).unwrap();
614
+ let mut out = Vec::new();
615
+ reader.read_to_end(&mut out).unwrap();
616
+
617
+ assert_eq!(out, vec![10, 11, 12, 13, 14, 15]);
618
+
619
+ let _ = std::fs::remove_file(path);
620
+ }
621
+ }