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/Cargo.toml +3 -1
- package/README.md +26 -13
- 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.js +62 -58
- package/dist/utils/encoder.js +13 -6
- package/native/encoder.rs +10 -22
- package/native/io_advice.rs +43 -0
- package/native/lib.rs +2 -0
- package/native/main.rs +81 -43
- package/native/packer.rs +232 -75
- 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 +313 -114
- package/native/streaming_encode.rs +272 -128
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
295
|
-
let
|
|
296
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
let content = &buf[content_start..content_end];
|
|
360
|
+
Ok(written)
|
|
361
|
+
}
|
|
313
362
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
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
|
+
}
|
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;
|