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/Cargo.toml +2 -1
- package/README.md +30 -20
- package/dist/cli.js +36 -194
- package/dist/rox-macos-universal +0 -0
- package/dist/roxify_native +0 -0
- package/dist/roxify_native-macos-arm64 +0 -0
- package/dist/roxify_native-macos-x64 +0 -0
- package/dist/roxify_native.exe +0 -0
- package/dist/utils/decoder.d.ts +1 -21
- package/dist/utils/decoder.js +42 -1241
- package/dist/utils/encoder.d.ts +1 -13
- package/dist/utils/encoder.js +36 -559
- package/dist/utils/rust-cli-wrapper.d.ts +2 -2
- package/dist/utils/rust-cli-wrapper.js +8 -2
- package/native/encoder.rs +10 -22
- package/native/io_advice.rs +43 -0
- package/native/io_ntfs_optimized.rs +99 -0
- package/native/lib.rs +2 -0
- package/native/main.rs +329 -57
- package/native/packer.rs +188 -82
- package/native/png_chunk_writer.rs +146 -0
- package/native/png_utils.rs +70 -54
- package/native/streaming.rs +16 -39
- package/native/streaming_decode.rs +263 -110
- package/native/streaming_encode.rs +36 -59
- package/package.json +1 -1
- package/roxify_native-aarch64-apple-darwin.node +0 -0
- package/roxify_native-aarch64-pc-windows-msvc.node +0 -0
- package/roxify_native-aarch64-unknown-linux-gnu.node +0 -0
- package/roxify_native-i686-pc-windows-msvc.node +0 -0
- package/roxify_native-i686-unknown-linux-gnu.node +0 -0
- package/roxify_native-x86_64-apple-darwin.node +0 -0
- package/roxify_native-x86_64-pc-windows-msvc.node +0 -0
- package/roxify_native-x86_64-unknown-linux-gnu.node +0 -0
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
354
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
+
}
|
package/native/png_utils.rs
CHANGED
|
@@ -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
|
|
371
|
-
|
|
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[
|
|
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
|
|
439
|
-
|
|
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[
|
|
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
|
|
486
|
-
|
|
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
|
|
515
|
-
|
|
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;
|