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/Cargo.toml +3 -1
- package/README.md +17 -13
- package/dist/cli.js +14 -19
- package/dist/rox-macos-universal +0 -0
- package/dist/roxify_native +0 -0
- package/dist/roxify_native-macos-arm64 +0 -0
- package/dist/roxify_native-macos-x64 +0 -0
- package/dist/roxify_native.exe +0 -0
- package/dist/utils/rust-cli-wrapper.d.ts +3 -2
- package/dist/utils/rust-cli-wrapper.js +26 -6
- package/native/core.rs +3 -48
- package/native/crypto.rs +5 -0
- package/native/encoder.rs +10 -228
- package/native/lib.rs +54 -39
- package/native/main.rs +53 -21
- package/native/packer.rs +57 -6
- package/native/streaming_decode.rs +141 -7
- package/native/streaming_encode.rs +284 -112
- package/package.json +1 -1
- package/roxify_native-aarch64-apple-darwin.node +0 -0
- package/roxify_native-aarch64-pc-windows-msvc.node +0 -0
- package/roxify_native-aarch64-unknown-linux-gnu.node +0 -0
- package/roxify_native-i686-pc-windows-msvc.node +0 -0
- package/roxify_native-i686-unknown-linux-gnu.node +0 -0
- package/roxify_native-x86_64-apple-darwin.node +0 -0
- package/roxify_native-x86_64-pc-windows-msvc.node +0 -0
- package/roxify_native-x86_64-unknown-linux-gnu.node +0 -0
|
@@ -4,17 +4,39 @@ use cipher::{KeyIvInit, StreamCipher};
|
|
|
4
4
|
|
|
5
5
|
const PIXEL_MAGIC: &[u8] = b"PXL1";
|
|
6
6
|
const MARKER_BYTES: usize = 12;
|
|
7
|
+
const PACK_MAGIC: [u8; 4] = 0x524f5850u32.to_be_bytes();
|
|
7
8
|
|
|
8
9
|
type Aes256Ctr = ctr::Ctr64BE<aes::Aes256>;
|
|
9
10
|
|
|
11
|
+
pub type DecodeProgressCallback = Box<dyn Fn(u64, u64, &str) + Send>;
|
|
12
|
+
|
|
10
13
|
pub fn streaming_decode_to_dir(png_path: &Path, out_dir: &Path) -> Result<Vec<String>, String> {
|
|
11
|
-
|
|
14
|
+
streaming_decode_selected_to_dir_encrypted_with_progress(png_path, out_dir, None, None, None)
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
pub fn streaming_decode_to_dir_encrypted(
|
|
15
18
|
png_path: &Path,
|
|
16
19
|
out_dir: &Path,
|
|
17
20
|
passphrase: Option<&str>,
|
|
21
|
+
) -> Result<Vec<String>, String> {
|
|
22
|
+
streaming_decode_selected_to_dir_encrypted_with_progress(png_path, out_dir, None, passphrase, None)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
pub fn streaming_decode_to_dir_encrypted_with_progress(
|
|
26
|
+
png_path: &Path,
|
|
27
|
+
out_dir: &Path,
|
|
28
|
+
passphrase: Option<&str>,
|
|
29
|
+
progress: Option<DecodeProgressCallback>,
|
|
30
|
+
) -> Result<Vec<String>, String> {
|
|
31
|
+
streaming_decode_selected_to_dir_encrypted_with_progress(png_path, out_dir, None, passphrase, progress)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pub fn streaming_decode_selected_to_dir_encrypted_with_progress(
|
|
35
|
+
png_path: &Path,
|
|
36
|
+
out_dir: &Path,
|
|
37
|
+
files_opt: Option<&[String]>,
|
|
38
|
+
passphrase: Option<&str>,
|
|
39
|
+
progress: Option<DecodeProgressCallback>,
|
|
18
40
|
) -> Result<Vec<String>, String> {
|
|
19
41
|
let file = std::fs::File::open(png_path).map_err(|e| format!("open: {}", e))?;
|
|
20
42
|
let mmap = unsafe { memmap2::Mmap::map(&file).map_err(|e| format!("mmap: {}", e))? };
|
|
@@ -24,7 +46,16 @@ pub fn streaming_decode_to_dir_encrypted(
|
|
|
24
46
|
return Err("Not a PNG file".into());
|
|
25
47
|
}
|
|
26
48
|
|
|
49
|
+
if let Some(ref cb) = progress {
|
|
50
|
+
cb(2, 100, "parsing_png");
|
|
51
|
+
}
|
|
52
|
+
|
|
27
53
|
let (width, height, idat_data_start, idat_data_end) = parse_png_header(data)?;
|
|
54
|
+
let total_expected = parse_rxfl_total_bytes(data).unwrap_or(0);
|
|
55
|
+
|
|
56
|
+
if let Some(ref cb) = progress {
|
|
57
|
+
cb(5, 100, "reading_header");
|
|
58
|
+
}
|
|
28
59
|
|
|
29
60
|
let mut reader = DeflatePixelReader::new(data, width, height, idat_data_start, idat_data_end);
|
|
30
61
|
|
|
@@ -51,6 +82,10 @@ pub fn streaming_decode_to_dir_encrypted(
|
|
|
51
82
|
reader.read_exact(&mut plen_buf).map_err(|e| format!("read payload_len: {}", e))?;
|
|
52
83
|
let payload_len = u32::from_be_bytes(plen_buf) as u64;
|
|
53
84
|
|
|
85
|
+
if let Some(ref cb) = progress {
|
|
86
|
+
cb(8, 100, "decrypting");
|
|
87
|
+
}
|
|
88
|
+
|
|
54
89
|
let payload_reader = reader.take(payload_len);
|
|
55
90
|
|
|
56
91
|
let first_byte_reader = FirstByteReader::new(payload_reader);
|
|
@@ -58,10 +93,13 @@ pub fn streaming_decode_to_dir_encrypted(
|
|
|
58
93
|
|
|
59
94
|
match enc_byte {
|
|
60
95
|
0x00 => {
|
|
96
|
+
if let Some(ref cb) = progress {
|
|
97
|
+
cb(10, 100, "decompressing");
|
|
98
|
+
}
|
|
61
99
|
let mut decoder = zstd::stream::Decoder::new(remaining_reader)
|
|
62
100
|
.map_err(|e| format!("zstd decoder: {}", e))?;
|
|
63
101
|
decoder.window_log_max(31).map_err(|e| format!("zstd window_log_max: {}", e))?;
|
|
64
|
-
|
|
102
|
+
read_rox1_and_unpack_with_progress(decoder, out_dir, files_opt, progress, total_expected)
|
|
65
103
|
}
|
|
66
104
|
0x03 => {
|
|
67
105
|
let pass = passphrase.ok_or("Passphrase required for AES-CTR decryption")?;
|
|
@@ -79,23 +117,42 @@ pub fn streaming_decode_to_dir_encrypted(
|
|
|
79
117
|
let encrypted_data_len = payload_len - 1 - 16 - 16 - hmac_size;
|
|
80
118
|
let ctr_reader = CtrDecryptReader::new(r.take(encrypted_data_len), cipher);
|
|
81
119
|
|
|
120
|
+
if let Some(ref cb) = progress {
|
|
121
|
+
cb(10, 100, "decompressing");
|
|
122
|
+
}
|
|
82
123
|
let mut decoder = zstd::stream::Decoder::new(ctr_reader)
|
|
83
124
|
.map_err(|e| format!("zstd decoder: {}", e))?;
|
|
84
125
|
decoder.window_log_max(31).map_err(|e| format!("zstd window_log_max: {}", e))?;
|
|
85
|
-
|
|
126
|
+
read_rox1_and_unpack_with_progress(decoder, out_dir, files_opt, progress, total_expected)
|
|
86
127
|
}
|
|
87
128
|
_ => Err(format!("Unsupported encryption (enc=0x{:02x}) in streaming decode", enc_byte)),
|
|
88
129
|
}
|
|
89
130
|
}
|
|
90
131
|
|
|
91
|
-
fn
|
|
132
|
+
fn read_rox1_and_unpack_with_progress<R: Read>(
|
|
133
|
+
mut decoder: R,
|
|
134
|
+
out_dir: &Path,
|
|
135
|
+
files_opt: Option<&[String]>,
|
|
136
|
+
progress: Option<DecodeProgressCallback>,
|
|
137
|
+
total_expected: u64,
|
|
138
|
+
) -> Result<Vec<String>, String> {
|
|
92
139
|
let mut magic = [0u8; 4];
|
|
93
140
|
decoder.read_exact(&mut magic).map_err(|e| format!("read ROX1: {}", e))?;
|
|
94
141
|
if &magic != b"ROX1" {
|
|
95
142
|
return Err(format!("Expected ROX1, got {:?}", magic));
|
|
96
143
|
}
|
|
97
144
|
std::fs::create_dir_all(out_dir).map_err(|e| format!("mkdir: {}", e))?;
|
|
98
|
-
|
|
145
|
+
|
|
146
|
+
let mut prefix = [0u8; 4];
|
|
147
|
+
decoder.read_exact(&mut prefix).map_err(|e| format!("read payload magic: {}", e))?;
|
|
148
|
+
let mut chained = std::io::Cursor::new(prefix).chain(decoder);
|
|
149
|
+
|
|
150
|
+
if prefix == PACK_MAGIC {
|
|
151
|
+
crate::packer::unpack_stream_to_dir(&mut chained, out_dir, files_opt, progress.as_deref(), total_expected)
|
|
152
|
+
.map_err(|e| format!("pack unpack: {}", e))
|
|
153
|
+
} else {
|
|
154
|
+
tar_unpack_from_reader_with_progress(chained, out_dir, files_opt, progress, total_expected)
|
|
155
|
+
}
|
|
99
156
|
}
|
|
100
157
|
|
|
101
158
|
fn parse_png_header(data: &[u8]) -> Result<(usize, usize, usize, usize), String> {
|
|
@@ -147,6 +204,30 @@ fn parse_png_header(data: &[u8]) -> Result<(usize, usize, usize, usize), String>
|
|
|
147
204
|
Ok((width, height, idat_start, idat_end))
|
|
148
205
|
}
|
|
149
206
|
|
|
207
|
+
fn parse_rxfl_total_bytes(data: &[u8]) -> Option<u64> {
|
|
208
|
+
let mut pos = 8;
|
|
209
|
+
while pos + 12 <= data.len() {
|
|
210
|
+
let chunk_len = u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
|
|
211
|
+
let chunk_type = &data[pos + 4..pos + 8];
|
|
212
|
+
let chunk_data_start = pos + 8;
|
|
213
|
+
|
|
214
|
+
if chunk_type == b"rXFL" && chunk_data_start + chunk_len <= data.len() {
|
|
215
|
+
let json_bytes = &data[chunk_data_start..chunk_data_start + chunk_len];
|
|
216
|
+
if let Ok(entries) = serde_json::from_slice::<Vec<serde_json::Value>>(json_bytes) {
|
|
217
|
+
let total: u64 = entries.iter()
|
|
218
|
+
.filter_map(|e| e.get("size").and_then(|s| s.as_u64()))
|
|
219
|
+
.sum();
|
|
220
|
+
return Some(total);
|
|
221
|
+
}
|
|
222
|
+
} else if chunk_type == b"IEND" {
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
pos = chunk_data_start + chunk_len + 4;
|
|
227
|
+
}
|
|
228
|
+
None
|
|
229
|
+
}
|
|
230
|
+
|
|
150
231
|
struct DeflatePixelReader<'a> {
|
|
151
232
|
data: &'a [u8],
|
|
152
233
|
height: usize,
|
|
@@ -298,16 +379,29 @@ impl<R: Read> Read for CtrDecryptReader<R> {
|
|
|
298
379
|
}
|
|
299
380
|
}
|
|
300
381
|
|
|
301
|
-
fn
|
|
382
|
+
fn tar_unpack_from_reader_with_progress<R: Read>(
|
|
383
|
+
reader: R,
|
|
384
|
+
output_dir: &Path,
|
|
385
|
+
files_opt: Option<&[String]>,
|
|
386
|
+
progress: Option<DecodeProgressCallback>,
|
|
387
|
+
total_expected: u64,
|
|
388
|
+
) -> Result<Vec<String>, String> {
|
|
302
389
|
let buf_reader = std::io::BufReader::with_capacity(8 * 1024 * 1024, reader);
|
|
303
390
|
let mut archive = tar::Archive::new(buf_reader);
|
|
304
391
|
let mut written = Vec::new();
|
|
305
392
|
let mut created_dirs = std::collections::HashSet::new();
|
|
393
|
+
let mut bytes_extracted: u64 = 0;
|
|
394
|
+
let mut last_pct: u64 = 10;
|
|
395
|
+
let files_filter: Option<std::collections::HashSet<&str>> = files_opt.map(|files| files.iter().map(|file| file.as_str()).collect());
|
|
396
|
+
let mut remaining = files_filter.as_ref().map(|files| files.len()).unwrap_or(usize::MAX);
|
|
306
397
|
|
|
307
398
|
let entries = archive.entries().map_err(|e| format!("tar entries: {}", e))?;
|
|
308
399
|
for entry in entries {
|
|
309
400
|
let mut entry = entry.map_err(|e| format!("tar entry: {}", e))?;
|
|
401
|
+
let entry_size = entry.size();
|
|
310
402
|
let path = entry.path().map_err(|e| format!("tar path: {}", e))?.to_path_buf();
|
|
403
|
+
let logical_path = path.to_string_lossy().replace('\\', "/");
|
|
404
|
+
let should_write = files_filter.as_ref().map(|files| files.contains(logical_path.as_str())).unwrap_or(true);
|
|
311
405
|
|
|
312
406
|
let mut safe = std::path::PathBuf::new();
|
|
313
407
|
for comp in path.components() {
|
|
@@ -319,6 +413,23 @@ fn tar_unpack_from_reader<R: Read>(reader: R, output_dir: &Path) -> Result<Vec<S
|
|
|
319
413
|
continue;
|
|
320
414
|
}
|
|
321
415
|
|
|
416
|
+
if !should_write {
|
|
417
|
+
std::io::copy(&mut entry, &mut std::io::sink()).map_err(|e| format!("skip {:?}: {}", safe, e))?;
|
|
418
|
+
bytes_extracted += entry_size;
|
|
419
|
+
if let Some(ref cb) = progress {
|
|
420
|
+
let pct = if total_expected > 0 {
|
|
421
|
+
10 + (bytes_extracted * 89 / total_expected).min(89)
|
|
422
|
+
} else {
|
|
423
|
+
(10 + (bytes_extracted / (1024 * 1024))).min(99)
|
|
424
|
+
};
|
|
425
|
+
if pct > last_pct {
|
|
426
|
+
last_pct = pct;
|
|
427
|
+
cb(pct, 100, "extracting");
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
|
|
322
433
|
let dest = output_dir.join(&safe);
|
|
323
434
|
if let Some(parent) = dest.parent() {
|
|
324
435
|
if created_dirs.insert(parent.to_path_buf()) {
|
|
@@ -327,11 +438,34 @@ fn tar_unpack_from_reader<R: Read>(reader: R, output_dir: &Path) -> Result<Vec<S
|
|
|
327
438
|
}
|
|
328
439
|
|
|
329
440
|
let mut f = std::io::BufWriter::with_capacity(
|
|
330
|
-
(
|
|
441
|
+
(entry_size as usize).min(4 * 1024 * 1024).max(8192),
|
|
331
442
|
std::fs::File::create(&dest).map_err(|e| format!("create {:?}: {}", dest, e))?,
|
|
332
443
|
);
|
|
333
444
|
std::io::copy(&mut entry, &mut f).map_err(|e| format!("write {:?}: {}", dest, e))?;
|
|
334
445
|
written.push(safe.to_string_lossy().to_string());
|
|
446
|
+
if files_filter.is_some() {
|
|
447
|
+
remaining = remaining.saturating_sub(1);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
bytes_extracted += entry_size;
|
|
451
|
+
if let Some(ref cb) = progress {
|
|
452
|
+
let pct = if total_expected > 0 {
|
|
453
|
+
10 + (bytes_extracted * 89 / total_expected).min(89)
|
|
454
|
+
} else {
|
|
455
|
+
(10 + (bytes_extracted / (1024 * 1024))).min(99)
|
|
456
|
+
};
|
|
457
|
+
if pct > last_pct {
|
|
458
|
+
last_pct = pct;
|
|
459
|
+
cb(pct, 100, "extracting");
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if remaining == 0 {
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if let Some(ref cb) = progress {
|
|
468
|
+
cb(99, 100, "finishing");
|
|
335
469
|
}
|
|
336
470
|
|
|
337
471
|
Ok(written)
|