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.
package/native/packer.rs CHANGED
@@ -255,96 +255,210 @@ fn unpack_entries_sequential(buf: &[u8], start: usize, out_dir: &Path, files_opt
255
255
  Ok(written)
256
256
  }
257
257
 
258
- pub fn unpack_stream_to_dir<R: std::io::Read>(reader: &mut R, out_dir: &Path, files_opt: Option<&[String]>) -> Result<Vec<String>> {
258
+ fn unpack_progress_percent(total_expected: u64, bytes_processed: u64, file_count: usize, processed_files: usize) -> u64 {
259
+ if total_expected > 0 {
260
+ return 10 + (bytes_processed.saturating_mul(89) / total_expected).min(89);
261
+ }
262
+ if file_count > 0 {
263
+ return 10 + ((processed_files as u64).saturating_mul(89) / file_count as u64).min(89);
264
+ }
265
+ 10
266
+ }
267
+
268
+ fn report_unpack_progress(
269
+ progress: Option<&(dyn Fn(u64, u64, &str) + Send)>,
270
+ total_expected: u64,
271
+ bytes_processed: u64,
272
+ file_count: usize,
273
+ processed_files: usize,
274
+ last_pct: &mut u64,
275
+ ) {
276
+ if let Some(cb) = progress {
277
+ let pct = unpack_progress_percent(total_expected, bytes_processed, file_count, processed_files);
278
+ if pct > *last_pct {
279
+ *last_pct = pct;
280
+ cb(pct, 100, "extracting");
281
+ }
282
+ }
283
+ }
284
+
285
+ pub fn unpack_stream_to_dir<R: std::io::Read>(
286
+ reader: &mut R,
287
+ out_dir: &Path,
288
+ files_opt: Option<&[String]>,
289
+ progress: Option<&(dyn Fn(u64, u64, &str) + Send)>,
290
+ total_expected: u64,
291
+ ) -> Result<Vec<String>> {
259
292
  let mut written = Vec::new();
260
- let mut buf: Vec<u8> = Vec::new();
261
- let mut pos: usize = 0;
262
- let mut temp = [0u8; 64 * 1024];
263
293
  let files_filter: Option<std::collections::HashSet<String>> = files_opt.map(|l| l.iter().map(|s| s.clone()).collect());
264
294
  let mut requested = files_filter.as_ref().map(|s| s.len()).unwrap_or(usize::MAX);
295
+ let mut file_count = 0usize;
296
+ let mut processed_files = 0usize;
297
+ let mut bytes_processed = 0u64;
298
+ let mut last_pct = 10u64;
299
+
300
+ let mut magic = read_pack_u32(reader)?;
301
+ if magic == 0x524f5831u32 {
302
+ magic = read_pack_u32(reader)?;
303
+ }
304
+ if magic == 0x524f5849u32 {
305
+ let index_len = read_pack_u32(reader)? as u64;
306
+ discard_pack_bytes(reader, index_len, &mut bytes_processed, file_count, processed_files, total_expected, progress, &mut last_pct)?;
307
+ magic = read_pack_u32(reader)?;
308
+ }
309
+ if magic != 0x524f5850u32 {
310
+ return Err(anyhow::anyhow!("Invalid pack magic: 0x{:08x}", magic));
311
+ }
265
312
 
266
- let mut header_parsed = false;
267
- let debug = std::env::var("ROX_DEBUG").is_ok();
268
- if debug { eprintln!("[rox debug] unpack_stream_to_dir called (out_dir={:?})", out_dir); }
269
-
270
- loop {
271
- loop {
272
- if !header_parsed {
273
- if pos + 8 > buf.len() { break; }
274
- if debug {
275
- eprintln!("[rox debug] buf.len={} pos={} first16={:?}", buf.len(), pos, &buf[0..std::cmp::min(16, buf.len())]);
276
- eprintln!("[rox debug] after first debug");
277
- }
278
- if debug { eprintln!("[rox debug] before reading magic_header"); }
279
- let magic_header = u32::from_be_bytes(buf[pos..pos+4].try_into().unwrap());
280
- if debug { eprintln!("[rox debug] magic_header=0x{:08x}", magic_header); }
281
- if magic_header == 0x524f5850u32 {
282
- pos += 4;
283
- let _file_count = u32::from_be_bytes(buf[pos..pos+4].try_into().unwrap()) as usize;
284
- pos += 4;
285
- header_parsed = true;
286
- if debug { eprintln!("[rox debug] header parsed, file_count={}", _file_count); }
287
- } else if magic_header == 0x524f5831u32 {
288
- if debug { eprintln!("[rox debug] found ROX1 outer magic, skipping 4 bytes"); }
289
- pos += 4;
290
- continue; } else {
291
- }
313
+ file_count = read_pack_u32(reader)? as usize;
314
+
315
+ for _ in 0..file_count {
316
+ let name_len = read_pack_u16(reader)? as usize;
317
+ let mut name_bytes = vec![0u8; name_len];
318
+ read_pack_exact(reader, &mut name_bytes)?;
319
+ let name = String::from_utf8_lossy(&name_bytes).to_string();
320
+ let size = read_pack_u64(reader)?;
321
+
322
+ let should_write = match &files_filter {
323
+ Some(set) => set.contains(&name),
324
+ None => true,
325
+ };
326
+
327
+ if should_write {
328
+ let safe = sanitize_pack_path(&name);
329
+ let dest = out_dir.join(&safe);
330
+ if let Some(parent) = dest.parent() {
331
+ std::fs::create_dir_all(parent).map_err(|e| anyhow::anyhow!("Cannot create parent dir {:?}: {}", parent, e))?;
332
+ }
333
+ let file = std::fs::File::create(&dest).map_err(|e| anyhow::anyhow!("Cannot write {:?}: {}", dest, e))?;
334
+ let mut writer = std::io::BufWriter::with_capacity(file_buffer_capacity(size), file);
335
+ copy_pack_bytes(reader, &mut writer, size, &mut bytes_processed, file_count, processed_files, total_expected, progress, &mut last_pct)?;
336
+ finalize_output_file(writer, size, &dest)?;
337
+ written.push(safe.to_string_lossy().to_string());
338
+ if files_filter.is_some() {
339
+ requested = requested.saturating_sub(1);
292
340
  }
341
+ } else {
342
+ discard_pack_bytes(reader, size, &mut bytes_processed, file_count, processed_files, total_expected, progress, &mut last_pct)?;
343
+ }
344
+
345
+ processed_files = processed_files.saturating_add(1);
346
+ report_unpack_progress(progress, total_expected, bytes_processed, file_count, processed_files, &mut last_pct);
293
347
 
294
- if pos + 8 > buf.len() { break; }
295
- let magic = u32::from_be_bytes(buf[pos..pos+4].try_into().unwrap());
296
- if magic == 0x524f5849u32 {
297
- if pos + 8 > buf.len() { break; }
298
- let index_len = u32::from_be_bytes(buf[pos+4..pos+8].try_into().unwrap()) as usize;
299
- if pos + 8 + index_len > buf.len() { break; }
300
- pos += 8 + index_len;
348
+ if requested == 0 {
349
+ if let Some(cb) = progress {
350
+ cb(99, 100, "finishing");
301
351
  }
352
+ return Ok(written);
353
+ }
354
+ }
302
355
 
303
- if pos + 2 > buf.len() { break; }
304
- let name_len = u16::from_be_bytes(buf[pos..pos+2].try_into().unwrap()) as usize;
305
- if pos + 2 + name_len + 8 > buf.len() { break; }
306
- let name = String::from_utf8_lossy(&buf[pos+2..pos+2+name_len]).to_string();
307
- let size = u64::from_be_bytes(buf[pos+2+name_len..pos+2+name_len+8].try_into().unwrap()) as usize;
308
- if pos + 2 + name_len + 8 + size > buf.len() { break; }
356
+ if let Some(cb) = progress {
357
+ cb(99, 100, "finishing");
358
+ }
309
359
 
310
- let content_start = pos + 2 + name_len + 8;
311
- let content_end = content_start + size;
312
- let content = &buf[content_start..content_end];
360
+ Ok(written)
361
+ }
313
362
 
314
- let p = Path::new(&name);
315
- let mut safe = std::path::PathBuf::new();
316
- for comp in p.components() {
317
- if let std::path::Component::Normal(osstr) = comp {
318
- safe.push(osstr);
319
- }
320
- }
321
- let dest = out_dir.join(&safe);
363
+ fn read_pack_exact<R: std::io::Read>(reader: &mut R, buf: &mut [u8]) -> Result<()> {
364
+ reader.read_exact(buf).map_err(|e| anyhow::anyhow!("Stream read error: {}", e))
365
+ }
322
366
 
323
- if files_filter.is_none() || files_filter.as_ref().map_or(false, |s| s.contains(&name)) {
324
- if let Some(parent) = dest.parent() {
325
- std::fs::create_dir_all(parent).map_err(|e| anyhow::anyhow!("Cannot create parent dir {:?}: {}", parent, e))?;
326
- }
327
- std::fs::write(&dest, content).map_err(|e| anyhow::anyhow!("Cannot write {:?}: {}", dest, e))?;
328
- written.push(safe.to_string_lossy().to_string());
329
- if let Some(_set) = files_filter.as_ref() {
330
- requested = requested.saturating_sub(1);
331
- if requested == 0 { return Ok(written); }
332
- }
333
- }
367
+ fn read_pack_u16<R: std::io::Read>(reader: &mut R) -> Result<u16> {
368
+ let mut buf = [0u8; 2];
369
+ read_pack_exact(reader, &mut buf)?;
370
+ Ok(u16::from_be_bytes(buf))
371
+ }
334
372
 
335
- pos = content_end; if pos > 0 {
336
- buf.drain(0..pos);
337
- pos = 0;
338
- }
373
+ fn read_pack_u32<R: std::io::Read>(reader: &mut R) -> Result<u32> {
374
+ let mut buf = [0u8; 4];
375
+ read_pack_exact(reader, &mut buf)?;
376
+ Ok(u32::from_be_bytes(buf))
377
+ }
378
+
379
+ fn read_pack_u64<R: std::io::Read>(reader: &mut R) -> Result<u64> {
380
+ let mut buf = [0u8; 8];
381
+ read_pack_exact(reader, &mut buf)?;
382
+ Ok(u64::from_be_bytes(buf))
383
+ }
384
+
385
+ fn sanitize_pack_path(name: &str) -> std::path::PathBuf {
386
+ let p = Path::new(name);
387
+ let mut safe = std::path::PathBuf::new();
388
+ for comp in p.components() {
389
+ if let std::path::Component::Normal(osstr) = comp {
390
+ safe.push(osstr);
339
391
  }
392
+ }
393
+ safe
394
+ }
395
+
396
+ fn file_buffer_capacity(size: u64) -> usize {
397
+ usize::try_from(size)
398
+ .unwrap_or(4 * 1024 * 1024)
399
+ .min(4 * 1024 * 1024)
400
+ .max(8192)
401
+ }
402
+
403
+ fn finalize_output_file(
404
+ mut writer: std::io::BufWriter<std::fs::File>,
405
+ size: u64,
406
+ dest: &Path,
407
+ ) -> Result<()> {
408
+ std::io::Write::flush(&mut writer).map_err(|e| anyhow::anyhow!("Cannot flush {:?}: {}", dest, e))?;
409
+ let file = writer.into_inner().map_err(|e| anyhow::anyhow!("Cannot finalize {:?}: {}", dest, e.error()))?;
410
+ crate::io_advice::sync_and_drop(&file, size);
411
+ Ok(())
412
+ }
340
413
 
341
- match reader.read(&mut temp) {
342
- Ok(0) => break, Ok(n) => buf.extend_from_slice(&temp[..n]),
343
- Err(e) => return Err(anyhow::anyhow!("Stream read error: {}", e)),
414
+ fn copy_pack_bytes<R: std::io::Read, W: std::io::Write>(
415
+ reader: &mut R,
416
+ writer: &mut W,
417
+ mut remaining: u64,
418
+ bytes_processed: &mut u64,
419
+ file_count: usize,
420
+ processed_files: usize,
421
+ total_expected: u64,
422
+ progress: Option<&(dyn Fn(u64, u64, &str) + Send)>,
423
+ last_pct: &mut u64,
424
+ ) -> Result<()> {
425
+ let mut buf = vec![0u8; 1024 * 1024];
426
+ while remaining > 0 {
427
+ let take = remaining.min(buf.len() as u64) as usize;
428
+ let read = reader.read(&mut buf[..take]).map_err(|e| anyhow::anyhow!("Stream read error: {}", e))?;
429
+ if read == 0 {
430
+ return Err(anyhow::anyhow!("Truncated pack content"));
344
431
  }
432
+ writer.write_all(&buf[..read]).map_err(|e| anyhow::anyhow!("Stream write error: {}", e))?;
433
+ remaining -= read as u64;
434
+ *bytes_processed = bytes_processed.saturating_add(read as u64);
435
+ report_unpack_progress(progress, total_expected, *bytes_processed, file_count, processed_files, last_pct);
345
436
  }
437
+ Ok(())
438
+ }
346
439
 
347
- Ok(written)
440
+ fn discard_pack_bytes<R: std::io::Read>(
441
+ reader: &mut R,
442
+ mut remaining: u64,
443
+ bytes_processed: &mut u64,
444
+ file_count: usize,
445
+ processed_files: usize,
446
+ total_expected: u64,
447
+ progress: Option<&(dyn Fn(u64, u64, &str) + Send)>,
448
+ last_pct: &mut u64,
449
+ ) -> Result<()> {
450
+ let mut buf = vec![0u8; 1024 * 1024];
451
+ while remaining > 0 {
452
+ let take = remaining.min(buf.len() as u64) as usize;
453
+ let read = reader.read(&mut buf[..take]).map_err(|e| anyhow::anyhow!("Stream read error: {}", e))?;
454
+ if read == 0 {
455
+ return Err(anyhow::anyhow!("Truncated pack content"));
456
+ }
457
+ remaining -= read as u64;
458
+ *bytes_processed = bytes_processed.saturating_add(read as u64);
459
+ report_unpack_progress(progress, total_expected, *bytes_processed, file_count, processed_files, last_pct);
460
+ }
461
+ Ok(())
348
462
  }
349
463
 
350
464
  #[cfg(test)]
@@ -353,6 +467,18 @@ mod stream_tests {
353
467
  use std::io::{Write, Read};
354
468
  use std::time::{SystemTime, UNIX_EPOCH};
355
469
 
470
+ struct ChunkedReader<R> {
471
+ inner: R,
472
+ max_chunk: usize,
473
+ }
474
+
475
+ impl<R: Read> Read for ChunkedReader<R> {
476
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
477
+ let limit = buf.len().min(self.max_chunk);
478
+ self.inner.read(&mut buf[..limit])
479
+ }
480
+ }
481
+
356
482
  #[test]
357
483
  fn test_unpack_stream_to_dir() -> Result<()> {
358
484
  let mut parts: Vec<u8> = Vec::new();
@@ -390,7 +516,7 @@ mod stream_tests {
390
516
  let tmpdir = std::env::temp_dir().join(format!("rox_unpack_test_{}", ms));
391
517
  let _ = std::fs::create_dir_all(&tmpdir);
392
518
 
393
- let out = unpack_stream_to_dir(&mut dec2, &tmpdir, None)?;
519
+ let out = unpack_stream_to_dir(&mut dec2, &tmpdir, None, None, 0)?;
394
520
 
395
521
  assert_eq!(out.len(), 2);
396
522
  assert!(tmpdir.join("file1.txt").exists());
@@ -432,7 +558,7 @@ mod stream_tests {
432
558
  let tmpdir = std::env::temp_dir().join(format!("rox_unpack_png_test_{}", ms));
433
559
  let _ = std::fs::create_dir_all(&tmpdir);
434
560
 
435
- let out = unpack_stream_to_dir(&mut dec, &tmpdir, None)?;
561
+ let out = unpack_stream_to_dir(&mut dec, &tmpdir, None, None, 0)?;
436
562
 
437
563
  assert_eq!(out.len(), 2);
438
564
  assert!(tmpdir.join("file1.txt").exists());
@@ -443,5 +569,36 @@ mod stream_tests {
443
569
  let _ = std::fs::remove_dir(&tmpdir);
444
570
  Ok(())
445
571
  }
572
+
573
+ #[test]
574
+ fn test_unpack_stream_to_dir_large_file_small_reads() -> Result<()> {
575
+ let large = vec![0x5a; 2 * 1024 * 1024];
576
+ let mut parts: Vec<u8> = Vec::new();
577
+ parts.extend_from_slice(&0x524f5850u32.to_be_bytes());
578
+ parts.extend_from_slice(&(1u32.to_be_bytes()));
579
+ let name = b"big.bin";
580
+ parts.extend_from_slice(&(name.len() as u16).to_be_bytes());
581
+ parts.extend_from_slice(name);
582
+ parts.extend_from_slice(&(large.len() as u64).to_be_bytes());
583
+ parts.extend_from_slice(&large);
584
+
585
+ let reader = std::io::Cursor::new(parts);
586
+ let mut reader = ChunkedReader { inner: reader, max_chunk: 37 };
587
+
588
+ let ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis();
589
+ let tmpdir = std::env::temp_dir().join(format!("rox_unpack_large_stream_test_{}", ms));
590
+ let _ = std::fs::create_dir_all(&tmpdir);
591
+
592
+ let out = unpack_stream_to_dir(&mut reader, &tmpdir, None, None, large.len() as u64)?;
593
+
594
+ assert_eq!(out, vec!["big.bin".to_string()]);
595
+ let restored = std::fs::read(tmpdir.join("big.bin"))?;
596
+ assert_eq!(restored.len(), large.len());
597
+ assert_eq!(restored, large);
598
+
599
+ let _ = std::fs::remove_file(tmpdir.join("big.bin"));
600
+ let _ = std::fs::remove_dir(&tmpdir);
601
+ Ok(())
602
+ }
446
603
  }
447
604
 
@@ -0,0 +1,146 @@
1
+ use std::io::{self, Write};
2
+
3
+ pub const MAX_PNG_CHUNK_DATA_LEN: usize = 64 * 1024 * 1024;
4
+
5
+ pub fn write_png_chunk<W: Write>(writer: &mut W, chunk_type: &[u8; 4], data: &[u8]) -> anyhow::Result<()> {
6
+ let len = u32::try_from(data.len())
7
+ .map_err(|_| anyhow::anyhow!("chunk too large: {}", data.len()))?;
8
+ writer.write_all(&len.to_be_bytes())?;
9
+ writer.write_all(chunk_type)?;
10
+ writer.write_all(data)?;
11
+
12
+ let mut hasher = crc32fast::Hasher::new();
13
+ hasher.update(chunk_type);
14
+ hasher.update(data);
15
+ writer.write_all(&hasher.finalize().to_be_bytes())?;
16
+ Ok(())
17
+ }
18
+
19
+ pub fn write_chunked_idat_bytes<W: Write>(writer: &mut W, data: &[u8]) -> anyhow::Result<()> {
20
+ write_chunked_idat_bytes_with_limit(writer, data, MAX_PNG_CHUNK_DATA_LEN)
21
+ }
22
+
23
+ fn write_chunked_idat_bytes_with_limit<W: Write>(writer: &mut W, data: &[u8], max_chunk_len: usize) -> anyhow::Result<()> {
24
+ anyhow::ensure!(max_chunk_len > 0, "max_chunk_len must be > 0");
25
+ for chunk in data.chunks(max_chunk_len) {
26
+ write_png_chunk(writer, b"IDAT", chunk)?;
27
+ }
28
+ Ok(())
29
+ }
30
+
31
+ pub struct ChunkedIdatWriter<'a, W: Write> {
32
+ writer: &'a mut W,
33
+ buffer: Vec<u8>,
34
+ max_chunk_len: usize,
35
+ }
36
+
37
+ impl<'a, W: Write> ChunkedIdatWriter<'a, W> {
38
+ pub fn new(writer: &'a mut W) -> Self {
39
+ Self::with_max_chunk_len(writer, MAX_PNG_CHUNK_DATA_LEN)
40
+ }
41
+
42
+ fn with_max_chunk_len(writer: &'a mut W, max_chunk_len: usize) -> Self {
43
+ Self {
44
+ writer,
45
+ buffer: Vec::with_capacity(max_chunk_len.max(1).min(MAX_PNG_CHUNK_DATA_LEN)),
46
+ max_chunk_len: max_chunk_len.max(1),
47
+ }
48
+ }
49
+
50
+ fn flush_chunk(&mut self) -> anyhow::Result<()> {
51
+ if self.buffer.is_empty() {
52
+ return Ok(());
53
+ }
54
+ write_png_chunk(self.writer, b"IDAT", &self.buffer)?;
55
+ self.buffer.clear();
56
+ Ok(())
57
+ }
58
+
59
+ pub fn finish(mut self) -> anyhow::Result<()> {
60
+ self.flush_chunk()
61
+ }
62
+ }
63
+
64
+ impl<W: Write> Write for ChunkedIdatWriter<'_, W> {
65
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
66
+ let mut offset = 0;
67
+ while offset < buf.len() {
68
+ if self.buffer.len() == self.max_chunk_len {
69
+ self.flush_chunk().map_err(io_error)?;
70
+ }
71
+ let space = self.max_chunk_len - self.buffer.len();
72
+ let take = space.min(buf.len() - offset);
73
+ self.buffer.extend_from_slice(&buf[offset..offset + take]);
74
+ offset += take;
75
+ }
76
+ Ok(buf.len())
77
+ }
78
+
79
+ fn flush(&mut self) -> io::Result<()> {
80
+ self.flush_chunk().map_err(io_error)?;
81
+ self.writer.flush()
82
+ }
83
+ }
84
+
85
+ fn io_error(err: anyhow::Error) -> io::Error {
86
+ io::Error::other(err.to_string())
87
+ }
88
+
89
+ #[cfg(test)]
90
+ mod tests {
91
+ use super::*;
92
+
93
+ #[test]
94
+ fn split_large_idat_stream_into_multiple_chunks() {
95
+ let mut png = Vec::new();
96
+ png.extend_from_slice(&[137, 80, 78, 71, 13, 10, 26, 10]);
97
+
98
+ let mut ihdr = [0u8; 13];
99
+ ihdr[0..4].copy_from_slice(&1u32.to_be_bytes());
100
+ ihdr[4..8].copy_from_slice(&1u32.to_be_bytes());
101
+ ihdr[8] = 8;
102
+ ihdr[9] = 2;
103
+
104
+ write_png_chunk(&mut png, b"IHDR", &ihdr).unwrap();
105
+ write_chunked_idat_bytes_with_limit(&mut png, &[1, 2, 3, 4, 5, 6, 7, 8, 9], 4).unwrap();
106
+ write_png_chunk(&mut png, b"IEND", &[]).unwrap();
107
+
108
+ let chunks = crate::png_utils::extract_png_chunks(&png).unwrap();
109
+ let idat_sizes: Vec<usize> = chunks.into_iter()
110
+ .filter(|chunk| chunk.name == "IDAT")
111
+ .map(|chunk| chunk.data.len())
112
+ .collect();
113
+
114
+ assert_eq!(idat_sizes, vec![4, 4, 1]);
115
+ }
116
+
117
+ #[test]
118
+ fn chunked_idat_writer_flushes_multiple_chunks() {
119
+ let mut png = Vec::new();
120
+ png.extend_from_slice(&[137, 80, 78, 71, 13, 10, 26, 10]);
121
+
122
+ let mut ihdr = [0u8; 13];
123
+ ihdr[0..4].copy_from_slice(&1u32.to_be_bytes());
124
+ ihdr[4..8].copy_from_slice(&1u32.to_be_bytes());
125
+ ihdr[8] = 8;
126
+ ihdr[9] = 2;
127
+
128
+ write_png_chunk(&mut png, b"IHDR", &ihdr).unwrap();
129
+ {
130
+ let mut writer = ChunkedIdatWriter::with_max_chunk_len(&mut png, 3);
131
+ writer.write_all(&[1, 2]).unwrap();
132
+ writer.write_all(&[3, 4, 5]).unwrap();
133
+ writer.write_all(&[6, 7]).unwrap();
134
+ writer.finish().unwrap();
135
+ }
136
+ write_png_chunk(&mut png, b"IEND", &[]).unwrap();
137
+
138
+ let chunks = crate::png_utils::extract_png_chunks(&png).unwrap();
139
+ let idat_sizes: Vec<usize> = chunks.into_iter()
140
+ .filter(|chunk| chunk.name == "IDAT")
141
+ .map(|chunk| chunk.data.len())
142
+ .collect();
143
+
144
+ assert_eq!(idat_sizes, vec![3, 3, 1]);
145
+ }
146
+ }
@@ -7,6 +7,8 @@ use std::io::{Cursor, Read, Seek, SeekFrom};
7
7
  struct PngSignature([u8; 8]);
8
8
 
9
9
  const PNG_SIG: PngSignature = PngSignature([137, 80, 78, 71, 13, 10, 26, 10]);
10
+ const HEADER_VERSION_V1: u8 = 1;
11
+ const HEADER_VERSION_V2: u8 = 2;
10
12
 
11
13
  #[derive(Debug, Clone)]
12
14
  pub struct PngChunk {
@@ -367,21 +369,10 @@ fn extract_payload_from_embedded_nn(png_data: &[u8]) -> Result<Vec<u8>, String>
367
369
  }
368
370
  found.ok_or("PXL1 not found in reconstructed pixels")?
369
371
  };
370
- let mut idx = pos + 4;
371
- if idx + 2 > logical_rgb.len() { return Err("Truncated header in embedded NN".to_string()); }
372
- let _version = logical_rgb[idx]; idx += 1;
373
- let name_len = logical_rgb[idx] as usize; idx += 1;
374
- if idx + name_len > logical_rgb.len() { return Err("Truncated name in embedded NN".to_string()); }
375
- idx += name_len;
376
- if idx + 4 > logical_rgb.len() { return Err("Truncated payload length in embedded NN".to_string()); }
377
- let payload_len = ((logical_rgb[idx] as u32) << 24)
378
- | ((logical_rgb[idx+1] as u32) << 16)
379
- | ((logical_rgb[idx+2] as u32) << 8)
380
- | (logical_rgb[idx+3] as u32);
381
- idx += 4;
382
- let end = idx + (payload_len as usize);
372
+ let header = parse_pixel_payload_header(&logical_rgb, pos)?;
373
+ let end = header.payload_offset + header.payload_len;
383
374
  if end > logical_rgb.len() { return Err("Truncated payload in embedded NN".to_string()); }
384
- Ok(logical_rgb[idx..end].to_vec())
375
+ Ok(logical_rgb[header.payload_offset..end].to_vec())
385
376
  }
386
377
 
387
378
  pub fn extract_name_from_png(png_data: &[u8]) -> Option<String> {
@@ -420,6 +411,64 @@ fn extract_name_direct(png_data: &[u8]) -> Option<String> {
420
411
  String::from_utf8(raw[idx..idx + name_len].to_vec()).ok()
421
412
  }
422
413
 
414
+ struct PixelPayloadHeader {
415
+ payload_offset: usize,
416
+ payload_len: usize,
417
+ }
418
+
419
+ fn parse_pixel_payload_header(buf: &[u8], pos: usize) -> Result<PixelPayloadHeader, String> {
420
+ let mut idx = pos + 4;
421
+ if idx + 2 > buf.len() {
422
+ return Err("Truncated header".to_string());
423
+ }
424
+
425
+ let version = buf[idx];
426
+ idx += 1;
427
+ let name_len = buf[idx] as usize;
428
+ idx += 1;
429
+ if idx + name_len > buf.len() {
430
+ return Err("Truncated name".to_string());
431
+ }
432
+ idx += name_len;
433
+
434
+ let payload_len = match version {
435
+ HEADER_VERSION_V1 => {
436
+ if idx + 4 > buf.len() {
437
+ return Err("Truncated payload length".to_string());
438
+ }
439
+ let len = u32::from_be_bytes([buf[idx], buf[idx + 1], buf[idx + 2], buf[idx + 3]]) as u64;
440
+ idx += 4;
441
+ len
442
+ }
443
+ HEADER_VERSION_V2 => {
444
+ if idx + 8 > buf.len() {
445
+ return Err("Truncated payload length64".to_string());
446
+ }
447
+ let len = u64::from_be_bytes([
448
+ buf[idx],
449
+ buf[idx + 1],
450
+ buf[idx + 2],
451
+ buf[idx + 3],
452
+ buf[idx + 4],
453
+ buf[idx + 5],
454
+ buf[idx + 6],
455
+ buf[idx + 7],
456
+ ]);
457
+ idx += 8;
458
+ len
459
+ }
460
+ other => return Err(format!("Unsupported header version {}", other)),
461
+ };
462
+
463
+ let payload_len = usize::try_from(payload_len)
464
+ .map_err(|_| "Payload too large for this platform".to_string())?;
465
+
466
+ Ok(PixelPayloadHeader {
467
+ payload_offset: idx,
468
+ payload_len,
469
+ })
470
+ }
471
+
423
472
  fn extract_name_from_embedded_nn(png_data: &[u8]) -> Result<String, String> {
424
473
  let (pixels, width, height) = decode_to_rgba_grid(png_data)?;
425
474
  let logical_rgb = reconstruct_logical_pixels_from_nn(&pixels, width, height)?;
@@ -435,21 +484,10 @@ fn extract_name_from_embedded_nn(png_data: &[u8]) -> Result<String, String> {
435
484
  fn extract_payload_direct(png_data: &[u8]) -> Result<Vec<u8>, String> {
436
485
  let raw = decode_to_rgb(png_data)?;
437
486
  let pos = find_pixel_header(&raw)?;
438
- let mut idx = pos + 4;
439
- if idx + 2 > raw.len() { return Err("Truncated header".to_string()); }
440
- let _version = raw[idx]; idx += 1;
441
- let name_len = raw[idx] as usize; idx += 1;
442
- if idx + name_len > raw.len() { return Err("Truncated name".to_string()); }
443
- idx += name_len;
444
- if idx + 4 > raw.len() { return Err("Truncated payload length".to_string()); }
445
- let payload_len = ((raw[idx] as u32) << 24)
446
- | ((raw[idx+1] as u32) << 16)
447
- | ((raw[idx+2] as u32) << 8)
448
- | (raw[idx+3] as u32);
449
- idx += 4;
450
- let end = idx + (payload_len as usize);
487
+ let header = parse_pixel_payload_header(&raw, pos)?;
488
+ let end = header.payload_offset + header.payload_len;
451
489
  if end > raw.len() { return Err("Truncated payload".to_string()); }
452
- let payload = raw[idx..end].to_vec();
490
+ let payload = raw[header.payload_offset..end].to_vec();
453
491
  Ok(payload)
454
492
  }
455
493
 
@@ -482,19 +520,8 @@ fn extract_file_list_from_embedded_nn(png_data: &[u8]) -> Result<String, String>
482
520
  let (pixels, width, height) = decode_to_rgba_grid(png_data)?;
483
521
  let logical_rgb = reconstruct_logical_pixels_from_nn(&pixels, width, height)?;
484
522
  let pos = find_pixel_header(&logical_rgb)?;
485
- let mut idx = pos + 4;
486
- if idx + 2 > logical_rgb.len() { return Err("Truncated".to_string()); }
487
- idx += 1;
488
- let name_len = logical_rgb[idx] as usize; idx += 1;
489
- if idx + name_len > logical_rgb.len() { return Err("Truncated".to_string()); }
490
- idx += name_len;
491
- if idx + 4 > logical_rgb.len() { return Err("Truncated".to_string()); }
492
- let payload_len = ((logical_rgb[idx] as u32) << 24)
493
- | ((logical_rgb[idx+1] as u32) << 16)
494
- | ((logical_rgb[idx+2] as u32) << 8)
495
- | (logical_rgb[idx+3] as u32);
496
- idx += 4;
497
- idx += payload_len as usize;
523
+ let header = parse_pixel_payload_header(&logical_rgb, pos)?;
524
+ let mut idx = header.payload_offset + header.payload_len;
498
525
  if idx + 8 > logical_rgb.len() { return Err("No file list in embedded NN".to_string()); }
499
526
  if &logical_rgb[idx..idx + 4] != b"rXFL" { return Err("No rXFL marker in embedded NN".to_string()); }
500
527
  idx += 4;
@@ -511,19 +538,8 @@ fn extract_file_list_from_embedded_nn(png_data: &[u8]) -> Result<String, String>
511
538
  fn extract_file_list_direct(png_data: &[u8]) -> Result<String, String> {
512
539
  let raw = decode_to_rgb(png_data)?;
513
540
  let pos = find_pixel_header(&raw)?;
514
- let mut idx = pos + 4;
515
- if idx + 2 > raw.len() { return Err("Truncated header".to_string()); }
516
- idx += 1;
517
- let name_len = raw[idx] as usize; idx += 1;
518
- if idx + name_len > raw.len() { return Err("Truncated name".to_string()); }
519
- idx += name_len;
520
- if idx + 4 > raw.len() { return Err("Truncated payload length".to_string()); }
521
- let payload_len = ((raw[idx] as u32) << 24)
522
- | ((raw[idx+1] as u32) << 16)
523
- | ((raw[idx+2] as u32) << 8)
524
- | (raw[idx+3] as u32);
525
- idx += 4;
526
- idx += payload_len as usize;
541
+ let header = parse_pixel_payload_header(&raw, pos)?;
542
+ let mut idx = header.payload_offset + header.payload_len;
527
543
  if idx + 8 > raw.len() { return Err("No file list in pixel data".to_string()); }
528
544
  if &raw[idx..idx + 4] != b"rXFL" { return Err("No rXFL marker in pixel data".to_string()); }
529
545
  idx += 4;