roxify 1.13.4 → 1.13.6

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.
@@ -2,12 +2,15 @@ use std::io::{Write, BufWriter};
2
2
  use std::fs::File;
3
3
  use std::path::Path;
4
4
 
5
+ use crate::png_chunk_writer::{ChunkedIdatWriter, write_png_chunk};
6
+
5
7
  const PNG_HEADER: &[u8] = &[137, 80, 78, 71, 13, 10, 26, 10];
6
8
  const PIXEL_MAGIC: &[u8] = b"PXL1";
7
9
  const MARKER_START: [(u8, u8, u8); 3] = [(255, 0, 0), (0, 255, 0), (0, 0, 255)];
8
10
  const MARKER_END: [(u8, u8, u8); 3] = [(0, 0, 255), (0, 255, 0), (255, 0, 0)];
9
11
  const MARKER_ZSTD: (u8, u8, u8) = (0, 255, 0);
10
12
  const MAGIC: &[u8] = b"ROX1";
13
+ const HEADER_VERSION_V2: u8 = 2;
11
14
 
12
15
  pub fn encode_to_png_file(
13
16
  data: &[u8],
@@ -75,10 +78,6 @@ pub fn encode_to_png_file(
75
78
 
76
79
  let adler = crate::core::adler32_bytes(&scanlines);
77
80
 
78
- const MAX_BLOCK: usize = 65535;
79
- let num_blocks = (scanlines_total + MAX_BLOCK - 1) / MAX_BLOCK;
80
- let idat_len = 2 + num_blocks * 5 + scanlines_total + 4;
81
-
82
81
  let f = File::create(output_path)?;
83
82
  let mut w = BufWriter::with_capacity(16 * 1024 * 1024, f);
84
83
 
@@ -89,15 +88,15 @@ pub fn encode_to_png_file(
89
88
  ihdr[4..8].copy_from_slice(&(height as u32).to_be_bytes());
90
89
  ihdr[8] = 8;
91
90
  ihdr[9] = 2;
92
- write_chunk_small(&mut w, b"IHDR", &ihdr)?;
91
+ write_png_chunk(&mut w, b"IHDR", &ihdr)?;
93
92
 
94
- write_idat_direct(&mut w, &scanlines, idat_len, adler)?;
93
+ write_idat_direct(&mut w, &scanlines, adler)?;
95
94
  drop(scanlines);
96
95
 
97
96
  if let Some(fl) = file_list {
98
- write_chunk_small(&mut w, b"rXFL", fl.as_bytes())?;
97
+ write_png_chunk(&mut w, b"rXFL", fl.as_bytes())?;
99
98
  }
100
- write_chunk_small(&mut w, b"IEND", &[])?;
99
+ write_png_chunk(&mut w, b"IEND", &[])?;
101
100
  w.flush()?;
102
101
 
103
102
  Ok(())
@@ -106,20 +105,14 @@ pub fn encode_to_png_file(
106
105
  fn write_idat_direct<W: Write>(
107
106
  w: &mut W,
108
107
  scanlines: &[u8],
109
- idat_len: usize,
110
108
  adler: u32,
111
109
  ) -> anyhow::Result<()> {
112
110
  const MAX_BLOCK: usize = 65535;
113
111
 
114
- w.write_all(&(idat_len as u32).to_be_bytes())?;
115
- w.write_all(b"IDAT")?;
116
-
117
- let mut crc = crc32fast::Hasher::new();
118
- crc.update(b"IDAT");
112
+ let mut idat = ChunkedIdatWriter::new(w);
119
113
 
120
114
  let zlib = [0x78u8, 0x01];
121
- w.write_all(&zlib)?;
122
- crc.update(&zlib);
115
+ idat.write_all(&zlib)?;
123
116
 
124
117
  let mut offset = 0;
125
118
  while offset < scanlines.len() {
@@ -132,20 +125,15 @@ fn write_idat_direct<W: Write>(
132
125
  !chunk_size as u8,
133
126
  (!(chunk_size >> 8)) as u8,
134
127
  ];
135
- w.write_all(&header)?;
136
- crc.update(&header);
128
+ idat.write_all(&header)?;
137
129
  let slice = &scanlines[offset..offset + chunk_size];
138
- w.write_all(slice)?;
139
- crc.update(slice);
130
+ idat.write_all(slice)?;
140
131
  offset += chunk_size;
141
132
  }
142
133
 
143
134
  let adler_bytes = adler.to_be_bytes();
144
- w.write_all(&adler_bytes)?;
145
- crc.update(&adler_bytes);
146
-
147
- w.write_all(&crc.finalize().to_be_bytes())?;
148
- Ok(())
135
+ idat.write_all(&adler_bytes)?;
136
+ idat.finish()
149
137
  }
150
138
 
151
139
  fn build_flat_buffer(
@@ -175,12 +163,12 @@ fn build_flat_buffer(
175
163
  }
176
164
 
177
165
  fn build_meta_pixel(payload: &[u8], name: Option<&str>, file_list: Option<&str>) -> anyhow::Result<Vec<u8>> {
178
- let version = 1u8;
166
+ let version = HEADER_VERSION_V2;
179
167
  let name_bytes = name.map(|n| n.as_bytes()).unwrap_or(&[]);
180
168
  let name_len = name_bytes.len().min(255) as u8;
181
- let payload_len_bytes = (payload.len() as u32).to_be_bytes();
169
+ let payload_len_bytes = (payload.len() as u64).to_be_bytes();
182
170
 
183
- let mut result = Vec::with_capacity(1 + 1 + name_len as usize + 4 + payload.len() + 256);
171
+ let mut result = Vec::with_capacity(1 + 1 + name_len as usize + 8 + payload.len() + 256);
184
172
  result.push(version);
185
173
  result.push(name_len);
186
174
  if name_len > 0 {
@@ -199,14 +187,3 @@ fn build_meta_pixel(payload: &[u8], name: Option<&str>, file_list: Option<&str>)
199
187
  Ok(result)
200
188
  }
201
189
 
202
- fn write_chunk_small<W: Write>(w: &mut W, chunk_type: &[u8; 4], data: &[u8]) -> anyhow::Result<()> {
203
- w.write_all(&(data.len() as u32).to_be_bytes())?;
204
- w.write_all(chunk_type)?;
205
- w.write_all(data)?;
206
-
207
- let mut h = crc32fast::Hasher::new();
208
- h.update(chunk_type);
209
- h.update(data);
210
- w.write_all(&h.finalize().to_be_bytes())?;
211
- Ok(())
212
- }
@@ -1,10 +1,13 @@
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;
7
8
  const PACK_MAGIC: [u8; 4] = 0x524f5850u32.to_be_bytes();
9
+ const HEADER_VERSION_V1: u8 = 1;
10
+ const HEADER_VERSION_V2: u8 = 2;
8
11
 
9
12
  type Aes256Ctr = ctr::Ctr64BE<aes::Aes256>;
10
13
 
@@ -38,26 +41,20 @@ pub fn streaming_decode_selected_to_dir_encrypted_with_progress(
38
41
  passphrase: Option<&str>,
39
42
  progress: Option<DecodeProgressCallback>,
40
43
  ) -> Result<Vec<String>, String> {
41
- let file = std::fs::File::open(png_path).map_err(|e| format!("open: {}", e))?;
42
- let mmap = unsafe { memmap2::Mmap::map(&file).map_err(|e| format!("mmap: {}", e))? };
43
- let data = &mmap[..];
44
-
45
- if data.len() < 8 || &data[0..8] != &[137, 80, 78, 71, 13, 10, 26, 10] {
46
- return Err("Not a PNG file".into());
47
- }
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)?;
48
46
 
49
47
  if let Some(ref cb) = progress {
50
48
  cb(2, 100, "parsing_png");
51
49
  }
52
50
 
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
51
  if let Some(ref cb) = progress {
57
52
  cb(5, 100, "reading_header");
58
53
  }
59
54
 
60
- 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))?;
61
58
 
62
59
  let mut marker_buf = [0u8; MARKER_BYTES];
63
60
  reader.read_exact(&mut marker_buf).map_err(|e| format!("read markers: {}", e))?;
@@ -78,25 +75,22 @@ pub fn streaming_decode_selected_to_dir_encrypted_with_progress(
78
75
  reader.read_exact(&mut name_buf).map_err(|e| format!("read name: {}", e))?;
79
76
  }
80
77
 
81
- let mut plen_buf = [0u8; 4];
82
- reader.read_exact(&mut plen_buf).map_err(|e| format!("read payload_len: {}", e))?;
83
- let payload_len = u32::from_be_bytes(plen_buf) as u64;
78
+ let payload_len = read_payload_len(&mut reader, hdr[0])?;
84
79
 
85
80
  if let Some(ref cb) = progress {
86
81
  cb(8, 100, "decrypting");
87
82
  }
88
83
 
89
- let payload_reader = reader.take(payload_len);
90
-
91
- let first_byte_reader = FirstByteReader::new(payload_reader);
92
- let (enc_byte, remaining_reader) = first_byte_reader.into_parts()?;
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);
93
87
 
94
- match enc_byte {
88
+ match enc_byte[0] {
95
89
  0x00 => {
96
90
  if let Some(ref cb) = progress {
97
91
  cb(10, 100, "decompressing");
98
92
  }
99
- let mut decoder = zstd::stream::Decoder::new(remaining_reader)
93
+ let mut decoder = zstd::stream::Decoder::new(reader)
100
94
  .map_err(|e| format!("zstd decoder: {}", e))?;
101
95
  decoder.window_log_max(31).map_err(|e| format!("zstd window_log_max: {}", e))?;
102
96
  read_rox1_and_unpack_with_progress(decoder, out_dir, files_opt, progress, total_expected)
@@ -105,7 +99,7 @@ pub fn streaming_decode_selected_to_dir_encrypted_with_progress(
105
99
  let pass = passphrase.ok_or("Passphrase required for AES-CTR decryption")?;
106
100
  let mut salt = [0u8; 16];
107
101
  let mut iv = [0u8; 16];
108
- let mut r = remaining_reader;
102
+ let mut r = reader.take(remaining_payload_len);
109
103
  r.read_exact(&mut salt).map_err(|e| format!("read salt: {}", e))?;
110
104
  r.read_exact(&mut iv).map_err(|e| format!("read iv: {}", e))?;
111
105
 
@@ -114,7 +108,7 @@ pub fn streaming_decode_selected_to_dir_encrypted_with_progress(
114
108
  .map_err(|e| format!("AES-CTR init: {}", e))?;
115
109
 
116
110
  let hmac_size = 32u64;
117
- let encrypted_data_len = payload_len - 1 - 16 - 16 - hmac_size;
111
+ let encrypted_data_len = remaining_payload_len - 16 - 16 - hmac_size;
118
112
  let ctr_reader = CtrDecryptReader::new(r.take(encrypted_data_len), cipher);
119
113
 
120
114
  if let Some(ref cb) = progress {
@@ -125,7 +119,23 @@ pub fn streaming_decode_selected_to_dir_encrypted_with_progress(
125
119
  decoder.window_log_max(31).map_err(|e| format!("zstd window_log_max: {}", e))?;
126
120
  read_rox1_and_unpack_with_progress(decoder, out_dir, files_opt, progress, total_expected)
127
121
  }
128
- _ => Err(format!("Unsupported encryption (enc=0x{:02x}) in streaming decode", enc_byte)),
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)
132
+ }
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)),
129
139
  }
130
140
  }
131
141
 
@@ -155,84 +165,88 @@ fn read_rox1_and_unpack_with_progress<R: Read>(
155
165
  }
156
166
  }
157
167
 
158
- fn parse_png_header(data: &[u8]) -> Result<(usize, usize, usize, usize), String> {
159
- 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
+ }
160
174
 
161
175
  let mut width = 0usize;
162
176
  let mut height = 0usize;
163
- let mut idat_start = 0usize;
164
- 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
+ }
165
187
 
166
- while pos + 12 <= data.len() {
167
- let chunk_len = u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
168
- let chunk_type = &data[pos + 4..pos + 8];
169
- 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())?;
170
194
 
171
195
  if chunk_type == b"IHDR" {
172
196
  if chunk_len < 13 {
173
197
  return Err("Invalid IHDR".into());
174
198
  }
175
- width = u32::from_be_bytes([
176
- data[chunk_data_start],
177
- data[chunk_data_start + 1],
178
- data[chunk_data_start + 2],
179
- data[chunk_data_start + 3],
180
- ]) as usize;
181
- height = u32::from_be_bytes([
182
- data[chunk_data_start + 4],
183
- data[chunk_data_start + 5],
184
- data[chunk_data_start + 6],
185
- data[chunk_data_start + 7],
186
- ]) 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
+ }
187
206
  } else if chunk_type == b"IDAT" {
188
- idat_start = chunk_data_start;
189
- 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);
190
214
  } else if chunk_type == b"IEND" {
191
215
  break;
216
+ } else {
217
+ file.seek(SeekFrom::Current(chunk_len as i64)).map_err(|e| format!("seek chunk: {}", e))?;
192
218
  }
