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.
package/native/packer.rs CHANGED
@@ -290,9 +290,6 @@ pub fn unpack_stream_to_dir<R: std::io::Read>(
290
290
  total_expected: u64,
291
291
  ) -> Result<Vec<String>> {
292
292
  let mut written = Vec::new();
293
- let mut buf: Vec<u8> = Vec::new();
294
- let mut pos: usize = 0;
295
- let mut temp = [0u8; 64 * 1024];
296
293
  let files_filter: Option<std::collections::HashSet<String>> = files_opt.map(|l| l.iter().map(|s| s.clone()).collect());
297
294
  let mut requested = files_filter.as_ref().map(|s| s.len()).unwrap_or(usize::MAX);
298
295
  let mut file_count = 0usize;
@@ -300,94 +297,59 @@ pub fn unpack_stream_to_dir<R: std::io::Read>(
300
297
  let mut bytes_processed = 0u64;
301
298
  let mut last_pct = 10u64;
302
299
 
303
- let mut header_parsed = false;
304
- let debug = std::env::var("ROX_DEBUG").is_ok();
305
- if debug { eprintln!("[rox debug] unpack_stream_to_dir called (out_dir={:?})", out_dir); }
306
-
307
- loop {
308
- loop {
309
- if !header_parsed {
310
- if pos + 8 > buf.len() { break; }
311
- if debug {
312
- eprintln!("[rox debug] buf.len={} pos={} first16={:?}", buf.len(), pos, &buf[0..std::cmp::min(16, buf.len())]);
313
- eprintln!("[rox debug] after first debug");
314
- }
315
- if debug { eprintln!("[rox debug] before reading magic_header"); }
316
- let magic_header = u32::from_be_bytes(buf[pos..pos+4].try_into().unwrap());
317
- if debug { eprintln!("[rox debug] magic_header=0x{:08x}", magic_header); }
318
- if magic_header == 0x524f5850u32 {
319
- pos += 4;
320
- file_count = u32::from_be_bytes(buf[pos..pos+4].try_into().unwrap()) as usize;
321
- pos += 4;
322
- header_parsed = true;
323
- if debug { eprintln!("[rox debug] header parsed, file_count={}", file_count); }
324
- } else if magic_header == 0x524f5831u32 {
325
- if debug { eprintln!("[rox debug] found ROX1 outer magic, skipping 4 bytes"); }
326
- pos += 4;
327
- continue; } else {
328
- }
329
- }
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
+ }
330
312
 
331
- if pos + 8 > buf.len() { break; }
332
- let magic = u32::from_be_bytes(buf[pos..pos+4].try_into().unwrap());
333
- if magic == 0x524f5849u32 {
334
- if pos + 8 > buf.len() { break; }
335
- let index_len = u32::from_be_bytes(buf[pos+4..pos+8].try_into().unwrap()) as usize;
336
- if pos + 8 + index_len > buf.len() { break; }
337
- pos += 8 + index_len;
338
- }
313
+ file_count = read_pack_u32(reader)? as usize;
339
314
 
340
- if pos + 2 > buf.len() { break; }
341
- let name_len = u16::from_be_bytes(buf[pos..pos+2].try_into().unwrap()) as usize;
342
- if pos + 2 + name_len + 8 > buf.len() { break; }
343
- let name = String::from_utf8_lossy(&buf[pos+2..pos+2+name_len]).to_string();
344
- let size = u64::from_be_bytes(buf[pos+2+name_len..pos+2+name_len+8].try_into().unwrap()) as usize;
345
- if pos + 2 + name_len + 8 + size > buf.len() { break; }
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)?;
346
321
 
347
- let content_start = pos + 2 + name_len + 8;
348
- let content_end = content_start + size;
349
- let content = &buf[content_start..content_end];
350
- processed_files = processed_files.saturating_add(1);
351
- bytes_processed = bytes_processed.saturating_add(size as u64);
322
+ let should_write = match &files_filter {
323
+ Some(set) => set.contains(&name),
324
+ None => true,
325
+ };
352
326
 
353
- let p = Path::new(&name);
354
- let mut safe = std::path::PathBuf::new();
355
- for comp in p.components() {
356
- if let std::path::Component::Normal(osstr) = comp {
357
- safe.push(osstr);
358
- }
359
- }
327
+ if should_write {
328
+ let safe = sanitize_pack_path(&name);
360
329
  let dest = out_dir.join(&safe);
361
-
362
- if files_filter.is_none() || files_filter.as_ref().map_or(false, |s| s.contains(&name)) {
363
- if let Some(parent) = dest.parent() {
364
- std::fs::create_dir_all(parent).map_err(|e| anyhow::anyhow!("Cannot create parent dir {:?}: {}", parent, e))?;
365
- }
366
- std::fs::write(&dest, content).map_err(|e| anyhow::anyhow!("Cannot write {:?}: {}", dest, e))?;
367
- written.push(safe.to_string_lossy().to_string());
368
- if let Some(_set) = files_filter.as_ref() {
369
- requested = requested.saturating_sub(1);
370
- report_unpack_progress(progress, total_expected, bytes_processed, file_count, processed_files, &mut last_pct);
371
- if requested == 0 {
372
- if let Some(cb) = progress {
373
- cb(99, 100, "finishing");
374
- }
375
- return Ok(written);
376
- }
377
- }
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))?;
378
332
  }
379
-
380
- report_unpack_progress(progress, total_expected, bytes_processed, file_count, processed_files, &mut last_pct);
381
-
382
- pos = content_end; if pos > 0 {
383
- buf.drain(0..pos);
384
- pos = 0;
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);
385
340
  }
341
+ } else {
342
+ discard_pack_bytes(reader, size, &mut bytes_processed, file_count, processed_files, total_expected, progress, &mut last_pct)?;
386
343
  }
387
344
 
388
- match reader.read(&mut temp) {
389
- Ok(0) => break, Ok(n) => buf.extend_from_slice(&temp[..n]),
390
- Err(e) => return Err(anyhow::anyhow!("Stream read error: {}", e)),
345
+ processed_files = processed_files.saturating_add(1);
346
+ report_unpack_progress(progress, total_expected, bytes_processed, file_count, processed_files, &mut last_pct);
347
+
348
+ if requested == 0 {
349
+ if let Some(cb) = progress {
350
+ cb(99, 100, "finishing");
351
+ }
352
+ return Ok(written);
391
353
  }
392
354
  }
393
355
 
@@ -398,12 +360,125 @@ pub fn unpack_stream_to_dir<R: std::io::Read>(
398
360
  Ok(written)
399
361
  }
400
362
 
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
+ }
366
+
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
+ }
372
+
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);
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
+ }
413
+
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"));
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);
436
+ }
437
+ Ok(())
438
+ }
439
+
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(())
462
+ }
463
+
401
464
  #[cfg(test)]
402
465
  mod stream_tests {
403
466
  use super::*;
404
467
  use std::io::{Write, Read};
405
468
  use std::time::{SystemTime, UNIX_EPOCH};
406
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
+
407
482
  #[test]
408
483
  fn test_unpack_stream_to_dir() -> Result<()> {
409
484
  let mut parts: Vec<u8> = Vec::new();
@@ -494,5 +569,36 @@ mod stream_tests {
494
569
  let _ = std::fs::remove_dir(&tmpdir);
495
570
  Ok(())
496
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
+ }
497
603
  }
498
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;