roxify 1.14.2 → 1.14.3

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 DELETED
@@ -1,863 +0,0 @@
1
- use anyhow::Result;
2
- use rayon::prelude::*;
3
- use serde_json::json;
4
- use std::fs;
5
- use std::io::Read;
6
- use std::path::{Path, PathBuf};
7
- use walkdir::WalkDir;
8
-
9
- pub struct PackResult {
10
- pub data: Vec<u8>,
11
- pub file_list_json: Option<String>,
12
- }
13
-
14
- pub fn pack_directory(dir_path: &Path, base_dir: Option<&Path>) -> Result<Vec<u8>> {
15
- let base = base_dir.unwrap_or(dir_path);
16
-
17
- let files: Vec<PathBuf> = WalkDir::new(dir_path)
18
- .follow_links(false)
19
- .into_iter()
20
- .filter_map(|e| e.ok())
21
- .filter(|e| e.file_type().is_file())
22
- .map(|e| e.path().to_path_buf())
23
- .collect();
24
-
25
- let file_data: Vec<(String, Vec<u8>)> = files
26
- .par_iter()
27
- .filter_map(|file_path| {
28
- let rel_path = file_path
29
- .strip_prefix(base)
30
- .unwrap_or(file_path.as_path())
31
- .to_string_lossy()
32
- .replace('\\', "/");
33
-
34
- match fs::read(file_path) {
35
- Ok(content) => Some((rel_path, content)),
36
- Err(e) => {
37
- eprintln!("⚠️ Erreur lecture {}: {}", rel_path, e);
38
- None
39
- }
40
- }
41
- })
42
- .collect();
43
-
44
- let total_size: usize = file_data
45
- .par_iter()
46
- .map(|(path, content)| path.len() + content.len() + 10)
47
- .sum();
48
- let mut result = Vec::with_capacity(8 + total_size);
49
-
50
- result.extend_from_slice(&0x524f5850u32.to_be_bytes());
51
- result.extend_from_slice(&(file_data.len() as u32).to_be_bytes());
52
-
53
- for (rel_path, content) in file_data {
54
- let name_bytes = rel_path.as_bytes();
55
- let name_len = (name_bytes.len() as u16).to_be_bytes();
56
- let size = (content.len() as u64).to_be_bytes();
57
-
58
- result.extend_from_slice(&name_len);
59
- result.extend_from_slice(name_bytes);
60
- result.extend_from_slice(&size);
61
- result.extend_from_slice(&content);
62
- }
63
-
64
- Ok(result)
65
- }
66
-
67
- pub fn pack_path(path: &Path) -> Result<Vec<u8>> {
68
- if path.is_file() {
69
- fs::read(path).map_err(Into::into)
70
- } else if path.is_dir() {
71
- pack_directory(path, Some(path))
72
- } else {
73
- Err(anyhow::anyhow!("Path is neither file nor directory"))
74
- }
75
- }
76
-
77
- pub fn pack_path_with_metadata(path: &Path) -> Result<PackResult> {
78
- if path.is_file() {
79
- let data = fs::read(path)?;
80
- let size = data.len();
81
- let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("file");
82
-
83
- let file_list = json!([{"name": name, "size": size}]);
84
- Ok(PackResult {
85
- data,
86
- file_list_json: Some(file_list.to_string()),
87
- })
88
- } else if path.is_dir() {
89
- let base = path;
90
- let files: Vec<PathBuf> = WalkDir::new(path)
91
- .follow_links(false)
92
- .into_iter()
93
- .filter_map(|e| e.ok())
94
- .filter(|e| e.file_type().is_file())
95
- .map(|e| e.path().to_path_buf())
96
- .collect();
97
-
98
- let file_data: Vec<(String, Vec<u8>)> = files
99
- .par_iter()
100
- .filter_map(|file_path| {
101
- let rel_path = file_path
102
- .strip_prefix(base)
103
- .unwrap_or(file_path.as_path())
104
- .to_string_lossy()
105
- .replace('\\', "/");
106
-
107
- match fs::read(file_path) {
108
- Ok(content) => Some((rel_path, content)),
109
- Err(e) => {
110
- eprintln!("⚠️ Erreur lecture {}: {}", rel_path, e);
111
- None
112
- }
113
- }
114
- })
115
- .collect();
116
-
117
- let file_list: Vec<_> = file_data
118
- .iter()
119
- .map(|(name, content)| json!({"name": name, "size": content.len()}))
120
- .collect();
121
-
122
- let total_size: usize = file_data
123
- .par_iter()
124
- .map(|(path, content)| path.len() + content.len() + 10)
125
- .sum();
126
-
127
- let mut result = Vec::with_capacity(8 + total_size);
128
- result.extend_from_slice(&0x524f5850u32.to_be_bytes());
129
- result.extend_from_slice(&(file_data.len() as u32).to_be_bytes());
130
-
131
- for (rel_path, content) in file_data {
132
- let name_bytes = rel_path.as_bytes();
133
- let name_len = (name_bytes.len() as u16).to_be_bytes();
134
- let size = (content.len() as u64).to_be_bytes();
135
-
136
- result.extend_from_slice(&name_len);
137
- result.extend_from_slice(name_bytes);
138
- result.extend_from_slice(&size);
139
- result.extend_from_slice(&content);
140
- }
141
-
142
- Ok(PackResult {
143
- data: result,
144
- file_list_json: Some(serde_json::to_string(&file_list)?),
145
- })
146
- } else {
147
- Err(anyhow::anyhow!("Path is neither file nor directory"))
148
- }
149
- }
150
-
151
- pub fn unpack_buffer_to_dir(
152
- buf: &[u8],
153
- out_dir: &Path,
154
- files_opt: Option<&[String]>,
155
- ) -> Result<Vec<String>> {
156
- use std::convert::TryInto;
157
- let mut written = Vec::new();
158
- let mut pos = 0usize;
159
-
160
- if buf.len() < 8 {
161
- return Err(anyhow::anyhow!("Buffer too small"));
162
- }
163
- let magic = u32::from_be_bytes(buf[0..4].try_into().unwrap());
164
-
165
- if magic == 0x524f5849u32 {
166
- let index_len = u32::from_be_bytes(buf[4..8].try_into().unwrap()) as usize;
167
- pos = 8 + index_len;
168
- return unpack_entries_sequential(buf, pos, out_dir, files_opt);
169
- }
170
-
171
- if magic != 0x524f5850u32 {
172
- return Err(anyhow::anyhow!("Invalid pack magic"));
173
- }
174
- pos += 4;
175
- let file_count = u32::from_be_bytes(buf[pos..pos + 4].try_into().unwrap()) as usize;
176
- pos += 4;
177
-
178
- let files_filter: Option<std::collections::HashSet<String>> =
179
- files_opt.map(|l| l.iter().map(|s| s.clone()).collect());
180
-
181
- for _ in 0..file_count {
182
- if pos + 2 > buf.len() {
183
- return Err(anyhow::anyhow!("Truncated pack (name len)"));
184
- }
185
- let name_len = u16::from_be_bytes(buf[pos..pos + 2].try_into().unwrap()) as usize;
186
- pos += 2;
187
- if pos + name_len > buf.len() {
188
- return Err(anyhow::anyhow!("Truncated pack (name)"));
189
- }
190
- let name = String::from_utf8_lossy(&buf[pos..pos + name_len]).to_string();
191
- pos += name_len;
192
- if pos + 8 > buf.len() {
193
- return Err(anyhow::anyhow!("Truncated pack (size)"));
194
- }
195
- let size = u64::from_be_bytes(buf[pos..pos + 8].try_into().unwrap()) as usize;
196
- pos += 8;
197
- if pos + size > buf.len() {
198
- return Err(anyhow::anyhow!("Truncated pack (content)"));
199
- }
200
-
201
- let should_write = match &files_filter {
202
- Some(set) => set.contains(&name),
203
- None => true,
204
- };
205
-
206
- if should_write {
207
- let content = &buf[pos..pos + size];
208
- let p = Path::new(&name);
209
- let mut safe = std::path::PathBuf::new();
210
- for comp in p.components() {
211
- if let std::path::Component::Normal(osstr) = comp {
212
- safe.push(osstr);
213
- }
214
- }
215
- let dest = out_dir.join(&safe);
216
- if let Some(parent) = dest.parent() {
217
- std::fs::create_dir_all(parent)
218
- .map_err(|e| anyhow::anyhow!("Cannot create parent dir {:?}: {}", parent, e))?;
219
- }
220
- std::fs::write(&dest, content)
221
- .map_err(|e| anyhow::anyhow!("Cannot write {:?}: {}", dest, e))?;
222
- written.push(safe.to_string_lossy().to_string());
223
- }
224
-
225
- pos += size;
226
- }
227
-
228
- Ok(written)
229
- }
230
-
231
- fn unpack_entries_sequential(
232
- buf: &[u8],
233
- start: usize,
234
- out_dir: &Path,
235
- files_opt: Option<&[String]>,
236
- ) -> Result<Vec<String>> {
237
- let mut written = Vec::new();
238
- let mut pos = start;
239
- let files_filter: Option<std::collections::HashSet<String>> =
240
- files_opt.map(|l| l.iter().map(|s| s.clone()).collect());
241
-
242
- while pos + 2 < buf.len() {
243
- let magic = u32::from_be_bytes(buf[pos..pos + 4].try_into().unwrap_or([0; 4]));
244
- if magic == 0x524f5849u32 {
245
- if pos + 8 > buf.len() {
246
- break;
247
- }
248
- let index_len = u32::from_be_bytes(buf[pos + 4..pos + 8].try_into().unwrap()) as usize;
249
- pos += 8 + index_len;
250
- continue;
251
- }
252
-
253
- if pos + 2 > buf.len() {
254
- break;
255
- }
256
- let name_len = u16::from_be_bytes(buf[pos..pos + 2].try_into().unwrap()) as usize;
257
- pos += 2;
258
- if pos + name_len > buf.len() {
259
- break;
260
- }
261
- let name = String::from_utf8_lossy(&buf[pos..pos + name_len]).to_string();
262
- pos += name_len;
263
- if pos + 8 > buf.len() {
264
- break;
265
- }
266
- let size = u64::from_be_bytes(buf[pos..pos + 8].try_into().unwrap()) as usize;
267
- pos += 8;
268
- if pos + size > buf.len() {
269
- break;
270
- }
271
-
272
- let should_write = match &files_filter {
273
- Some(set) => set.contains(&name),
274
- None => true,
275
- };
276
-
277
- if should_write {
278
- let content = &buf[pos..pos + size];
279
- let p = Path::new(&name);
280
- let mut safe = std::path::PathBuf::new();
281
- for comp in p.components() {
282
- if let std::path::Component::Normal(osstr) = comp {
283
- safe.push(osstr);
284
- }
285
- }
286
- let dest = out_dir.join(&safe);
287
- if let Some(parent) = dest.parent() {
288
- std::fs::create_dir_all(parent)
289
- .map_err(|e| anyhow::anyhow!("Cannot create parent dir {:?}: {}", parent, e))?;
290
- }
291
- std::fs::write(&dest, content)
292
- .map_err(|e| anyhow::anyhow!("Cannot write {:?}: {}", dest, e))?;
293
- written.push(safe.to_string_lossy().to_string());
294
- }
295
-
296
- pos += size;
297
- }
298
-
299
- Ok(written)
300
- }
301
-
302
- fn unpack_progress_percent(
303
- total_expected: u64,
304
- bytes_processed: u64,
305
- file_count: usize,
306
- processed_files: usize,
307
- ) -> u64 {
308
- if total_expected > 0 {
309
- return 10 + (bytes_processed.saturating_mul(89) / total_expected).min(89);
310
- }
311
- if file_count > 0 {
312
- return 10 + ((processed_files as u64).saturating_mul(89) / file_count as u64).min(89);
313
- }
314
- 10
315
- }
316
-
317
- fn report_unpack_progress(
318
- progress: Option<&(dyn Fn(u64, u64, &str) + Send)>,
319
- total_expected: u64,
320
- bytes_processed: u64,
321
- file_count: usize,
322
- processed_files: usize,
323
- last_pct: &mut u64,
324
- ) {
325
- if let Some(cb) = progress {
326
- let pct =
327
- unpack_progress_percent(total_expected, bytes_processed, file_count, processed_files);
328
- if pct > *last_pct {
329
- *last_pct = pct;
330
- cb(pct, 100, "extracting");
331
- }
332
- }
333
- }
334
-
335
- pub fn unpack_stream_to_dir<R: std::io::Read>(
336
- reader: &mut R,
337
- out_dir: &Path,
338
- files_opt: Option<&[String]>,
339
- progress: Option<&(dyn Fn(u64, u64, &str) + Send)>,
340
- total_expected: u64,
341
- ) -> Result<Vec<String>> {
342
- let mut written = Vec::new();
343
- let files_filter: Option<std::collections::HashSet<String>> =
344
- files_opt.map(|l| l.iter().map(|s| s.clone()).collect());
345
- let mut requested = files_filter.as_ref().map(|s| s.len()).unwrap_or(usize::MAX);
346
- let mut file_count = 0usize;
347
- let mut processed_files = 0usize;
348
- let mut bytes_processed = 0u64;
349
- let mut last_pct = 10u64;
350
-
351
- let mut magic = read_pack_u32(reader)?;
352
- if magic == 0x524f5831u32 {
353
- magic = read_pack_u32(reader)?;
354
- }
355
- if magic == 0x524f5849u32 {
356
- // ROXI format: index contains file metadata, data follows directly
357
- let index_len = read_pack_u32(reader)? as u64;
358
- let mut index_bytes = vec![0u8; index_len as usize];
359
- read_pack_exact(reader, &mut index_bytes)?;
360
-
361
- // Parse index JSON
362
- let index: Vec<serde_json::Value> = serde_json::from_slice(&index_bytes)
363
- .map_err(|e| anyhow::anyhow!("Failed to parse ROXI index: {}", e))?;
364
- file_count = index.len();
365
-
366
- // Read next 4 bytes to check for ROXP
367
- let next = read_pack_u32(reader)?;
368
-
369
- // If no ROXP, data follows directly - put back the 4 bytes we read
370
- if next != 0x524f5850u32 {
371
- // ROXI-only format: put back the 4 bytes and process data stream
372
- let prefix = next.to_be_bytes();
373
- let mut chained = std::io::Cursor::new(prefix).chain(reader);
374
- return unpack_roxi_only_stream(
375
- &mut chained,
376
- &index,
377
- out_dir,
378
- files_filter.as_ref(),
379
- progress,
380
- total_expected,
381
- );
382
- }
383
-
384
- // ROXP follows - continue with normal ROXP processing
385
- magic = next;
386
- }
387
- if magic != 0x524f5850u32 {
388
- return Err(anyhow::anyhow!("Invalid pack magic: 0x{:08x}", magic));
389
- }
390
-
391
- file_count = read_pack_u32(reader)? as usize;
392
-
393
- for _ in 0..file_count {
394
- let name_len = read_pack_u16(reader)? as usize;
395
- let mut name_bytes = vec![0u8; name_len];
396
- read_pack_exact(reader, &mut name_bytes)?;
397
- let name = String::from_utf8_lossy(&name_bytes).to_string();
398
- let size = read_pack_u64(reader)?;
399
-
400
- let should_write = match &files_filter {
401
- Some(set) => set.contains(&name),
402
- None => true,
403
- };
404
-
405
- if should_write {
406
- let safe = sanitize_pack_path(&name);
407
- let dest = out_dir.join(&safe);
408
- if let Some(parent) = dest.parent() {
409
- std::fs::create_dir_all(parent)
410
- .map_err(|e| anyhow::anyhow!("Cannot create parent dir {:?}: {}", parent, e))?;
411
- }
412
- let file = std::fs::File::create(&dest)
413
- .map_err(|e| anyhow::anyhow!("Cannot write {:?}: {}", dest, e))?;
414
- let mut writer = std::io::BufWriter::with_capacity(file_buffer_capacity(size), file);
415
- copy_pack_bytes(
416
- reader,
417
- &mut writer,
418
- size,
419
- &mut bytes_processed,
420
- file_count,
421
- processed_files,
422
- total_expected,
423
- progress,
424
- &mut last_pct,
425
- )?;
426
- finalize_output_file(writer, size, &dest)?;
427
- written.push(safe.to_string_lossy().to_string());
428
- if files_filter.is_some() {
429
- requested = requested.saturating_sub(1);
430
- }
431
- } else {
432
- discard_pack_bytes(
433
- reader,
434
- size,
435
- &mut bytes_processed,
436
- file_count,
437
- processed_files,
438
- total_expected,
439
- progress,
440
- &mut last_pct,
441
- )?;
442
- }
443
-
444
- processed_files = processed_files.saturating_add(1);
445
- report_unpack_progress(
446
- progress,
447
- total_expected,
448
- bytes_processed,
449
- file_count,
450
- processed_files,
451
- &mut last_pct,
452
- );
453
-
454
- if requested == 0 {
455
- if let Some(cb) = progress {
456
- cb(99, 100, "finishing");
457
- }
458
- return Ok(written);
459
- }
460
- }
461
-
462
- if let Some(cb) = progress {
463
- cb(99, 100, "finishing");
464
- }
465
-
466
- Ok(written)
467
- }
468
-
469
- fn unpack_roxi_only_stream<R: std::io::Read>(
470
- reader: &mut R,
471
- index: &[serde_json::Value],
472
- out_dir: &Path,
473
- files_filter: Option<&std::collections::HashSet<String>>,
474
- progress: Option<&(dyn Fn(u64, u64, &str) + Send)>,
475
- total_expected: u64,
476
- ) -> Result<Vec<String>> {
477
- let mut written = Vec::new();
478
- let mut requested = files_filter.map(|s| s.len()).unwrap_or(usize::MAX);
479
- let mut bytes_processed = 0u64;
480
- let mut last_pct = 10u64;
481
- let file_count = index.len();
482
- let mut processed_files = 0usize;
483
-
484
- // Files are already in offset order in the index, no sorting needed
485
- for entry in index {
486
- let name = entry
487
- .get("path")
488
- .and_then(|p| p.as_str())
489
- .ok_or_else(|| anyhow::anyhow!("Missing path in ROXI index"))?
490
- .to_string();
491
- let size = entry
492
- .get("size")
493
- .and_then(|s| s.as_u64())
494
- .ok_or_else(|| anyhow::anyhow!("Missing size in ROXI index"))?;
495
-
496
- // Skip header: nameLen (2) + name (nameLen) + size (8)
497
- let name_len = name.len() as u64;
498
- let header_size = 2 + name_len + 8;
499
- discard_pack_bytes(
500
- reader,
501
- header_size,
502
- &mut bytes_processed,
503
- file_count,
504
- processed_files,
505
- total_expected,
506
- progress,
507
- &mut last_pct,
508
- )?;
509
-
510
- let should_write = match files_filter {
511
- Some(set) => set.contains(&name),
512
- None => true,
513
- };
514
-
515
- if should_write {
516
- let safe = sanitize_pack_path(&name);
517
- let dest = out_dir.join(&safe);
518
- if let Some(parent) = dest.parent() {
519
- std::fs::create_dir_all(parent)
520
- .map_err(|e| anyhow::anyhow!("Cannot create parent dir {:?}: {}", parent, e))?;
521
- }
522
- let file = std::fs::File::create(&dest)
523
- .map_err(|e| anyhow::anyhow!("Cannot write {:?}: {}", dest, e))?;
524
- let mut writer = std::io::BufWriter::with_capacity(file_buffer_capacity(size), file);
525
- copy_pack_bytes(
526
- reader,
527
- &mut writer,
528
- size,
529
- &mut bytes_processed,
530
- file_count,
531
- processed_files,
532
- total_expected,
533
- progress,
534
- &mut last_pct,
535
- )?;
536
- finalize_output_file(writer, size, &dest)?;
537
- written.push(safe.to_string_lossy().to_string());
538
- if files_filter.is_some() {
539
- requested = requested.saturating_sub(1);
540
- }
541
- } else {
542
- discard_pack_bytes(
543
- reader,
544
- size,
545
- &mut bytes_processed,
546
- file_count,
547
- processed_files,
548
- total_expected,
549
- progress,
550
- &mut last_pct,
551
- )?;
552
- }
553
-
554
- processed_files = processed_files.saturating_add(1);
555
- report_unpack_progress(
556
- progress,
557
- total_expected,
558
- bytes_processed,
559
- file_count,
560
- processed_files,
561
- &mut last_pct,
562
- );
563
-
564
- if requested == 0 {
565
- if let Some(cb) = progress {
566
- cb(99, 100, "finishing");
567
- }
568
- return Ok(written);
569
- }
570
- }
571
-
572
- if let Some(cb) = progress {
573
- cb(99, 100, "finishing");
574
- }
575
-
576
- Ok(written)
577
- }
578
-
579
- fn read_pack_exact<R: std::io::Read>(reader: &mut R, buf: &mut [u8]) -> Result<()> {
580
- reader
581
- .read_exact(buf)
582
- .map_err(|e| anyhow::anyhow!("Stream read error: {}", e))
583
- }
584
-
585
- fn read_pack_u16<R: std::io::Read>(reader: &mut R) -> Result<u16> {
586
- let mut buf = [0u8; 2];
587
- read_pack_exact(reader, &mut buf)?;
588
- Ok(u16::from_be_bytes(buf))
589
- }
590
-
591
- fn read_pack_u32<R: std::io::Read>(reader: &mut R) -> Result<u32> {
592
- let mut buf = [0u8; 4];
593
- read_pack_exact(reader, &mut buf)?;
594
- Ok(u32::from_be_bytes(buf))
595
- }
596
-
597
- fn read_pack_u64<R: std::io::Read>(reader: &mut R) -> Result<u64> {
598
- let mut buf = [0u8; 8];
599
- read_pack_exact(reader, &mut buf)?;
600
- Ok(u64::from_be_bytes(buf))
601
- }
602
-
603
- fn sanitize_pack_path(name: &str) -> std::path::PathBuf {
604
- let p = Path::new(name);
605
- let mut safe = std::path::PathBuf::new();
606
- for comp in p.components() {
607
- if let std::path::Component::Normal(osstr) = comp {
608
- safe.push(osstr);
609
- }
610
- }
611
- safe
612
- }
613
-
614
- fn file_buffer_capacity(size: u64) -> usize {
615
- usize::try_from(size)
616
- .unwrap_or(4 * 1024 * 1024)
617
- .min(4 * 1024 * 1024)
618
- .max(8192)
619
- }
620
-
621
- fn finalize_output_file(
622
- mut writer: std::io::BufWriter<std::fs::File>,
623
- size: u64,
624
- dest: &Path,
625
- ) -> Result<()> {
626
- std::io::Write::flush(&mut writer)
627
- .map_err(|e| anyhow::anyhow!("Cannot flush {:?}: {}", dest, e))?;
628
- let file = writer
629
- .into_inner()
630
- .map_err(|e| anyhow::anyhow!("Cannot finalize {:?}: {}", dest, e.error()))?;
631
- crate::io_advice::sync_and_drop(&file, size);
632
- Ok(())
633
- }
634
-
635
- fn copy_pack_bytes<R: std::io::Read, W: std::io::Write>(
636
- reader: &mut R,
637
- writer: &mut W,
638
- mut remaining: u64,
639
- bytes_processed: &mut u64,
640
- file_count: usize,
641
- processed_files: usize,
642
- total_expected: u64,
643
- progress: Option<&(dyn Fn(u64, u64, &str) + Send)>,
644
- last_pct: &mut u64,
645
- ) -> Result<()> {
646
- let mut buf = vec![0u8; 1024 * 1024];
647
- while remaining > 0 {
648
- let take = remaining.min(buf.len() as u64) as usize;
649
- let read = reader
650
- .read(&mut buf[..take])
651
- .map_err(|e| anyhow::anyhow!("Stream read error: {}", e))?;
652
- if read == 0 {
653
- return Err(anyhow::anyhow!("Truncated pack content"));
654
- }
655
- writer
656
- .write_all(&buf[..read])
657
- .map_err(|e| anyhow::anyhow!("Stream write error: {}", e))?;
658
- remaining -= read as u64;
659
- *bytes_processed = bytes_processed.saturating_add(read as u64);
660
- report_unpack_progress(
661
- progress,
662
- total_expected,
663
- *bytes_processed,
664
- file_count,
665
- processed_files,
666
- last_pct,
667
- );
668
- }
669
- Ok(())
670
- }
671
-
672
- fn discard_pack_bytes<R: std::io::Read>(
673
- reader: &mut R,
674
- mut remaining: u64,
675
- bytes_processed: &mut u64,
676
- file_count: usize,
677
- processed_files: usize,
678
- total_expected: u64,
679
- progress: Option<&(dyn Fn(u64, u64, &str) + Send)>,
680
- last_pct: &mut u64,
681
- ) -> Result<()> {
682
- let mut buf = vec![0u8; 1024 * 1024];
683
- while remaining > 0 {
684
- let take = remaining.min(buf.len() as u64) as usize;
685
- let read = reader
686
- .read(&mut buf[..take])
687
- .map_err(|e| anyhow::anyhow!("Stream read error: {}", e))?;
688
- if read == 0 {
689
- return Err(anyhow::anyhow!("Truncated pack content"));
690
- }
691
- remaining -= read as u64;
692
- *bytes_processed = bytes_processed.saturating_add(read as u64);
693
- report_unpack_progress(
694
- progress,
695
- total_expected,
696
- *bytes_processed,
697
- file_count,
698
- processed_files,
699
- last_pct,
700
- );
701
- }
702
- Ok(())
703
- }
704
-
705
- #[cfg(test)]
706
- mod stream_tests {
707
- use super::*;
708
- use std::io::{Read, Write};
709
- use std::time::{SystemTime, UNIX_EPOCH};
710
-
711
- struct ChunkedReader<R> {
712
- inner: R,
713
- max_chunk: usize,
714
- }
715
-
716
- impl<R: Read> Read for ChunkedReader<R> {
717
- fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
718
- let limit = buf.len().min(self.max_chunk);
719
- self.inner.read(&mut buf[..limit])
720
- }
721
- }
722
-
723
- #[test]
724
- fn test_unpack_stream_to_dir() -> Result<()> {
725
- let mut parts: Vec<u8> = Vec::new();
726
- parts.extend_from_slice(&0x524f5850u32.to_be_bytes());
727
- parts.extend_from_slice(&(2u32.to_be_bytes()));
728
- let name1 = b"file1.txt";
729
- parts.extend_from_slice(&(name1.len() as u16).to_be_bytes());
730
- parts.extend_from_slice(name1);
731
- let content1 = b"hello world";
732
- parts.extend_from_slice(&(content1.len() as u64).to_be_bytes());
733
- parts.extend_from_slice(content1);
734
-
735
- let name2 = b"file2.txt";
736
- parts.extend_from_slice(&(name2.len() as u16).to_be_bytes());
737
- parts.extend_from_slice(name2);
738
- let content2 = b"goodbye";
739
- parts.extend_from_slice(&(content2.len() as u64).to_be_bytes());
740
- parts.extend_from_slice(content2);
741
-
742
- let mut encoder =
743
- zstd::stream::Encoder::new(Vec::new(), 0).map_err(|e| anyhow::anyhow!(e))?;
744
- encoder.write_all(&parts).map_err(|e| anyhow::anyhow!(e))?;
745
- let compressed = encoder.finish().map_err(|e| anyhow::anyhow!(e))?;
746
-
747
- let mut dec = zstd::stream::Decoder::new(std::io::Cursor::new(compressed.clone()))
748
- .map_err(|e| anyhow::anyhow!(e))?;
749
- dec.window_log_max(31).map_err(|e| anyhow::anyhow!(e))?;
750
-
751
- let mut all = Vec::new();
752
- dec.read_to_end(&mut all).map_err(|e| anyhow::anyhow!(e))?;
753
- assert_eq!(all.len(), parts.len());
754
- assert_eq!(&all[..], &parts[..]);
755
-
756
- let mut dec2 = zstd::stream::Decoder::new(std::io::Cursor::new(compressed))
757
- .map_err(|e| anyhow::anyhow!(e))?;
758
- dec2.window_log_max(31).map_err(|e| anyhow::anyhow!(e))?;
759
-
760
- let ms = SystemTime::now()
761
- .duration_since(UNIX_EPOCH)
762
- .unwrap()
763
- .as_millis();
764
- let tmpdir = std::env::temp_dir().join(format!("rox_unpack_test_{}", ms));
765
- let _ = std::fs::create_dir_all(&tmpdir);
766
-
767
- let out = unpack_stream_to_dir(&mut dec2, &tmpdir, None, None, 0)?;
768
-
769
- assert_eq!(out.len(), 2);
770
- assert!(tmpdir.join("file1.txt").exists());
771
- assert!(tmpdir.join("file2.txt").exists());
772
- let _ = std::fs::remove_file(tmpdir.join("file1.txt"));
773
- let _ = std::fs::remove_file(tmpdir.join("file2.txt"));
774
- let _ = std::fs::remove_dir(&tmpdir);
775
- Ok(())
776
- }
777
-
778
- #[test]
779
- fn test_unpack_stream_from_png_payload() -> Result<()> {
780
- let mut parts: Vec<u8> = Vec::new();
781
- parts.extend_from_slice(&0x524f5850u32.to_be_bytes());
782
- parts.extend_from_slice(&(2u32.to_be_bytes()));
783
- let name1 = b"file1.txt";
784
- parts.extend_from_slice(&(name1.len() as u16).to_be_bytes());
785
- parts.extend_from_slice(name1);
786
- let content1 = b"hello world";
787
- parts.extend_from_slice(&(content1.len() as u64).to_be_bytes());
788
- parts.extend_from_slice(content1);
789
-
790
- let name2 = b"file2.txt";
791
- parts.extend_from_slice(&(name2.len() as u16).to_be_bytes());
792
- parts.extend_from_slice(name2);
793
- let content2 = b"goodbye";
794
- parts.extend_from_slice(&(content2.len() as u64).to_be_bytes());
795
- parts.extend_from_slice(content2);
796
-
797
- let png = crate::encoder::encode_to_png_with_name_and_filelist(&parts, 0, None, None)?;
798
- let payload =
799
- crate::png_utils::extract_payload_from_png(&png).map_err(|e| anyhow::anyhow!(e))?;
800
- assert!(!payload.is_empty());
801
- let first = payload[0];
802
- assert_eq!(first, 0x00u8);
803
- let compressed = payload[1..].to_vec();
804
- let mut dec = zstd::stream::Decoder::new(std::io::Cursor::new(compressed))
805
- .map_err(|e| anyhow::anyhow!(e))?;
806
- dec.window_log_max(31).map_err(|e| anyhow::anyhow!(e))?;
807
-
808
- let ms = SystemTime::now()
809
- .duration_since(UNIX_EPOCH)
810
- .unwrap()
811
- .as_millis();
812
- let tmpdir = std::env::temp_dir().join(format!("rox_unpack_png_test_{}", ms));
813
- let _ = std::fs::create_dir_all(&tmpdir);
814
-
815
- let out = unpack_stream_to_dir(&mut dec, &tmpdir, None, None, 0)?;
816
-
817
- assert_eq!(out.len(), 2);
818
- assert!(tmpdir.join("file1.txt").exists());
819
- assert!(tmpdir.join("file2.txt").exists());
820
-
821
- let _ = std::fs::remove_file(tmpdir.join("file1.txt"));
822
- let _ = std::fs::remove_file(tmpdir.join("file2.txt"));
823
- let _ = std::fs::remove_dir(&tmpdir);
824
- Ok(())
825
- }
826
-
827
- #[test]
828
- fn test_unpack_stream_to_dir_large_file_small_reads() -> Result<()> {
829
- let large = vec![0x5a; 2 * 1024 * 1024];
830
- let mut parts: Vec<u8> = Vec::new();
831
- parts.extend_from_slice(&0x524f5850u32.to_be_bytes());
832
- parts.extend_from_slice(&(1u32.to_be_bytes()));
833
- let name = b"big.bin";
834
- parts.extend_from_slice(&(name.len() as u16).to_be_bytes());
835
- parts.extend_from_slice(name);
836
- parts.extend_from_slice(&(large.len() as u64).to_be_bytes());
837
- parts.extend_from_slice(&large);
838
-
839
- let reader = std::io::Cursor::new(parts);
840
- let mut reader = ChunkedReader {
841
- inner: reader,
842
- max_chunk: 37,
843
- };
844
-
845
- let ms = SystemTime::now()
846
- .duration_since(UNIX_EPOCH)
847
- .unwrap()
848
- .as_millis();
849
- let tmpdir = std::env::temp_dir().join(format!("rox_unpack_large_stream_test_{}", ms));
850
- let _ = std::fs::create_dir_all(&tmpdir);
851
-
852
- let out = unpack_stream_to_dir(&mut reader, &tmpdir, None, None, large.len() as u64)?;
853
-
854
- assert_eq!(out, vec!["big.bin".to_string()]);
855
- let restored = std::fs::read(tmpdir.join("big.bin"))?;
856
- assert_eq!(restored.len(), large.len());
857
- assert_eq!(restored, large);
858
-
859
- let _ = std::fs::remove_file(tmpdir.join("big.bin"));
860
- let _ = std::fs::remove_dir(&tmpdir);
861
- Ok(())
862
- }
863
- }