193
219
 
194
- pos = chunk_data_start + chunk_len + 4;
220
+ file.seek(SeekFrom::Current(4)).map_err(|e| format!("seek crc: {}", e))?;
195
221
  }
196
222
 
197
223
  if width == 0 || height == 0 {
198
224
  return Err("IHDR not found".into());
199
225
  }
200
- if idat_start == 0 {
226
+ if idat_ranges.is_empty() {
201
227
  return Err("IDAT not found".into());
202
228
  }
203
229
 
204
- Ok((width, height, idat_start, idat_end))
230
+ Ok((width, height, idat_ranges, total_expected))
205
231
  }
206
232
 
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;
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());
227
238
  }
228
239
  None
229
240
  }
230
241
 
231
- struct DeflatePixelReader<'a> {
232
- data: &'a [u8],
242
+ struct DeflatePixelReader {
243
+ file: File,
233
244
  height: usize,
234
- offset: usize,
235
- idat_end: usize,
245
+ idat_ranges: Vec<(u64, u64)>,
246
+ range_index: usize,
247
+ offset: u64,
248
+ range_end: u64,
249
+ dropped_until: u64,
236
250
  block_remaining: usize,
237
251
  current_row: usize,
238
252
  col_in_row: usize,
@@ -240,20 +254,104 @@ struct DeflatePixelReader<'a> {
240
254
  row_bytes: usize,
241
255
  }
