roxify 1.13.2 → 1.13.4

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/lib.rs CHANGED
@@ -69,51 +69,51 @@ pub fn native_adler32(buffer: Buffer) -> u32 {
69
69
 
70
70
  #[cfg(not(test))]
71
71
  #[napi]
72
- pub fn native_delta_encode(buffer: Buffer) -> Vec<u8> {
73
- core::delta_encode_bytes(&buffer)
72
+ pub fn native_delta_encode(buffer: Buffer) -> Buffer {
73
+ core::delta_encode_bytes(&buffer).into()
74
74
  }
75
75
 
76
76
  #[cfg(not(test))]
77
77
  #[napi]
78
- pub fn native_delta_decode(buffer: Buffer) -> Vec<u8> {
79
- core::delta_decode_bytes(&buffer)
78
+ pub fn native_delta_decode(buffer: Buffer) -> Buffer {
79
+ core::delta_decode_bytes(&buffer).into()
80
80
  }
81
81
 
82
82
  #[cfg(not(test))]
83
83
  #[napi]
84
- pub fn native_zstd_compress(buffer: Buffer, level: i32) -> Result<Vec<u8>> {
85
- core::zstd_compress_bytes(&buffer, level, None).map_err(|e| Error::from_reason(e))
84
+ pub fn native_zstd_compress(buffer: Buffer, level: i32) -> Result<Buffer> {
85
+ core::zstd_compress_bytes(&buffer, level, None).map(Buffer::from).map_err(|e| Error::from_reason(e))
86
86
  }
87
87
 
88
88
  #[cfg(not(test))]
89
89
  #[napi]
90
- pub fn native_zstd_compress_with_dict(buffer: Buffer, level: i32, dict: Buffer) -> Result<Vec<u8>> {
90
+ pub fn native_zstd_compress_with_dict(buffer: Buffer, level: i32, dict: Buffer) -> Result<Buffer> {
91
91
  let dict_slice: &[u8] = &dict;
92
- core::zstd_compress_bytes(&buffer, level, Some(dict_slice)).map_err(|e| Error::from_reason(e))
92
+ core::zstd_compress_bytes(&buffer, level, Some(dict_slice)).map(Buffer::from).map_err(|e| Error::from_reason(e))
93
93
  }
94
94
 
95
95
  #[cfg(not(test))]
96
96
  #[napi]
97
- pub fn native_zstd_decompress(buffer: Buffer) -> Result<Vec<u8>> {
98
- core::zstd_decompress_bytes(&buffer, None).map_err(|e| Error::from_reason(e))
97
+ pub fn native_zstd_decompress(buffer: Buffer) -> Result<Buffer> {
98
+ core::zstd_decompress_bytes(&buffer, None).map(Buffer::from).map_err(|e| Error::from_reason(e))
99
99
  }
100
100
 
101
101
  #[cfg(not(test))]
102
102
  #[napi]
103
- pub fn native_zstd_decompress_with_dict(buffer: Buffer, dict: Buffer) -> Result<Vec<u8>> {
103
+ pub fn native_zstd_decompress_with_dict(buffer: Buffer, dict: Buffer) -> Result<Buffer> {
104
104
  let dict_slice: &[u8] = &dict;
105
- core::zstd_decompress_bytes(&buffer, Some(dict_slice)).map_err(|e| Error::from_reason(e))
105
+ core::zstd_decompress_bytes(&buffer, Some(dict_slice)).map(Buffer::from).map_err(|e| Error::from_reason(e))
106
106
  }
107
107
 
108
108
  #[cfg(not(test))]
109
109
  #[napi]
110
- pub fn bwt_transform(buffer: Buffer) -> Result<Vec<u8>> {
110
+ pub fn bwt_transform(buffer: Buffer) -> Result<Buffer> {
111
111
  match bwt::bwt_encode(&buffer) {
112
112
  Ok(result) => {
113
113
  let mut output = Vec::with_capacity(4 + result.transformed.len());
114
114
  output.extend_from_slice(&result.primary_index.to_le_bytes());
115
115
  output.extend_from_slice(&result.transformed);
116
- Ok(output)
116
+ Ok(output.into())
117
117
  }
118
118
  Err(e) => Err(Error::from_reason(e.to_string())),
119
119
  }
@@ -191,15 +191,17 @@ mod tests {
191
191
 
192
192
  #[cfg(not(test))]
193
193
  #[napi]
194
- pub fn native_encode_png(buffer: Buffer, compression_level: i32) -> Result<Vec<u8>> {
194
+ pub fn native_encode_png(buffer: Buffer, compression_level: i32) -> Result<Buffer> {
195
195
  encoder::encode_to_png(&buffer, compression_level)
196
+ .map(Buffer::from)
196
197
  .map_err(|e| Error::from_reason(e.to_string()))
197
198
  }
198
199
 
199
200
  #[cfg(not(test))]
200
201
  #[napi]
201
- pub fn native_encode_png_raw(buffer: Buffer, compression_level: i32) -> Result<Vec<u8>> {
202
+ pub fn native_encode_png_raw(buffer: Buffer, compression_level: i32) -> Result<Buffer> {
202
203
  encoder::encode_to_png_raw(&buffer, compression_level)
204
+ .map(Buffer::from)
203
205
  .map_err(|e| Error::from_reason(e.to_string()))
204
206
  }
205
207
 
@@ -210,13 +212,14 @@ pub fn native_encode_png_with_name_and_filelist(
210
212
  compression_level: i32,
211
213
  name: Option<String>,
212
214
  file_list_json: Option<String>,
213
- ) -> Result<Vec<u8>> {
215
+ ) -> Result<Buffer> {
214
216
  encoder::encode_to_png_with_name_and_filelist(
215
217
  &buffer,
216
218
  compression_level,
217
219
  name.as_deref(),
218
220
  file_list_json.as_deref(),
219
221
  )
222
+ .map(Buffer::from)
220
223
  .map_err(|e| Error::from_reason(e.to_string()))
221
224
  }
222
225
 
@@ -229,7 +232,7 @@ pub fn native_encode_png_with_encryption_name_and_filelist(
229
232
  encrypt_type: Option<String>,
230
233
  name: Option<String>,
231
234
  file_list_json: Option<String>,
232
- ) -> Result<Vec<u8>> {
235
+ ) -> Result<Buffer> {
233
236
  encoder::encode_to_png_with_encryption_name_and_filelist(
234
237
  &buffer,
235
238
  compression_level,
@@ -238,13 +241,14 @@ pub fn native_encode_png_with_encryption_name_and_filelist(
238
241
  name.as_deref(),
239
242
  file_list_json.as_deref(),
240
243
  )
244
+ .map(Buffer::from)
241
245
  .map_err(|e| Error::from_reason(e.to_string()))
242
246
  }
243
247
 
244
248
  #[napi(object)]
245
249
  pub struct PngChunkData {
246
250
  pub name: String,
247
- pub data: Vec<u8>,
251
+ pub data: Buffer,
248
252
  }
249
253
 
250
254
  #[cfg(not(test))]
@@ -255,21 +259,22 @@ pub fn extract_png_chunks(png_buffer: Buffer) -> Result<Vec<PngChunkData>> {
255
259
 
256
260
  Ok(chunks.into_iter().map(|c| PngChunkData {
257
261
  name: c.name,
258
- data: c.data,
262
+ data: c.data.into(),
259
263
  }).collect())
260
264
  }
261
265
 
262
266
  #[cfg(not(test))]
263
267
  #[napi]
264
- pub fn encode_png_chunks(chunks: Vec<PngChunkData>) -> Result<Vec<u8>> {
268
+ pub fn encode_png_chunks(chunks: Vec<PngChunkData>) -> Result<Buffer> {
265
269
  let native_chunks: Vec<png_utils::PngChunk> = chunks.into_iter()
266
270
  .map(|c| png_utils::PngChunk {
267
271
  name: c.name,
268
- data: c.data,
272
+ data: c.data.to_vec(),
269
273
  })
270
274
  .collect();
271
275
 
272
276
  png_utils::encode_png_chunks(&native_chunks)
277
+ .map(Buffer::from)
273
278
  .map_err(|e| Error::from_reason(e))
274
279
  }
275
280
 
@@ -309,22 +314,23 @@ pub fn sharp_resize_image(
309
314
  width: u32,
310
315
  height: u32,
311
316
  kernel: String,
312
- ) -> Result<Vec<u8>> {
317
+ ) -> Result<Buffer> {
313
318
  image_utils::sharp_resize(&input_buffer, width, height, &kernel)
319
+ .map(Buffer::from)
314
320
  .map_err(|e| Error::from_reason(e))
315
321
  }
316
322
 
317
323
  #[cfg(not(test))]
318
324
  #[napi]
319
- pub fn sharp_raw_pixels(input_buffer: Buffer) -> Result<Vec<u8>> {
325
+ pub fn sharp_raw_pixels(input_buffer: Buffer) -> Result<Buffer> {
320
326
  let (pixels, _w, _h) = image_utils::sharp_raw_pixels(&input_buffer)
321
327
  .map_err(|e| Error::from_reason(e))?;
322
- Ok(pixels)
328
+ Ok(pixels.into())
323
329
  }
324
330
 
325
331
  #[napi(object)]
326
332
  pub struct RawPixelsWithDimensions {
327
- pub pixels: Vec<u8>,
333
+ pub pixels: Buffer,
328
334
  pub width: u32,
329
335
  pub height: u32,
330
336
  }
@@ -334,7 +340,7 @@ pub struct RawPixelsWithDimensions {
334
340
  pub fn sharp_to_raw(input_buffer: Buffer) -> Result<RawPixelsWithDimensions> {
335
341
  let (pixels, width, height) = image_utils::sharp_raw_pixels(&input_buffer)
336
342
  .map_err(|e| Error::from_reason(e))?;
337
- Ok(RawPixelsWithDimensions { pixels, width, height })
343
+ Ok(RawPixelsWithDimensions { pixels: pixels.into(), width, height })
338
344
  }
339
345
 
340
346
  #[cfg(not(test))]
@@ -347,8 +353,9 @@ pub fn sharp_metadata(input_buffer: Buffer) -> Result<SharpMetadata> {
347
353
 
348
354
  #[cfg(not(test))]
349
355
  #[napi]
350
- pub fn rgb_to_png(rgb_buffer: Buffer, width: u32, height: u32) -> Result<Vec<u8>> {
356
+ pub fn rgb_to_png(rgb_buffer: Buffer, width: u32, height: u32) -> Result<Buffer> {
351
357
  image_utils::rgb_to_png(&rgb_buffer, width, height)
358
+ .map(Buffer::from)
352
359
  .map_err(|e| Error::from_reason(e))
353
360
  }
354
361
 
@@ -357,27 +364,30 @@ pub fn rgb_to_png(rgb_buffer: Buffer, width: u32, height: u32) -> Result<Vec<u8>
357
364
  pub fn png_to_rgb(png_buffer: Buffer) -> Result<RawPixelsWithDimensions> {
358
365
  let (pixels, width, height) = image_utils::png_to_rgb(&png_buffer)
359
366
  .map_err(|e| Error::from_reason(e))?;
360
- Ok(RawPixelsWithDimensions { pixels, width, height })
367
+ Ok(RawPixelsWithDimensions { pixels: pixels.into(), width, height })
361
368
  }
362
369
 
363
370
  #[cfg(not(test))]
364
371
  #[napi]
365
- pub fn crop_and_reconstitute(png_buffer: Buffer) -> Result<Vec<u8>> {
372
+ pub fn crop_and_reconstitute(png_buffer: Buffer) -> Result<Buffer> {
366
373
  reconstitution::crop_and_reconstitute(&png_buffer)
374
+ .map(Buffer::from)
367
375
  .map_err(|e| Error::from_reason(e))
368
376
  }
369
377
 
370
378
  #[cfg(not(test))]
371
379
  #[napi]
372
- pub fn unstretch_nn(png_buffer: Buffer) -> Result<Vec<u8>> {
380
+ pub fn unstretch_nn(png_buffer: Buffer) -> Result<Buffer> {
373
381
  reconstitution::unstretch_nn(&png_buffer)
382
+ .map(Buffer::from)
374
383
  .map_err(|e| Error::from_reason(e))
375
384
  }
376
385
 
377
386
  #[cfg(not(test))]
378
387
  #[napi]
379
- pub fn extract_payload_from_png(png_buffer: Buffer) -> Result<Vec<u8>> {
388
+ pub fn extract_payload_from_png(png_buffer: Buffer) -> Result<Buffer> {
380
389
  png_utils::extract_payload_from_png(&png_buffer)
390
+ .map(Buffer::from)
381
391
  .map_err(|e| Error::from_reason(e))
382
392
  }
383
393
 
@@ -392,8 +402,9 @@ pub fn extract_file_list_from_pixels(png_buffer: Buffer) -> Result<String> {
392
402
 
393
403
  #[cfg(not(test))]
394
404
  #[napi]
395
- pub fn native_encode_wav(buffer: Buffer, compression_level: i32) -> Result<Vec<u8>> {
405
+ pub fn native_encode_wav(buffer: Buffer, compression_level: i32) -> Result<Buffer> {
396
406
  encoder::encode_to_wav(&buffer, compression_level)
407
+ .map(Buffer::from)
397
408
  .map_err(|e| Error::from_reason(e.to_string()))
398
409
  }
399
410
 
@@ -404,13 +415,14 @@ pub fn native_encode_wav_with_name_and_filelist(
404
415
  compression_level: i32,
405
416
  name: Option<String>,
406
417
  file_list_json: Option<String>,
407
- ) -> Result<Vec<u8>> {
418
+ ) -> Result<Buffer> {
408
419
  encoder::encode_to_wav_with_name_and_filelist(
409
420
  &buffer,
410
421
  compression_level,
411
422
  name.as_deref(),
412
423
  file_list_json.as_deref(),
413
424
  )
425
+ .map(Buffer::from)
414
426
  .map_err(|e| Error::from_reason(e.to_string()))
415
427
  }
416
428
 
@@ -423,7 +435,7 @@ pub fn native_encode_wav_with_encryption_name_and_filelist(
423
435
  encrypt_type: Option<String>,
424
436
  name: Option<String>,
425
437
  file_list_json: Option<String>,
426
- ) -> Result<Vec<u8>> {
438
+ ) -> Result<Buffer> {
427
439
  encoder::encode_to_wav_with_encryption_name_and_filelist(
428
440
  &buffer,
429
441
  compression_level,
@@ -432,26 +444,29 @@ pub fn native_encode_wav_with_encryption_name_and_filelist(
432
444
  name.as_deref(),
433
445
  file_list_json.as_deref(),
434
446
  )
447
+ .map(Buffer::from)
435
448
  .map_err(|e| Error::from_reason(e.to_string()))
436
449
  }
437
450
 
438
451
  #[cfg(not(test))]
439
452
  #[napi]
440
- pub fn native_decode_wav_payload(wav_buffer: Buffer) -> Result<Vec<u8>> {
453
+ pub fn native_decode_wav_payload(wav_buffer: Buffer) -> Result<Buffer> {
441
454
  encoder::decode_wav_payload(&wav_buffer)
455
+ .map(Buffer::from)
442
456
  .map_err(|e| Error::from_reason(e.to_string()))
443
457
  }
444
458
 
445
459
  #[cfg(not(test))]
446
460
  #[napi]
447
- pub fn native_bytes_to_wav(buffer: Buffer) -> Vec<u8> {
448
- audio::bytes_to_wav(&buffer)
461
+ pub fn native_bytes_to_wav(buffer: Buffer) -> Buffer {
462
+ audio::bytes_to_wav(&buffer).into()
449
463
  }
450
464
 
451
465
  #[cfg(not(test))]
452
466
  #[napi]
453
- pub fn native_wav_to_bytes(wav_buffer: Buffer) -> Result<Vec<u8>> {
467
+ pub fn native_wav_to_bytes(wav_buffer: Buffer) -> Result<Buffer> {
454
468
  audio::wav_to_bytes(&wav_buffer)
469
+ .map(Buffer::from)
455
470
  .map_err(|e| Error::from_reason(e))
456
471
  }
457
472
 
package/native/main.rs CHANGED
@@ -144,6 +144,19 @@ fn parse_markers(v: &[String]) -> Option<Vec<u8>> {
144
144
 
145
145
  fn main() -> anyhow::Result<()> {
146
146
  let cli = Cli::parse();
147
+
148
+ fn parse_requested_files(files: &str) -> anyhow::Result<Vec<String>> {
149
+ if files.trim_start().starts_with('[') {
150
+ serde_json::from_str::<Vec<String>>(files)
151
+ .map_err(|e| anyhow::anyhow!("Invalid JSON for --files: {}", e))
152
+ } else {
153
+ Ok(files
154
+ .split(',')
155
+ .map(|file| file.trim().to_string())
156
+ .filter(|file| !file.is_empty())
157
+ .collect())
158
+ }
159
+ }
147
160
  match cli.command {
148
161
  Commands::TrainDict { samples, size, output } => {
149
162
  let dict = core::train_zstd_dictionary(&samples, size)?;
@@ -169,7 +182,7 @@ fn main() -> anyhow::Result<()> {
169
182
  eprintln!("PROGRESS:{}:{}:{}", current, total, step);
170
183
  })),
171
184
  )?;
172
- println!("(TAR archive, rXFL chunk embedded)");
185
+ println!("(directory payload, rXFL chunk embedded)");
173
186
  return Ok(());
174
187
  }
175
188
 
@@ -234,7 +247,7 @@ fn main() -> anyhow::Result<()> {
234
247
  if file_list_json.is_some() {
235
248
  eprintln!("PROGRESS:100:100:done");
236
249
  if is_dir {
237
- println!("(TAR archive, rXFL chunk embedded)");
250
+ println!("(directory payload, rXFL chunk embedded)");
238
251
  } else {
239
252
  println!("(rXFL chunk embedded)");
240
253
  }
@@ -343,13 +356,24 @@ fn main() -> anyhow::Result<()> {
343
356
  && sig == [137, 80, 78, 71, 13, 10, 26, 10]
344
357
  });
345
358
 
346
- if is_png_file && files.is_none() && dict.is_none() && file_size > 100_000_000 {
359
+ let requested_files = match files.as_deref() {
360
+ Some(files_str) => Some(parse_requested_files(files_str)?),
361
+ None => None,
362
+ };
363
+
364
+ if is_png_file && files.is_none() && dict.is_none() {
347
365
  let out_dir = output.clone().unwrap_or_else(|| PathBuf::from("out.raw"));
348
- eprintln!("PROGRESS:5:100:decoding");
349
- match streaming_decode::streaming_decode_to_dir_encrypted(&input, &out_dir, passphrase.as_deref()) {
366
+ match streaming_decode::streaming_decode_to_dir_encrypted_with_progress(
367
+ &input,
368
+ &out_dir,
369
+ passphrase.as_deref(),
370
+ Some(Box::new(|current, total, step| {
371
+ eprintln!("PROGRESS:{}:{}:{}", current, total, step);
372
+ })),
373
+ ) {
350
374
  Ok(written) => {
351
375
  eprintln!("PROGRESS:100:100:done");
352
- println!("Unpacked {} files (TAR)", written.len());
376
+ println!("Unpacked {} files", written.len());
353
377
  return Ok(());
354
378
  }
355
379
  Err(e) => {
@@ -358,25 +382,31 @@ fn main() -> anyhow::Result<()> {
358
382
  }
359
383
  }
360
384
 
385
+ if is_png_file && requested_files.is_some() && dict.is_none() {
386
+ let out_dir = output.clone().unwrap_or_else(|| PathBuf::from("."));
387
+ std::fs::create_dir_all(&out_dir).map_err(|e| anyhow::anyhow!("Cannot create output directory {:?}: {}", out_dir, e))?;
388
+ let written = streaming_decode::streaming_decode_selected_to_dir_encrypted_with_progress(
389
+ &input,
390
+ &out_dir,
391
+ requested_files.as_deref(),
392
+ passphrase.as_deref(),
393
+ Some(Box::new(|current, total, step| {
394
+ eprintln!("PROGRESS:{}:{}:{}", current, total, step);
395
+ })),
396
+ ).map_err(|e| anyhow::anyhow!(e))?;
397
+ eprintln!("PROGRESS:100:100:done");
398
+ println!("Unpacked {} files", written.len());
399
+ return Ok(());
400
+ }
401
+
361
402
  let buf = read_all(&input)?;
362
403
  eprintln!("PROGRESS:20:100:decompressing");
363
404
  let dict_bytes: Option<Vec<u8>> = match dict {
364
405
  Some(path) => Some(read_all(&path)?),
365
406
  None => None,
366
407
  };
367
- if let Some(files_str) = files {
368
- let file_list: Option<Vec<String>> = if files_str.trim_start().starts_with('[') {
369
- match serde_json::from_str::<Vec<String>>(&files_str) {
370
- Ok(v) => Some(v),
371
- Err(e) => {
372
- eprintln!("Invalid JSON for --files: {}", e);
373
- std::process::exit(1);
374
- }
375
- }
376
- } else {
377
- let list = files_str.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect::<Vec<_>>();
378
- Some(list)
379
- };
408
+ if requested_files.is_some() {
409
+ let file_list = requested_files;
380
410
 
381
411
  let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
382
412
 
@@ -421,7 +451,9 @@ fn main() -> anyhow::Result<()> {
421
451
  std::fs::create_dir_all(&out_dir).map_err(|e| anyhow::anyhow!("Cannot create output directory {:?}: {}", out_dir, e))?;
422
452
  let files_slice = file_list.as_ref().map(|v| v.as_slice());
423
453
 
424
- let written = packer::unpack_stream_to_dir(&mut reader, &out_dir, files_slice).map_err(|e| anyhow::anyhow!(e))?;
454
+ let written = packer::unpack_stream_to_dir(&mut reader, &out_dir, files_slice, Some(&|current, total, step| {
455
+ eprintln!("PROGRESS:{}:{}:{}", current, total, step);
456
+ }), 0).map_err(|e| anyhow::anyhow!(e))?;
425
457
  eprintln!("PROGRESS:100:100:done");
426
458
  println!("Unpacked {} files", written.len());
427
459
  } else {
@@ -492,7 +524,7 @@ fn main() -> anyhow::Result<()> {
492
524
  .map_err(|e| anyhow::anyhow!("mkdir {:?}: {}", out_dir, e))?;
493
525
  let written = archive::tar_unpack(&out_bytes, &out_dir)
494
526
  .map_err(|e| anyhow::anyhow!(e))?;
495
- println!("Unpacked {} files (TAR) to {:?}", written.len(), out_dir);
527
+ println!("Unpacked {} files to {:?}", written.len(), out_dir);
496
528
  } else if out_bytes.len() >= 4
497
529
  && (u32::from_be_bytes(out_bytes[0..4].try_into().unwrap()) == 0x524f5850u32
498
530
  || u32::from_be_bytes(out_bytes[0..4].try_into().unwrap()) == 0x524f5849u32)
package/native/packer.rs CHANGED
@@ -255,13 +255,50 @@ 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
293
  let mut buf: Vec<u8> = Vec::new();
261
294
  let mut pos: usize = 0;
262
295
  let mut temp = [0u8; 64 * 1024];
263
296
  let files_filter: Option<std::collections::HashSet<String>> = files_opt.map(|l| l.iter().map(|s| s.clone()).collect());
264
297
  let mut requested = files_filter.as_ref().map(|s| s.len()).unwrap_or(usize::MAX);
298
+ let mut file_count = 0usize;
299
+ let mut processed_files = 0usize;
300
+ let mut bytes_processed = 0u64;
301
+ let mut last_pct = 10u64;
265
302
 
266
303
  let mut header_parsed = false;
267
304
  let debug = std::env::var("ROX_DEBUG").is_ok();
@@ -280,10 +317,10 @@ pub fn unpack_stream_to_dir<R: std::io::Read>(reader: &mut R, out_dir: &Path, fi
280
317
  if debug { eprintln!("[rox debug] magic_header=0x{:08x}", magic_header); }
281
318
  if magic_header == 0x524f5850u32 {
282
319
  pos += 4;
283
- let _file_count = u32::from_be_bytes(buf[pos..pos+4].try_into().unwrap()) as usize;
320
+ file_count = u32::from_be_bytes(buf[pos..pos+4].try_into().unwrap()) as usize;
284
321
  pos += 4;
285
322
  header_parsed = true;
286
- if debug { eprintln!("[rox debug] header parsed, file_count={}", _file_count); }
323
+ if debug { eprintln!("[rox debug] header parsed, file_count={}", file_count); }
287
324
  } else if magic_header == 0x524f5831u32 {
288
325
  if debug { eprintln!("[rox debug] found ROX1 outer magic, skipping 4 bytes"); }
289
326
  pos += 4;
@@ -310,6 +347,8 @@ pub fn unpack_stream_to_dir<R: std::io::Read>(reader: &mut R, out_dir: &Path, fi
310
347
  let content_start = pos + 2 + name_len + 8;
311
348
  let content_end = content_start + size;
312
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);
313
352
 
314
353
  let p = Path::new(&name);
315
354
  let mut safe = std::path::PathBuf::new();
@@ -328,10 +367,18 @@ pub fn unpack_stream_to_dir<R: std::io::Read>(reader: &mut R, out_dir: &Path, fi
328
367
  written.push(safe.to_string_lossy().to_string());
329
368
  if let Some(_set) = files_filter.as_ref() {
330
369
  requested = requested.saturating_sub(1);
331
- if requested == 0 { return Ok(written); }
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
+ }
332
377
  }
333
378
  }
334
379
 
380
+ report_unpack_progress(progress, total_expected, bytes_processed, file_count, processed_files, &mut last_pct);
381
+
335
382
  pos = content_end; if pos > 0 {
336
383
  buf.drain(0..pos);
337
384
  pos = 0;
@@ -344,6 +391,10 @@ pub fn unpack_stream_to_dir<R: std::io::Read>(reader: &mut R, out_dir: &Path, fi
344
391
  }
345
392
  }
346
393
 
394
+ if let Some(cb) = progress {
395
+ cb(99, 100, "finishing");
396
+ }
397
+
347
398
  Ok(written)
348
399
  }
349
400
 
@@ -390,7 +441,7 @@ mod stream_tests {
390
441
  let tmpdir = std::env::temp_dir().join(format!("rox_unpack_test_{}", ms));
391
442
  let _ = std::fs::create_dir_all(&tmpdir);
392
443
 
393
- let out = unpack_stream_to_dir(&mut dec2, &tmpdir, None)?;
444
+ let out = unpack_stream_to_dir(&mut dec2, &tmpdir, None, None, 0)?;
394
445
 
395
446
  assert_eq!(out.len(), 2);
396
447
  assert!(tmpdir.join("file1.txt").exists());
@@ -432,7 +483,7 @@ mod stream_tests {
432
483
  let tmpdir = std::env::temp_dir().join(format!("rox_unpack_png_test_{}", ms));
433
484
  let _ = std::fs::create_dir_all(&tmpdir);
434
485
 
435
- let out = unpack_stream_to_dir(&mut dec, &tmpdir, None)?;
486
+ let out = unpack_stream_to_dir(&mut dec, &tmpdir, None, None, 0)?;
436
487
 
437
488
  assert_eq!(out.len(), 2);
438
489
  assert!(tmpdir.join("file1.txt").exists());