roxify 1.14.2 → 1.14.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/dist/cli.js +1 -1
- package/package.json +2 -5
- package/Cargo.toml +0 -76
- package/native/archive.rs +0 -220
- package/native/audio.rs +0 -151
- package/native/bench_hybrid.rs +0 -145
- package/native/bwt.rs +0 -56
- package/native/context_mixing.rs +0 -117
- package/native/core.rs +0 -378
- package/native/crypto.rs +0 -209
- package/native/encoder.rs +0 -405
- package/native/hybrid.rs +0 -297
- package/native/image_utils.rs +0 -82
- package/native/io_advice.rs +0 -43
- package/native/io_ntfs_optimized.rs +0 -99
- package/native/lib.rs +0 -480
- package/native/main.rs +0 -939
- package/native/mtf.rs +0 -106
- package/native/packer.rs +0 -863
- package/native/png_chunk_writer.rs +0 -146
- package/native/png_utils.rs +0 -554
- package/native/pool.rs +0 -101
- package/native/progress.rs +0 -142
- package/native/rans.rs +0 -149
- package/native/rans_byte.rs +0 -286
- package/native/reconstitution.rs +0 -623
- package/native/streaming.rs +0 -189
- package/native/streaming_decode.rs +0 -720
- package/native/streaming_encode.rs +0 -684
- package/native/test_small_bwt.rs +0 -31
- package/native/test_stages.rs +0 -70
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
|
-
}
|