242
256
 
243
- impl<'a> DeflatePixelReader<'a> {
244
- 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))?;
245
264
  let row_bytes = width * 3;
246
- Self {
247
- data,
265
+ let mut reader = Self {
266
+ file,
248
267
  height,
249
- offset: idat_data_start + 2,
250
- idat_end: idat_data_end,
268
+ idat_ranges,
269
+ range_index: 0,
270
+ offset,
271
+ range_end,
272
+ dropped_until: offset,
251
273
  block_remaining: 0,
252
274
  current_row: 0,
253
275
  col_in_row: 0,
254
276
  scanline_filter_pending: true,
255
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();
256
353
  }
354
+ Ok(())
257
355
  }
258
356
 
259
357
  fn ensure_block(&mut self) -> Result<(), std::io::Error> {
@@ -261,13 +359,11 @@ impl<'a> DeflatePixelReader<'a> {
261
359
  return Ok(());
262
360
  }
263
361
 
264
- if self.offset + 5 > self.idat_end {
265
- return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "No more deflate blocks"));
266
- }
362
+ let mut header = [0u8; 5];
363
+ self.read_stream_exact(&mut header)?;
267
364
 
268
- let len_lo = self.data[self.offset + 1] as usize;
269
- let len_hi = self.data[self.offset + 2] as usize;
270
- self.offset += 5;
365
+ let len_lo = header[1] as usize;
366
+ let len_hi = header[2] as usize;
271
367
 
