roxify 1.7.6 → 1.8.1
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 +82 -0
- package/dist/cli.js +1 -1
- package/dist/roxify_native.exe +0 -0
- package/native/archive.rs +176 -0
- package/native/audio.rs +151 -0
- package/native/bwt.rs +100 -0
- package/native/context_mixing.rs +120 -0
- package/native/core.rs +297 -0
- package/native/crypto.rs +119 -0
- package/native/encoder.rs +640 -0
- package/native/gpu.rs +116 -0
- package/native/hybrid.rs +162 -0
- package/native/image_utils.rs +77 -0
- package/native/lib.rs +464 -0
- package/native/main.rs +462 -0
- package/native/packer.rs +447 -0
- package/native/png_utils.rs +192 -0
- package/native/pool.rs +101 -0
- package/native/progress.rs +43 -0
- package/native/rans.rs +149 -0
- package/native/reconstitution.rs +511 -0
- package/package.json +6 -1
- package/scripts/postinstall.cjs +101 -0
package/native/packer.rs
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
use anyhow::Result;
|
|
2
|
+
use std::fs;
|
|
3
|
+
use std::path::{Path, PathBuf};
|
|
4
|
+
use walkdir::WalkDir;
|
|
5
|
+
use rayon::prelude::*;
|
|
6
|
+
use serde_json::json;
|
|
7
|
+
|
|
8
|
+
pub struct PackResult {
|
|
9
|
+
pub data: Vec<u8>,
|
|
10
|
+
pub file_list_json: Option<String>,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
pub fn pack_directory(dir_path: &Path, base_dir: Option<&Path>) -> Result<Vec<u8>> {
|
|
14
|
+
let base = base_dir.unwrap_or(dir_path);
|
|
15
|
+
|
|
16
|
+
let files: Vec<PathBuf> = WalkDir::new(dir_path)
|
|
17
|
+
.follow_links(false)
|
|
18
|
+
.into_iter()
|
|
19
|
+
.filter_map(|e| e.ok())
|
|
20
|
+
.filter(|e| e.file_type().is_file())
|
|
21
|
+
.map(|e| e.path().to_path_buf())
|
|
22
|
+
.collect();
|
|
23
|
+
|
|
24
|
+
let file_data: Vec<(String, Vec<u8>)> = files
|
|
25
|
+
.par_iter()
|
|
26
|
+
.filter_map(|file_path| {
|
|
27
|
+
let rel_path = file_path.strip_prefix(base)
|
|
28
|
+
.unwrap_or(file_path.as_path())
|
|
29
|
+
.to_string_lossy()
|
|
30
|
+
.replace('\\', "/");
|
|
31
|
+
|
|
32
|
+
match fs::read(file_path) {
|
|
33
|
+
Ok(content) => Some((rel_path, content)),
|
|
34
|
+
Err(e) => {
|
|
35
|
+
eprintln!("⚠️ Erreur lecture {}: {}", rel_path, e);
|
|
36
|
+
None
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
.collect();
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
let total_size: usize = file_data.par_iter().map(|(path, content)| path.len() + content.len() + 10).sum();
|
|
44
|
+
let mut result = Vec::with_capacity(8 + total_size);
|
|
45
|
+
|
|
46
|
+
result.extend_from_slice(&0x524f5850u32.to_be_bytes());
|
|
47
|
+
result.extend_from_slice(&(file_data.len() as u32).to_be_bytes());
|
|
48
|
+
|
|
49
|
+
for (rel_path, content) in file_data {
|
|
50
|
+
let name_bytes = rel_path.as_bytes();
|
|
51
|
+
let name_len = (name_bytes.len() as u16).to_be_bytes();
|
|
52
|
+
let size = (content.len() as u64).to_be_bytes();
|
|
53
|
+
|
|
54
|
+
result.extend_from_slice(&name_len);
|
|
55
|
+
result.extend_from_slice(name_bytes);
|
|
56
|
+
result.extend_from_slice(&size);
|
|
57
|
+
result.extend_from_slice(&content);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
Ok(result)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pub fn pack_path(path: &Path) -> Result<Vec<u8>> {
|
|
64
|
+
if path.is_file() {
|
|
65
|
+
fs::read(path).map_err(Into::into)
|
|
66
|
+
} else if path.is_dir() {
|
|
67
|
+
pack_directory(path, Some(path))
|
|
68
|
+
} else {
|
|
69
|
+
Err(anyhow::anyhow!("Path is neither file nor directory"))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
pub fn pack_path_with_metadata(path: &Path) -> Result<PackResult> {
|
|
74
|
+
if path.is_file() {
|
|
75
|
+
let data = fs::read(path)?;
|
|
76
|
+
let size = data.len();
|
|
77
|
+
let name = path.file_name()
|
|
78
|
+
.and_then(|n| n.to_str())
|
|
79
|
+
.unwrap_or("file");
|
|
80
|
+
|
|
81
|
+
let file_list = json!([{"name": name, "size": size}]);
|
|
82
|
+
Ok(PackResult {
|
|
83
|
+
data,
|
|
84
|
+
file_list_json: Some(file_list.to_string()),
|
|
85
|
+
})
|
|
86
|
+
} else if path.is_dir() {
|
|
87
|
+
let base = path;
|
|
88
|
+
let files: Vec<PathBuf> = WalkDir::new(path)
|
|
89
|
+
.follow_links(false)
|
|
90
|
+
.into_iter()
|
|
91
|
+
.filter_map(|e| e.ok())
|
|
92
|
+
.filter(|e| e.file_type().is_file())
|
|
93
|
+
.map(|e| e.path().to_path_buf())
|
|
94
|
+
.collect();
|
|
95
|
+
|
|
96
|
+
let file_data: Vec<(String, Vec<u8>)> = files
|
|
97
|
+
.par_iter()
|
|
98
|
+
.filter_map(|file_path| {
|
|
99
|
+
let rel_path = file_path.strip_prefix(base)
|
|
100
|
+
.unwrap_or(file_path.as_path())
|
|
101
|
+
.to_string_lossy()
|
|
102
|
+
.replace('\\', "/");
|
|
103
|
+
|
|
104
|
+
match fs::read(file_path) {
|
|
105
|
+
Ok(content) => Some((rel_path, content)),
|
|
106
|
+
Err(e) => {
|
|
107
|
+
eprintln!("⚠️ Erreur lecture {}: {}", rel_path, e);
|
|
108
|
+
None
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
.collect();
|
|
113
|
+
|
|
114
|
+
let file_list: Vec<_> = file_data.iter()
|
|
115
|
+
.map(|(name, content)| json!({"name": name, "size": content.len()}))
|
|
116
|
+
.collect();
|
|
117
|
+
|
|
118
|
+
let total_size: usize = file_data.par_iter()
|
|
119
|
+
.map(|(path, content)| path.len() + content.len() + 10)
|
|
120
|
+
.sum();
|
|
121
|
+
|
|
122
|
+
let mut result = Vec::with_capacity(8 + total_size);
|
|
123
|
+
result.extend_from_slice(&0x524f5850u32.to_be_bytes());
|
|
124
|
+
result.extend_from_slice(&(file_data.len() as u32).to_be_bytes());
|
|
125
|
+
|
|
126
|
+
for (rel_path, content) in file_data {
|
|
127
|
+
let name_bytes = rel_path.as_bytes();
|
|
128
|
+
let name_len = (name_bytes.len() as u16).to_be_bytes();
|
|
129
|
+
let size = (content.len() as u64).to_be_bytes();
|
|
130
|
+
|
|
131
|
+
result.extend_from_slice(&name_len);
|
|
132
|
+
result.extend_from_slice(name_bytes);
|
|
133
|
+
result.extend_from_slice(&size);
|
|
134
|
+
result.extend_from_slice(&content);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
Ok(PackResult {
|
|
138
|
+
data: result,
|
|
139
|
+
file_list_json: Some(serde_json::to_string(&file_list)?),
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
} else {
|
|
143
|
+
Err(anyhow::anyhow!("Path is neither file nor directory"))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
pub fn unpack_buffer_to_dir(buf: &[u8], out_dir: &Path, files_opt: Option<&[String]>) -> Result<Vec<String>> {
|
|
148
|
+
|
|
149
|
+
use std::convert::TryInto;
|
|
150
|
+
let mut written = Vec::new();
|
|
151
|
+
let mut pos = 0usize;
|
|
152
|
+
|
|
153
|
+
if buf.len() < 8 { return Err(anyhow::anyhow!("Buffer too small")); }
|
|
154
|
+
let magic = u32::from_be_bytes(buf[0..4].try_into().unwrap());
|
|
155
|
+
|
|
156
|
+
if magic == 0x524f5849u32 {
|
|
157
|
+
let index_len = u32::from_be_bytes(buf[4..8].try_into().unwrap()) as usize;
|
|
158
|
+
pos = 8 + index_len;
|
|
159
|
+
return unpack_entries_sequential(buf, pos, out_dir, files_opt);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if magic != 0x524f5850u32 { return Err(anyhow::anyhow!("Invalid pack magic")); }
|
|
163
|
+
pos += 4;
|
|
164
|
+
let file_count = u32::from_be_bytes(buf[pos..pos+4].try_into().unwrap()) as usize; pos += 4;
|
|
165
|
+
|
|
166
|
+
let files_filter: Option<std::collections::HashSet<String>> = files_opt.map(|l| l.iter().map(|s| s.clone()).collect());
|
|
167
|
+
|
|
168
|
+
for _ in 0..file_count {
|
|
169
|
+
if pos + 2 > buf.len() { return Err(anyhow::anyhow!("Truncated pack (name len)")); }
|
|
170
|
+
let name_len = u16::from_be_bytes(buf[pos..pos+2].try_into().unwrap()) as usize; pos += 2;
|
|
171
|
+
if pos + name_len > buf.len() { return Err(anyhow::anyhow!("Truncated pack (name)")); }
|
|
172
|
+
let name = String::from_utf8_lossy(&buf[pos..pos+name_len]).to_string(); pos += name_len;
|
|
173
|
+
if pos + 8 > buf.len() { return Err(anyhow::anyhow!("Truncated pack (size)")); }
|
|
174
|
+
let size = u64::from_be_bytes(buf[pos..pos+8].try_into().unwrap()) as usize; pos += 8;
|
|
175
|
+
if pos + size > buf.len() { return Err(anyhow::anyhow!("Truncated pack (content)")); }
|
|
176
|
+
|
|
177
|
+
let should_write = match &files_filter {
|
|
178
|
+
Some(set) => set.contains(&name),
|
|
179
|
+
None => true,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if should_write {
|
|
183
|
+
let content = &buf[pos..pos+size];
|
|
184
|
+
let p = Path::new(&name);
|
|
185
|
+
let mut safe = std::path::PathBuf::new();
|
|
186
|
+
for comp in p.components() {
|
|
187
|
+
if let std::path::Component::Normal(osstr) = comp {
|
|
188
|
+
safe.push(osstr);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
let dest = out_dir.join(&safe);
|
|
192
|
+
if let Some(parent) = dest.parent() {
|
|
193
|
+
std::fs::create_dir_all(parent).map_err(|e| anyhow::anyhow!("Cannot create parent dir {:?}: {}", parent, e))?;
|
|
194
|
+
}
|
|
195
|
+
std::fs::write(&dest, content).map_err(|e| anyhow::anyhow!("Cannot write {:?}: {}", dest, e))?;
|
|
196
|
+
written.push(safe.to_string_lossy().to_string());
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
pos += size;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
Ok(written)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fn unpack_entries_sequential(buf: &[u8], start: usize, out_dir: &Path, files_opt: Option<&[String]>) -> Result<Vec<String>> {
|
|
206
|
+
let mut written = Vec::new();
|
|
207
|
+
let mut pos = start;
|
|
208
|
+
let files_filter: Option<std::collections::HashSet<String>> = files_opt.map(|l| l.iter().map(|s| s.clone()).collect());
|
|
209
|
+
|
|
210
|
+
while pos + 2 < buf.len() {
|
|
211
|
+
let magic = u32::from_be_bytes(buf[pos..pos+4].try_into().unwrap_or([0;4]));
|
|
212
|
+
if magic == 0x524f5849u32 {
|
|
213
|
+
if pos + 8 > buf.len() { break; }
|
|
214
|
+
let index_len = u32::from_be_bytes(buf[pos+4..pos+8].try_into().unwrap()) as usize;
|
|
215
|
+
pos += 8 + index_len;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if pos + 2 > buf.len() { break; }
|
|
220
|
+
let name_len = u16::from_be_bytes(buf[pos..pos+2].try_into().unwrap()) as usize;
|
|
221
|
+
pos += 2;
|
|
222
|
+
if pos + name_len > buf.len() { break; }
|
|
223
|
+
let name = String::from_utf8_lossy(&buf[pos..pos+name_len]).to_string();
|
|
224
|
+
pos += name_len;
|
|
225
|
+
if pos + 8 > buf.len() { break; }
|
|
226
|
+
let size = u64::from_be_bytes(buf[pos..pos+8].try_into().unwrap()) as usize;
|
|
227
|
+
pos += 8;
|
|
228
|
+
if pos + size > buf.len() { break; }
|
|
229
|
+
|
|
230
|
+
let should_write = match &files_filter {
|
|
231
|
+
Some(set) => set.contains(&name),
|
|
232
|
+
None => true,
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
if should_write {
|
|
236
|
+
let content = &buf[pos..pos+size];
|
|
237
|
+
let p = Path::new(&name);
|
|
238
|
+
let mut safe = std::path::PathBuf::new();
|
|
239
|
+
for comp in p.components() {
|
|
240
|
+
if let std::path::Component::Normal(osstr) = comp {
|
|
241
|
+
safe.push(osstr);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
let dest = out_dir.join(&safe);
|
|
245
|
+
if let Some(parent) = dest.parent() {
|
|
246
|
+
std::fs::create_dir_all(parent).map_err(|e| anyhow::anyhow!("Cannot create parent dir {:?}: {}", parent, e))?;
|
|
247
|
+
}
|
|
248
|
+
std::fs::write(&dest, content).map_err(|e| anyhow::anyhow!("Cannot write {:?}: {}", dest, e))?;
|
|
249
|
+
written.push(safe.to_string_lossy().to_string());
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
pos += size;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
Ok(written)
|
|
256
|
+
}
|
|
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>> {
|
|
259
|
+
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
|
+
let files_filter: Option<std::collections::HashSet<String>> = files_opt.map(|l| l.iter().map(|s| s.clone()).collect());
|
|
264
|
+
let mut requested = files_filter.as_ref().map(|s| s.len()).unwrap_or(usize::MAX);
|
|
265
|
+
|
|
266
|
+
let mut header_parsed = false;
|
|
267
|
+
let debug = std::env::var("ROX_DEBUG").is_ok();
|
|
268
|
+
if debug { eprintln!("[rox debug] unpack_stream_to_dir called (out_dir={:?})", out_dir); }
|
|
269
|
+
|
|
270
|
+
loop {
|
|
271
|
+
loop {
|
|
272
|
+
if !header_parsed {
|
|
273
|
+
if pos + 8 > buf.len() { break; }
|
|
274
|
+
if debug {
|
|
275
|
+
eprintln!("[rox debug] buf.len={} pos={} first16={:?}", buf.len(), pos, &buf[0..std::cmp::min(16, buf.len())]);
|
|
276
|
+
eprintln!("[rox debug] after first debug");
|
|
277
|
+
}
|
|
278
|
+
if debug { eprintln!("[rox debug] before reading magic_header"); }
|
|
279
|
+
let magic_header = u32::from_be_bytes(buf[pos..pos+4].try_into().unwrap());
|
|
280
|
+
if debug { eprintln!("[rox debug] magic_header=0x{:08x}", magic_header); }
|
|
281
|
+
if magic_header == 0x524f5850u32 {
|
|
282
|
+
pos += 4;
|
|
283
|
+
let _file_count = u32::from_be_bytes(buf[pos..pos+4].try_into().unwrap()) as usize;
|
|
284
|
+
pos += 4;
|
|
285
|
+
header_parsed = true;
|
|
286
|
+
if debug { eprintln!("[rox debug] header parsed, file_count={}", _file_count); }
|
|
287
|
+
} else if magic_header == 0x524f5831u32 {
|
|
288
|
+
if debug { eprintln!("[rox debug] found ROX1 outer magic, skipping 4 bytes"); }
|
|
289
|
+
pos += 4;
|
|
290
|
+
continue; } else {
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if pos + 8 > buf.len() { break; }
|
|
295
|
+
let magic = u32::from_be_bytes(buf[pos..pos+4].try_into().unwrap());
|
|
296
|
+
if magic == 0x524f5849u32 {
|
|
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;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if pos + 2 > buf.len() { break; }
|
|
304
|
+
let name_len = u16::from_be_bytes(buf[pos..pos+2].try_into().unwrap()) as usize;
|
|
305
|
+
if pos + 2 + name_len + 8 > buf.len() { break; }
|
|
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; }
|
|
309
|
+
|
|
310
|
+
let content_start = pos + 2 + name_len + 8;
|
|
311
|
+
let content_end = content_start + size;
|
|
312
|
+
let content = &buf[content_start..content_end];
|
|
313
|
+
|
|
314
|
+
let p = Path::new(&name);
|
|
315
|
+
let mut safe = std::path::PathBuf::new();
|
|
316
|
+
for comp in p.components() {
|
|
317
|
+
if let std::path::Component::Normal(osstr) = comp {
|
|
318
|
+
safe.push(osstr);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
let dest = out_dir.join(&safe);
|
|
322
|
+
|
|
323
|
+
if files_filter.is_none() || files_filter.as_ref().map_or(false, |s| s.contains(&name)) {
|
|
324
|
+
if let Some(parent) = dest.parent() {
|
|
325
|
+
std::fs::create_dir_all(parent).map_err(|e| anyhow::anyhow!("Cannot create parent dir {:?}: {}", parent, e))?;
|
|
326
|
+
}
|
|
327
|
+
std::fs::write(&dest, content).map_err(|e| anyhow::anyhow!("Cannot write {:?}: {}", dest, e))?;
|
|
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
|
+
}
|
|
334
|
+
|
|
335
|
+
pos = content_end; if pos > 0 {
|
|
336
|
+
buf.drain(0..pos);
|
|
337
|
+
pos = 0;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
match reader.read(&mut temp) {
|
|
342
|
+
Ok(0) => break, Ok(n) => buf.extend_from_slice(&temp[..n]),
|
|
343
|
+
Err(e) => return Err(anyhow::anyhow!("Stream read error: {}", e)),
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
Ok(written)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
#[cfg(test)]
|
|
351
|
+
mod stream_tests {
|
|
352
|
+
use super::*;
|
|
353
|
+
use std::io::{Write, Read};
|
|
354
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
355
|
+
|
|
356
|
+
#[test]
|
|
357
|
+
fn test_unpack_stream_to_dir() -> Result<()> {
|
|
358
|
+
let mut parts: Vec<u8> = Vec::new();
|
|
359
|
+
parts.extend_from_slice(&0x524f5850u32.to_be_bytes()); parts.extend_from_slice(&(2u32.to_be_bytes()));
|
|
360
|
+
let name1 = b"file1.txt";
|
|
361
|
+
parts.extend_from_slice(&(name1.len() as u16).to_be_bytes());
|
|
362
|
+
parts.extend_from_slice(name1);
|
|
363
|
+
let content1 = b"hello world";
|
|
364
|
+
parts.extend_from_slice(&(content1.len() as u64).to_be_bytes());
|
|
365
|
+
parts.extend_from_slice(content1);
|
|
366
|
+
|
|
367
|
+
let name2 = b"file2.txt";
|
|
368
|
+
parts.extend_from_slice(&(name2.len() as u16).to_be_bytes());
|
|
369
|
+
parts.extend_from_slice(name2);
|
|
370
|
+
let content2 = b"goodbye";
|
|
371
|
+
parts.extend_from_slice(&(content2.len() as u64).to_be_bytes());
|
|
372
|
+
parts.extend_from_slice(content2);
|
|
373
|
+
|
|
374
|
+
let mut encoder = zstd::stream::Encoder::new(Vec::new(), 0).map_err(|e| anyhow::anyhow!(e))?;
|
|
375
|
+
encoder.write_all(&parts).map_err(|e| anyhow::anyhow!(e))?;
|
|
376
|
+
let compressed = encoder.finish().map_err(|e| anyhow::anyhow!(e))?;
|
|
377
|
+
|
|
378
|
+
let mut dec = zstd::stream::Decoder::new(std::io::Cursor::new(compressed.clone())).map_err(|e| anyhow::anyhow!(e))?;
|
|
379
|
+
dec.window_log_max(31).map_err(|e| anyhow::anyhow!(e))?;
|
|
380
|
+
|
|
381
|
+
let mut all = Vec::new();
|
|
382
|
+
dec.read_to_end(&mut all).map_err(|e| anyhow::anyhow!(e))?;
|
|
383
|
+
assert_eq!(all.len(), parts.len());
|
|
384
|
+
assert_eq!(&all[..], &parts[..]);
|
|
385
|
+
|
|
386
|
+
let mut dec2 = zstd::stream::Decoder::new(std::io::Cursor::new(compressed)).map_err(|e| anyhow::anyhow!(e))?;
|
|
387
|
+
dec2.window_log_max(31).map_err(|e| anyhow::anyhow!(e))?;
|
|
388
|
+
|
|
389
|
+
let ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis();
|
|
390
|
+
let tmpdir = std::env::temp_dir().join(format!("rox_unpack_test_{}", ms));
|
|
391
|
+
let _ = std::fs::create_dir_all(&tmpdir);
|
|
392
|
+
|
|
393
|
+
let out = unpack_stream_to_dir(&mut dec2, &tmpdir, None)?;
|
|
394
|
+
|
|
395
|
+
assert_eq!(out.len(), 2);
|
|
396
|
+
assert!(tmpdir.join("file1.txt").exists());
|
|
397
|
+
assert!(tmpdir.join("file2.txt").exists());
|
|
398
|
+
let _ = std::fs::remove_file(tmpdir.join("file1.txt"));
|
|
399
|
+
let _ = std::fs::remove_file(tmpdir.join("file2.txt"));
|
|
400
|
+
let _ = std::fs::remove_dir(&tmpdir);
|
|
401
|
+
Ok(())
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
#[test]
|
|
405
|
+
fn test_unpack_stream_from_png_payload() -> Result<()> {
|
|
406
|
+
let mut parts: Vec<u8> = Vec::new();
|
|
407
|
+
parts.extend_from_slice(&0x524f5850u32.to_be_bytes()); parts.extend_from_slice(&(2u32.to_be_bytes()));
|
|
408
|
+
let name1 = b"file1.txt";
|
|
409
|
+
parts.extend_from_slice(&(name1.len() as u16).to_be_bytes());
|
|
410
|
+
parts.extend_from_slice(name1);
|
|
411
|
+
let content1 = b"hello world";
|
|
412
|
+
parts.extend_from_slice(&(content1.len() as u64).to_be_bytes());
|
|
413
|
+
parts.extend_from_slice(content1);
|
|
414
|
+
|
|
415
|
+
let name2 = b"file2.txt";
|
|
416
|
+
parts.extend_from_slice(&(name2.len() as u16).to_be_bytes());
|
|
417
|
+
parts.extend_from_slice(name2);
|
|
418
|
+
let content2 = b"goodbye";
|
|
419
|
+
parts.extend_from_slice(&(content2.len() as u64).to_be_bytes());
|
|
420
|
+
parts.extend_from_slice(content2);
|
|
421
|
+
|
|
422
|
+
let png = crate::encoder::encode_to_png_with_name_and_filelist(&parts, 0, None, None)?;
|
|
423
|
+
let payload = crate::png_utils::extract_payload_from_png(&png).map_err(|e| anyhow::anyhow!(e))?;
|
|
424
|
+
assert!(!payload.is_empty());
|
|
425
|
+
let first = payload[0];
|
|
426
|
+
assert_eq!(first, 0x00u8);
|
|
427
|
+
let compressed = payload[1..].to_vec();
|
|
428
|
+
let mut dec = zstd::stream::Decoder::new(std::io::Cursor::new(compressed)).map_err(|e| anyhow::anyhow!(e))?;
|
|
429
|
+
dec.window_log_max(31).map_err(|e| anyhow::anyhow!(e))?;
|
|
430
|
+
|
|
431
|
+
let ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis();
|
|
432
|
+
let tmpdir = std::env::temp_dir().join(format!("rox_unpack_png_test_{}", ms));
|
|
433
|
+
let _ = std::fs::create_dir_all(&tmpdir);
|
|
434
|
+
|
|
435
|
+
let out = unpack_stream_to_dir(&mut dec, &tmpdir, None)?;
|
|
436
|
+
|
|
437
|
+
assert_eq!(out.len(), 2);
|
|
438
|
+
assert!(tmpdir.join("file1.txt").exists());
|
|
439
|
+
assert!(tmpdir.join("file2.txt").exists());
|
|
440
|
+
|
|
441
|
+
let _ = std::fs::remove_file(tmpdir.join("file1.txt"));
|
|
442
|
+
let _ = std::fs::remove_file(tmpdir.join("file2.txt"));
|
|
443
|
+
let _ = std::fs::remove_dir(&tmpdir);
|
|
444
|
+
Ok(())
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
use bytemuck::{Pod, Zeroable};
|
|
2
|
+
use std::io::{Read, Seek, SeekFrom};
|
|
3
|
+
|
|
4
|
+
#[repr(C)]
|
|
5
|
+
#[derive(Clone, Copy, Pod, Zeroable)]
|
|
6
|
+
struct PngSignature([u8; 8]);
|
|
7
|
+
|
|
8
|
+
const PNG_SIG: PngSignature = PngSignature([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
9
|
+
|
|
10
|
+
#[derive(Debug, Clone)]
|
|
11
|
+
pub struct PngChunk {
|
|
12
|
+
pub name: String,
|
|
13
|
+
pub data: Vec<u8>,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fn read_u32_be(data: &[u8]) -> u32 {
|
|
17
|
+
u32::from_be_bytes([data[0], data[1], data[2], data[3]])
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
fn write_u32_be(val: u32) -> [u8; 4] {
|
|
21
|
+
val.to_be_bytes()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pub fn extract_png_chunks_streaming<R: Read + Seek>(reader: &mut R) -> Result<Vec<PngChunk>, String> {
|
|
25
|
+
let mut sig = [0u8; 8];
|
|
26
|
+
reader.read_exact(&mut sig).map_err(|e| format!("read sig: {}", e))?;
|
|
27
|
+
if sig != PNG_SIG.0 {
|
|
28
|
+
return Err("Invalid PNG signature".to_string());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let mut chunks = Vec::new();
|
|
32
|
+
let mut header = [0u8; 8];
|
|
33
|
+
|
|
34
|
+
loop {
|
|
35
|
+
if reader.read_exact(&mut header).is_err() {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let length = u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as usize;
|
|
40
|
+
let chunk_type = [header[4], header[5], header[6], header[7]];
|
|
41
|
+
let name = String::from_utf8_lossy(&chunk_type).to_string();
|
|
42
|
+
|
|
43
|
+
if name == "IDAT" {
|
|
44
|
+
reader.seek(SeekFrom::Current(length as i64 + 4)).map_err(|e| format!("seek: {}", e))?;
|
|
45
|
+
} else {
|
|
46
|
+
let mut data = vec![0u8; length];
|
|
47
|
+
reader.read_exact(&mut data).map_err(|e| format!("read chunk {}: {}", name, e))?;
|
|
48
|
+
reader.seek(SeekFrom::Current(4)).map_err(|e| format!("seek crc: {}", e))?;
|
|
49
|
+
chunks.push(PngChunk { name: name.clone(), data });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if &chunk_type == b"IEND" {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
Ok(chunks)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pub fn extract_png_chunks(png_data: &[u8]) -> Result<Vec<PngChunk>, String> {
|
|
61
|
+
if png_data.len() < 8 || &png_data[..8] != &PNG_SIG.0 {
|
|
62
|
+
return Err("Invalid PNG signature".to_string());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let mut chunks = Vec::new();
|
|
66
|
+
let mut pos = 8;
|
|
67
|
+
|
|
68
|
+
while pos + 12 <= png_data.len() {
|
|
69
|
+
let length = read_u32_be(&png_data[pos..pos + 4]) as usize;
|
|
70
|
+
let chunk_type = &png_data[pos + 4..pos + 8];
|
|
71
|
+
|
|
72
|
+
if pos + 12 + length > png_data.len() {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let chunk_data = &png_data[pos + 8..pos + 8 + length];
|
|
77
|
+
|
|
78
|
+
let name = String::from_utf8_lossy(chunk_type).to_string();
|
|
79
|
+
chunks.push(PngChunk {
|
|
80
|
+
name,
|
|
81
|
+
data: chunk_data.to_vec(),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
pos += 12 + length;
|
|
85
|
+
|
|
86
|
+
if chunk_type == b"IEND" {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
Ok(chunks)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
pub fn encode_png_chunks(chunks: &[PngChunk]) -> Result<Vec<u8>, String> {
|
|
95
|
+
let mut output = Vec::new();
|
|
96
|
+
|
|
97
|
+
output.extend_from_slice(&PNG_SIG.0);
|
|
98
|
+
|
|
99
|
+
for chunk in chunks {
|
|
100
|
+
let chunk_type = chunk.name.as_bytes();
|
|
101
|
+
if chunk_type.len() != 4 {
|
|
102
|
+
return Err(format!("Invalid chunk type length: {}", chunk.name));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let length = chunk.data.len() as u32;
|
|
106
|
+
output.extend_from_slice(&write_u32_be(length));
|
|
107
|
+
output.extend_from_slice(chunk_type);
|
|
108
|
+
output.extend_from_slice(&chunk.data);
|
|
109
|
+
|
|
110
|
+
let mut crc_data = Vec::new();
|
|
111
|
+
crc_data.extend_from_slice(chunk_type);
|
|
112
|
+
crc_data.extend_from_slice(&chunk.data);
|
|
113
|
+
let crc = crate::core::crc32_bytes(&crc_data);
|
|
114
|
+
output.extend_from_slice(&write_u32_be(crc));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
Ok(output)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
pub fn get_png_metadata(png_data: &[u8]) -> Result<(u32, u32, u8, u8), String> {
|
|
121
|
+
let chunks = extract_png_chunks(png_data)?;
|
|
122
|
+
|
|
123
|
+
let ihdr = chunks.iter()
|
|
124
|
+
.find(|c| c.name == "IHDR")
|
|
125
|
+
.ok_or("IHDR chunk not found")?;
|
|
126
|
+
|
|
127
|
+
if ihdr.data.len() < 13 {
|
|
128
|
+
return Err("Invalid IHDR chunk".to_string());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let width = read_u32_be(&ihdr.data[0..4]);
|
|
132
|
+
let height = read_u32_be(&ihdr.data[4..8]);
|
|
133
|
+
let bit_depth = ihdr.data[8];
|
|
134
|
+
let color_type = ihdr.data[9];
|
|
135
|
+
|
|
136
|
+
Ok((width, height, bit_depth, color_type))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
pub fn extract_payload_from_png(png_data: &[u8]) -> Result<Vec<u8>, String> {
|
|
140
|
+
if let Ok(payload) = extract_payload_direct(png_data) {
|
|
141
|
+
return Ok(payload);
|
|
142
|
+
}
|
|
143
|
+
let reconst = crate::reconstitution::crop_and_reconstitute(png_data)?;
|
|
144
|
+
extract_payload_direct(&reconst)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fn find_pixel_header(raw: &[u8]) -> Result<usize, String> {
|
|
148
|
+
let magic = b"PXL1";
|
|
149
|
+
for i in 0..(raw.len().saturating_sub(magic.len())) {
|
|
150
|
+
if &raw[i..i + magic.len()] == magic {
|
|
151
|
+
return Ok(i);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
Err("PIXEL_MAGIC not found".to_string())
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fn decode_to_rgb(png_data: &[u8]) -> Result<Vec<u8>, String> {
|
|
158
|
+
let img = image::load_from_memory(png_data).map_err(|e| format!("image load error: {}", e))?;
|
|
159
|
+
Ok(img.to_rgb8().into_raw())
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
pub fn extract_name_from_png(png_data: &[u8]) -> Option<String> {
|
|
163
|
+
let raw = decode_to_rgb(png_data).ok()?;
|
|
164
|
+
let pos = find_pixel_header(&raw).ok()?;
|
|
165
|
+
let mut idx = pos + 4;
|
|
166
|
+
if idx + 2 > raw.len() { return None; }
|
|
167
|
+
idx += 1;
|
|
168
|
+
let name_len = raw[idx] as usize; idx += 1;
|
|
169
|
+
if name_len == 0 || idx + name_len > raw.len() { return None; }
|
|
170
|
+
String::from_utf8(raw[idx..idx + name_len].to_vec()).ok()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fn extract_payload_direct(png_data: &[u8]) -> Result<Vec<u8>, String> {
|
|
174
|
+
let raw = decode_to_rgb(png_data)?;
|
|
175
|
+
let pos = find_pixel_header(&raw)?;
|
|
176
|
+
let mut idx = pos + 4;
|
|
177
|
+
if idx + 2 > raw.len() { return Err("Truncated header".to_string()); }
|
|
178
|
+
let _version = raw[idx]; idx += 1;
|
|
179
|
+
let name_len = raw[idx] as usize; idx += 1;
|
|
180
|
+
if idx + name_len > raw.len() { return Err("Truncated name".to_string()); }
|
|
181
|
+
idx += name_len;
|
|
182
|
+
if idx + 4 > raw.len() { return Err("Truncated payload length".to_string()); }
|
|
183
|
+
let payload_len = ((raw[idx] as u32) << 24)
|
|
184
|
+
| ((raw[idx+1] as u32) << 16)
|
|
185
|
+
| ((raw[idx+2] as u32) << 8)
|
|
186
|
+
| (raw[idx+3] as u32);
|
|
187
|
+
idx += 4;
|
|
188
|
+
let end = idx + (payload_len as usize);
|
|
189
|
+
if end > raw.len() { return Err("Truncated payload".to_string()); }
|
|
190
|
+
let payload = raw[idx..end].to_vec();
|
|
191
|
+
Ok(payload)
|
|
192
|
+
}
|