272
368
  self.block_remaining = len_lo | (len_hi << 8);
273
369
  Ok(())
@@ -277,14 +373,16 @@ impl<'a> DeflatePixelReader<'a> {
277
373
  let mut written = 0;
278
374
  while written < count {
279
375
  self.ensure_block()?;
280
- let avail = self.block_remaining.min(count - written).min(self.idat_end - self.offset);
376
+ let avail = self.block_remaining.min(count - written);
281
377
  if avail == 0 {
282
378
  break;
283
379
  }
284
- buf[written..written + avail].copy_from_slice(&self.data[self.offset..self.offset + avail]);
285
- self.offset += avail;
286
- self.block_remaining -= avail;
287
- 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;
288
386
  }
289
387
  Ok(written)
290
388
  }
@@ -293,11 +391,11 @@ impl<'a> DeflatePixelReader<'a> {
293
391
  let mut remaining = count;
294
392
  while remaining > 0 {
295
393
  self.ensure_block()?;
296
- let skip = self.block_remaining.min(remaining).min(self.idat_end - self.offset);
394
+ let skip = self.block_remaining.min(remaining);
297
395
  if skip == 0 {
298
396
  break;
299
397
  }
300
- self.offset += skip;
398
+ self.skip_stream_bytes(skip)?;
301
399
  self.block_remaining -= skip;
302
400
  remaining -= skip;
303
401
  }
@@ -305,7 +403,7 @@ impl<'a> DeflatePixelReader<'a> {
305
403
  }
306
404
  }
307
405
 
308
- impl<'a> Read for DeflatePixelReader<'a> {
406
+ impl Read for DeflatePixelReader {
309
407
  fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
310
408
  let mut filled = 0;
311
409
 
@@ -342,22 +440,6 @@ impl<'a> Read for DeflatePixelReader<'a> {
342
440
  }
343
441
  }
344
442
 
345
- struct FirstByteReader<R: Read> {
346
- inner: R,
347
- }
348
-
349
- impl<R: Read> FirstByteReader<R> {
350
- fn new(inner: R) -> Self {
351
- Self { inner }
352
- }
353
-
354
- fn into_parts(mut self) -> Result<(u8, impl Read), String> {
355
- let mut byte = [0u8; 1];
356
- self.inner.read_exact(&mut byte).map_err(|e| format!("read first byte: {}", e))?;
357
- Ok((byte[0], self.inner))
358
- }
359
- }
360
-
361
443
  struct CtrDecryptReader<R: Read> {
362
444
  inner: R,
363
445
  cipher: Aes256Ctr,
@@ -437,11 +519,17 @@ fn tar_unpack_from_reader_with_progress<R: Read>(
437
519
  }
438
520
  }
439
521
 
522
+ // NTFS optimization: larger buffer reduces syscalls (16MB max, 256KB min)
523
+ let buffer_size = (entry_size as usize)
524
+ .min(16 * 1024 * 1024) // 16MB max buffer
525
+ .max(256 * 1024); // 256KB min buffer for NTFS
440
526
  let mut f = std::io::BufWriter::with_capacity(
441
- (entry_size as usize).min(4 * 1024 * 1024).max(8192),
527
+ buffer_size,
442
528
  std::fs::File::create(&dest).map_err(|e| format!("create {:?}: {}", dest, e))?,
443
529
  );
444
530
  std::io::copy(&mut entry, &mut f).map_err(|e| format!("write {:?}: {}", dest, e))?;
531
+ let file = f.into_inner().map_err(|e| format!("flush {:?}: {}", dest, e.error()))?;
532
+ crate::io_advice::sync_and_drop(&file, entry_size);
445
533
  written.push(safe.to_string_lossy().to_string());
446
534
  if files_filter.is_some() {
447
535
  remaining = remaining.saturating_sub(1);
@@ -470,3 +558,68 @@ fn tar_unpack_from_reader_with_progress<R: Read>(
470
558
 
471
559
  Ok(written)
472
560
  }
561
+
562
+ #[cfg(test)]
563
+ mod tests {
564
+ use super::*;
565
+ use std::time::{SystemTime, UNIX_EPOCH};
566
+
567
+ #[test]
568
+ fn parse_png_header_collects_all_idat_ranges() {
569
+ let mut png = Vec::new();
570
+ png.extend_from_slice(&[137, 80, 78, 71, 13, 10, 26, 10]);
571
+
572
+ let mut ihdr = [0u8; 13];
573
+ ihdr[0..4].copy_from_slice(&1u32.to_be_bytes());
574
+ ihdr[4..8].copy_from_slice(&1u32.to_be_bytes());
575
+ ihdr[8] = 8;
576
+ ihdr[9] = 2;
577
+
578
+ crate::png_chunk_writer::write_png_chunk(&mut png, b"IHDR", &ihdr).unwrap();
579
+ crate::png_chunk_writer::write_png_chunk(&mut png, b"IDAT", &[1, 2, 3]).unwrap();
580
+ crate::png_chunk_writer::write_png_chunk(&mut png, b"IDAT", &[4, 5]).unwrap();
581
+ crate::png_chunk_writer::write_png_chunk(&mut png, b"IEND", &[]).unwrap();
582
+
583
+ let ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis();
584
+ let path = std::env::temp_dir().join(format!("rox_png_header_test_{}.png", ms));
585
+ std::fs::write(&path, &png).unwrap();
586
+
587
+ let mut file = File::open(&path).unwrap();
588
+ let (_, _, ranges, _) = parse_png_metadata(&mut file).unwrap();
589
+
590
+ assert_eq!(ranges.len(), 2);
591
+ assert_eq!(&png[ranges[0].0 as usize..ranges[0].1 as usize], &[1, 2, 3]);
592
+ assert_eq!(&png[ranges[1].0 as usize..ranges[1].1 as usize], &[4, 5]);
593
+
594
+ let _ = std::fs::remove_file(path);
595
+ }
596
+
597
+ #[test]
598
+ fn deflate_reader_reads_across_idat_boundaries() {
599
+ let scanline = [0u8, 10, 11, 12, 13, 14, 15];
600
+ let mut deflate = vec![0x78, 0x01, 0x01, 0x07, 0x00, 0xF8, 0xFF];
601
+ deflate.extend_from_slice(&scanline);
602
+ deflate.extend_from_slice(&crate::core::adler32_bytes(&scanline).to_be_bytes());
603
+
604
+ let mut data = Vec::new();
605
+ data.extend_from_slice(&deflate[0..4]);
606
+ data.extend_from_slice(&[200, 201, 202]);
607
+ data.extend_from_slice(&deflate[4..12]);
608
+ data.extend_from_slice(&[203, 204]);
609
+ data.extend_from_slice(&deflate[12..]);
610
+
611
+ let ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis();
612
+ let path = std::env::temp_dir().join(format!("rox_deflate_reader_test_{}.bin", ms));
613
+ std::fs::write(&path, &data).unwrap();
614
+
615
+ let ranges = vec![(0, 4), (7, 15), (17, 23)];
616
+ let file = File::open(&path).unwrap();
617
+ let mut reader = DeflatePixelReader::new(file, 2, 1, ranges).unwrap();
618
+ let mut out = Vec::new();
619
+ reader.read_to_end(&mut out).unwrap();
620
+
621
+ assert_eq!(out, vec![10, 11, 12, 13, 14, 15]);
622
+
623
+ let _ = std::fs::remove_file(path);
624
+ }
625
+ }