roxify 1.13.4 → 1.13.6
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 +2 -1
- package/README.md +30 -20
- package/dist/cli.js +36 -194
- 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/decoder.d.ts +1 -21
- package/dist/utils/decoder.js +42 -1241
- package/dist/utils/encoder.d.ts +1 -13
- package/dist/utils/encoder.js +36 -559
- package/dist/utils/rust-cli-wrapper.d.ts +2 -2
- package/dist/utils/rust-cli-wrapper.js +8 -2
- package/native/encoder.rs +10 -22
- package/native/io_advice.rs +43 -0
- package/native/io_ntfs_optimized.rs +99 -0
- package/native/lib.rs +2 -0
- package/native/main.rs +329 -57
- package/native/packer.rs +188 -82
- package/native/png_chunk_writer.rs +146 -0
- package/native/png_utils.rs +70 -54
- package/native/streaming.rs +16 -39
- package/native/streaming_decode.rs +263 -110
- package/native/streaming_encode.rs +36 -59
- 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
package/native/main.rs
CHANGED
|
@@ -11,6 +11,8 @@ mod encoder;
|
|
|
11
11
|
mod packer;
|
|
12
12
|
mod crypto;
|
|
13
13
|
mod png_utils;
|
|
14
|
+
mod png_chunk_writer;
|
|
15
|
+
mod io_advice;
|
|
14
16
|
mod audio;
|
|
15
17
|
mod reconstitution;
|
|
16
18
|
mod archive;
|
|
@@ -22,6 +24,34 @@ mod progress;
|
|
|
22
24
|
use crate::encoder::ImageFormat;
|
|
23
25
|
use std::path::PathBuf;
|
|
24
26
|
|
|
27
|
+
const MB_U64: u64 = 1024 * 1024;
|
|
28
|
+
const GB_U64: u64 = 1024 * MB_U64;
|
|
29
|
+
|
|
30
|
+
// Adaptive RAM Budget Configuration
|
|
31
|
+
const MIN_RAM_BUDGET_MB: u64 = 512;
|
|
32
|
+
const DEFAULT_RAM_BUDGET_MB: u64 = 2048;
|
|
33
|
+
const RESERVED_RAM_MB: u64 = 1024;
|
|
34
|
+
const MIN_WRITE_BUFFER_BYTES: usize = 16 * 1024 * 1024;
|
|
35
|
+
const MAX_WRITE_BUFFER_BYTES: usize = 256 * 1024 * 1024;
|
|
36
|
+
|
|
37
|
+
// Performance Targets (under 10 seconds)
|
|
38
|
+
const TARGET_ENCODE_TIME_SECS: u64 = 10;
|
|
39
|
+
const TARGET_DECODE_TIME_SECS: u64 = 10;
|
|
40
|
+
const FAST_COMPRESSION_THRESHOLD_MB: u64 = 100;
|
|
41
|
+
const STREAMING_THRESHOLD_MB: u64 = 500;
|
|
42
|
+
|
|
43
|
+
// Adaptive Compression Levels
|
|
44
|
+
const COMPRESSION_ULTRA_FAST: i32 = 1; // < 100MB files, lots of RAM
|
|
45
|
+
const COMPRESSION_FAST: i32 = 2; // < 500MB files
|
|
46
|
+
const COMPRESSION_BALANCED: i32 = 3; // Default, 500MB-2GB
|
|
47
|
+
const COMPRESSION_SMALL_FILES: i32 = 5; // Many small files
|
|
48
|
+
|
|
49
|
+
// RAM Usage Tiers
|
|
50
|
+
const RAM_TIER_ULTRA: u64 = 16384; // 16GB+ - Aggressive optimization
|
|
51
|
+
const RAM_TIER_HIGH: u64 = 8192; // 8GB+ - Fast mode
|
|
52
|
+
const RAM_TIER_MEDIUM: u64 = 4096; // 4GB+ - Balanced
|
|
53
|
+
const RAM_TIER_LOW: u64 = 2048; // 2GB+ - Conservative
|
|
54
|
+
|
|
25
55
|
#[derive(Parser)]
|
|
26
56
|
#[command(author, version)]
|
|
27
57
|
struct Cli {
|
|
@@ -45,6 +75,8 @@ enum Commands {
|
|
|
45
75
|
/// optional zstd dictionary file for payload compression
|
|
46
76
|
#[arg(long, value_name = "FILE")]
|
|
47
77
|
dict: Option<PathBuf>,
|
|
78
|
+
#[arg(long, value_name = "MB")]
|
|
79
|
+
ram_budget_mb: Option<u64>,
|
|
48
80
|
},
|
|
49
81
|
|
|
50
82
|
List {
|
|
@@ -98,6 +130,8 @@ enum Commands {
|
|
|
98
130
|
/// optional dictionary file used during decompression
|
|
99
131
|
#[arg(long, value_name = "FILE")]
|
|
100
132
|
dict: Option<PathBuf>,
|
|
133
|
+
#[arg(long, value_name = "MB")]
|
|
134
|
+
ram_budget_mb: Option<u64>,
|
|
101
135
|
},
|
|
102
136
|
Crc32 {
|
|
103
137
|
input: PathBuf,
|
|
@@ -116,10 +150,192 @@ fn read_all(path: &PathBuf) -> anyhow::Result<Vec<u8>> {
|
|
|
116
150
|
Ok(buf)
|
|
117
151
|
}
|
|
118
152
|
|
|
153
|
+
pub fn parse_linux_mem_available_mb() -> Option<u64> {
|
|
154
|
+
let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
|
|
155
|
+
let line = meminfo.lines().find(|l| l.starts_with("MemAvailable:"))?;
|
|
156
|
+
let kb = line
|
|
157
|
+
.split_whitespace()
|
|
158
|
+
.nth(1)
|
|
159
|
+
.and_then(|v| v.parse::<u64>().ok())?;
|
|
160
|
+
Some(kb / 1024)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fn parse_total_ram_mb() -> Option<u64> {
|
|
164
|
+
let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
|
|
165
|
+
let line = meminfo.lines().find(|l| l.starts_with("MemTotal:"))?;
|
|
166
|
+
let kb = line
|
|
167
|
+
.split_whitespace()
|
|
168
|
+
.nth(1)
|
|
169
|
+
.and_then(|v| v.parse::<u64>().ok())?;
|
|
170
|
+
Some(kb / 1024)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fn get_cpu_cores() -> usize {
|
|
174
|
+
std::thread::available_parallelism()
|
|
175
|
+
.map(|p| p.get())
|
|
176
|
+
.unwrap_or(4)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// Determine RAM tier based on available memory
|
|
180
|
+
fn get_ram_tier(available_mb: u64) -> &'static str {
|
|
181
|
+
match available_mb {
|
|
182
|
+
x if x >= RAM_TIER_ULTRA => "ultra",
|
|
183
|
+
x if x >= RAM_TIER_HIGH => "high",
|
|
184
|
+
x if x >= RAM_TIER_MEDIUM => "medium",
|
|
185
|
+
_ => "low",
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// Calculate adaptive RAM budget with aggressive optimization for Pyxelze
|
|
190
|
+
fn auto_ram_budget_mb() -> u64 {
|
|
191
|
+
let total_mb = parse_total_ram_mb().unwrap_or(4096);
|
|
192
|
+
let available_mb = parse_linux_mem_available_mb().unwrap_or(total_mb / 2);
|
|
193
|
+
let cpu_cores = get_cpu_cores();
|
|
194
|
+
|
|
195
|
+
// Base calculation: use up to 85% of available RAM for ultra-fast mode
|
|
196
|
+
let base_budget = available_mb.saturating_mul(85) / 100;
|
|
197
|
+
|
|
198
|
+
// Adjust based on RAM tier and CPU cores
|
|
199
|
+
let tier_multiplier = match get_ram_tier(total_mb) {
|
|
200
|
+
"ultra" if cpu_cores >= 8 => 90, // 16GB+ with 8+ cores: 90%
|
|
201
|
+
"ultra" => 85, // 16GB+ with fewer cores
|
|
202
|
+
"high" if cpu_cores >= 6 => 80, // 8GB+ with 6+ cores
|
|
203
|
+
"high" => 75, // 8GB+
|
|
204
|
+
"medium" => 70, // 4GB+
|
|
205
|
+
_ => 65, // 2GB+
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
let budget = available_mb.saturating_mul(tier_multiplier) / 100;
|
|
209
|
+
let budget = budget.max(MIN_RAM_BUDGET_MB);
|
|
210
|
+
|
|
211
|
+
// Cap at reasonable maximum to prevent OOM
|
|
212
|
+
let max_budget = (total_mb / 2).max(8192);
|
|
213
|
+
budget.min(max_budget)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/// Auto-select optimal compression level based on file size and available RAM
|
|
217
|
+
fn auto_compression_level(file_size_mb: u64, ram_budget_mb: u64) -> i32 {
|
|
218
|
+
// Ultra-fast for small files with lots of RAM
|
|
219
|
+
if file_size_mb < FAST_COMPRESSION_THRESHOLD_MB && ram_budget_mb >= RAM_TIER_HIGH {
|
|
220
|
+
return COMPRESSION_ULTRA_FAST;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Fast mode for medium files
|
|
224
|
+
if file_size_mb < STREAMING_THRESHOLD_MB {
|
|
225
|
+
return COMPRESSION_FAST;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Balanced for large files
|
|
229
|
+
if ram_budget_mb >= RAM_TIER_MEDIUM {
|
|
230
|
+
return COMPRESSION_BALANCED;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Conservative for low RAM
|
|
234
|
+
COMPRESSION_SMALL_FILES
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// Determine if streaming mode should be used
|
|
238
|
+
fn should_use_streaming(file_size_mb: u64, ram_budget_mb: u64) -> bool {
|
|
239
|
+
// Force streaming for very large files
|
|
240
|
+
if file_size_mb >= 2048 { // 2GB+
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Use streaming when file is larger than 60% of RAM budget
|
|
245
|
+
let threshold = ram_budget_mb.saturating_mul(60) / 100;
|
|
246
|
+
file_size_mb >= threshold
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// Get optimal thread count for zstd based on RAM and CPU
|
|
250
|
+
fn optimal_zstd_threads(file_size_mb: u64, ram_budget_mb: u64) -> i32 {
|
|
251
|
+
let cpu_cores = get_cpu_cores();
|
|
252
|
+
let ram_tier = get_ram_tier(parse_total_ram_mb().unwrap_or(4096));
|
|
253
|
+
|
|
254
|
+
match ram_tier {
|
|
255
|
+
"ultra" => cpu_cores.min(16) as i32,
|
|
256
|
+
"high" => cpu_cores.min(8) as i32,
|
|
257
|
+
_ if file_size_mb > 1000 => cpu_cores.min(4) as i32,
|
|
258
|
+
_ => cpu_cores.min(2) as i32,
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
fn resolve_ram_budget_mb(cli_value: Option<u64>) -> u64 {
|
|
263
|
+
if let Some(v) = cli_value {
|
|
264
|
+
return v.max(MIN_RAM_BUDGET_MB);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if let Ok(v) = std::env::var("ROX_RAM_BUDGET_MB") {
|
|
268
|
+
if let Ok(parsed) = v.trim().parse::<u64>() {
|
|
269
|
+
return parsed.max(MIN_RAM_BUDGET_MB);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
auto_ram_budget_mb()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
fn effective_ram_budget_mb() -> u64 {
|
|
277
|
+
if let Ok(v) = std::env::var("ROX_RAM_BUDGET_MB_EFFECTIVE") {
|
|
278
|
+
if let Ok(parsed) = v.trim().parse::<u64>() {
|
|
279
|
+
return parsed.max(MIN_RAM_BUDGET_MB);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
resolve_ram_budget_mb(None)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
fn ram_budget_bytes(ram_budget_mb: u64) -> u64 {
|
|
286
|
+
ram_budget_mb.saturating_mul(MB_U64)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
fn streaming_preference_threshold_bytes(ram_budget_mb: u64) -> u64 {
|
|
290
|
+
ram_budget_bytes(ram_budget_mb)
|
|
291
|
+
.saturating_mul(60)
|
|
292
|
+
.saturating_div(100)
|
|
293
|
+
.max(64 * MB_U64)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fn normalize_png_archive_bytes(png_data: &[u8], passphrase: Option<&str>) -> anyhow::Result<Vec<u8>> {
|
|
297
|
+
let payload = png_utils::extract_payload_from_png(png_data).map_err(|e| anyhow::anyhow!(e))?;
|
|
298
|
+
if payload.is_empty() {
|
|
299
|
+
return Err(anyhow::anyhow!("Empty payload"));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if payload[0] == 0x00u8 {
|
|
303
|
+
Ok(payload[1..].to_vec())
|
|
304
|
+
} else {
|
|
305
|
+
let decrypted = crate::crypto::try_decrypt(&payload, passphrase)
|
|
306
|
+
.map_err(|e| anyhow::anyhow!("Encrypted payload: {}", e))?;
|
|
307
|
+
Ok(decrypted)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
fn unpack_archive_bytes(
|
|
312
|
+
normalized: Vec<u8>,
|
|
313
|
+
out_dir: &std::path::Path,
|
|
314
|
+
files_slice: Option<&[String]>,
|
|
315
|
+
progress: Option<&(dyn Fn(u64, u64, &str) + Send)>,
|
|
316
|
+
) -> anyhow::Result<Vec<String>> {
|
|
317
|
+
let mut reader: Box<dyn std::io::Read> = if normalized.starts_with(b"ROX1") {
|
|
318
|
+
Box::new(std::io::Cursor::new(normalized[4..].to_vec()))
|
|
319
|
+
} else {
|
|
320
|
+
let mut dec = zstd::stream::Decoder::new(std::io::Cursor::new(normalized))
|
|
321
|
+
.map_err(|e| anyhow::anyhow!("zstd decoder init: {}", e))?;
|
|
322
|
+
dec.window_log_max(31)
|
|
323
|
+
.map_err(|e| anyhow::anyhow!("zstd window_log_max: {}", e))?;
|
|
324
|
+
Box::new(dec)
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
std::fs::create_dir_all(out_dir)
|
|
328
|
+
.map_err(|e| anyhow::anyhow!("Cannot create output directory {:?}: {}", out_dir, e))?;
|
|
329
|
+
packer::unpack_stream_to_dir(&mut reader, out_dir, files_slice, progress, 0)
|
|
330
|
+
.map_err(|e| anyhow::anyhow!(e))
|
|
331
|
+
}
|
|
332
|
+
|
|
119
333
|
fn write_all(path: &PathBuf, data: &[u8]) -> anyhow::Result<()> {
|
|
120
334
|
let f = File::create(path)?;
|
|
121
|
-
let
|
|
122
|
-
|
|
335
|
+
let budget_bytes = ram_budget_bytes(effective_ram_budget_mb());
|
|
336
|
+
let dynamic_cap = (budget_bytes / 24)
|
|
337
|
+
.clamp(MIN_WRITE_BUFFER_BYTES as u64, MAX_WRITE_BUFFER_BYTES as u64) as usize;
|
|
338
|
+
let buf_size = dynamic_cap.min(data.len().max(8192));
|
|
123
339
|
let mut writer = std::io::BufWriter::with_capacity(buf_size, f);
|
|
124
340
|
writer.write_all(data)?;
|
|
125
341
|
writer.flush()?;
|
|
@@ -164,7 +380,9 @@ fn parse_requested_files(files: &str) -> anyhow::Result<Vec<String>> {
|
|
|
164
380
|
println!("wrote {} bytes dictionary to {:?}", dict.len(), output);
|
|
165
381
|
return Ok(());
|
|
166
382
|
}
|
|
167
|
-
Commands::Encode { input, output, level, passphrase, encrypt, name, dict } => {
|
|
383
|
+
Commands::Encode { input, output, level, passphrase, encrypt, name, dict, ram_budget_mb } => {
|
|
384
|
+
let ram_budget_mb = resolve_ram_budget_mb(ram_budget_mb);
|
|
385
|
+
std::env::set_var("ROX_RAM_BUDGET_MB_EFFECTIVE", ram_budget_mb.to_string());
|
|
168
386
|
let is_dir = input.is_dir();
|
|
169
387
|
|
|
170
388
|
let file_name = name.as_deref()
|
|
@@ -203,7 +421,7 @@ fn parse_requested_files(files: &str) -> anyhow::Result<Vec<String>> {
|
|
|
203
421
|
None => None,
|
|
204
422
|
};
|
|
205
423
|
|
|
206
|
-
let use_streaming = payload.len()
|
|
424
|
+
let use_streaming = (payload.len() as u64) > streaming_preference_threshold_bytes(ram_budget_mb);
|
|
207
425
|
eprintln!("PROGRESS:50:100:encoding");
|
|
208
426
|
|
|
209
427
|
if use_streaming {
|
|
@@ -255,32 +473,39 @@ fn parse_requested_files(files: &str) -> anyhow::Result<Vec<String>> {
|
|
|
255
473
|
}
|
|
256
474
|
Commands::List { input } => {
|
|
257
475
|
let mut file = File::open(&input)?;
|
|
258
|
-
let
|
|
476
|
+
let mut chunk_scan_error: Option<anyhow::Error> = None;
|
|
259
477
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
478
|
+
match png_utils::extract_png_chunks_streaming(&mut file) {
|
|
479
|
+
Ok(chunks) => {
|
|
480
|
+
if let Some(rxfl_chunk) = chunks.iter().find(|c| c.name == "rXFL") {
|
|
481
|
+
println!("{}", String::from_utf8_lossy(&rxfl_chunk.data));
|
|
482
|
+
return Ok(());
|
|
483
|
+
}
|
|
264
484
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
485
|
+
if let Some(meta_chunk) = chunks.iter().find(|c| c.name == "rOXm") {
|
|
486
|
+
if let Some(pos) = meta_chunk.data.windows(4).position(|w| w == b"rXFL") {
|
|
487
|
+
if pos + 8 <= meta_chunk.data.len() {
|
|
488
|
+
let json_len = u32::from_be_bytes([
|
|
489
|
+
meta_chunk.data[pos + 4],
|
|
490
|
+
meta_chunk.data[pos + 5],
|
|
491
|
+
meta_chunk.data[pos + 6],
|
|
492
|
+
meta_chunk.data[pos + 7],
|
|
493
|
+
]) as usize;
|
|
494
|
+
|
|
495
|
+
let json_start = pos + 8;
|
|
496
|
+
let json_end = json_start + json_len;
|
|
497
|
+
|
|
498
|
+
if json_end <= meta_chunk.data.len() {
|
|
499
|
+
println!("{}", String::from_utf8_lossy(&meta_chunk.data[json_start..json_end]));
|
|
500
|
+
return Ok(());
|
|
501
|
+
}
|
|
502
|
+
}
|
|
281
503
|
}
|
|
282
504
|
}
|
|
283
505
|
}
|
|
506
|
+
Err(err) => {
|
|
507
|
+
chunk_scan_error = Some(anyhow::anyhow!(err));
|
|
508
|
+
}
|
|
284
509
|
}
|
|
285
510
|
|
|
286
511
|
let png_data = std::fs::read(&input)?;
|
|
@@ -289,11 +514,14 @@ fn parse_requested_files(files: &str) -> anyhow::Result<Vec<String>> {
|
|
|
289
514
|
println!("{}", json);
|
|
290
515
|
return Ok(());
|
|
291
516
|
}
|
|
292
|
-
Err(
|
|
517
|
+
Err(pixel_err) => {
|
|
518
|
+
if let Some(chunk_err) = chunk_scan_error {
|
|
519
|
+
return Err(anyhow::anyhow!("chunk scan: {}; pixel scan: {}", chunk_err, pixel_err));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
293
522
|
}
|
|
294
523
|
|
|
295
|
-
|
|
296
|
-
std::process::exit(1);
|
|
524
|
+
return Err(anyhow::anyhow!("No file list found in PNG"));
|
|
297
525
|
}
|
|
298
526
|
Commands::Havepassphrase { input } => {
|
|
299
527
|
let buf = read_all(&input)?;
|
|
@@ -347,7 +575,9 @@ fn parse_requested_files(files: &str) -> anyhow::Result<Vec<String>> {
|
|
|
347
575
|
let dest = output.unwrap_or_else(|| PathBuf::from("out.zst"));
|
|
348
576
|
write_all(&dest, &out)?;
|
|
349
577
|
}
|
|
350
|
-
Commands::Decompress { input, output, files, passphrase, dict } => {
|
|
578
|
+
Commands::Decompress { input, output, files, passphrase, dict, ram_budget_mb } => {
|
|
579
|
+
let ram_budget_mb = resolve_ram_budget_mb(ram_budget_mb);
|
|
580
|
+
std::env::set_var("ROX_RAM_BUDGET_MB_EFFECTIVE", ram_budget_mb.to_string());
|
|
351
581
|
let file_size = std::fs::metadata(&input).map(|m| m.len()).unwrap_or(0);
|
|
352
582
|
let is_png_file = input.extension().map(|e| e == "png").unwrap_or(false)
|
|
353
583
|
|| (file_size >= 8 && {
|
|
@@ -361,45 +591,87 @@ fn parse_requested_files(files: &str) -> anyhow::Result<Vec<String>> {
|
|
|
361
591
|
None => None,
|
|
362
592
|
};
|
|
363
593
|
|
|
364
|
-
if is_png_file &&
|
|
365
|
-
let out_dir =
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
594
|
+
if is_png_file && dict.is_none() {
|
|
595
|
+
let out_dir = if requested_files.is_some() {
|
|
596
|
+
output.clone().unwrap_or_else(|| PathBuf::from("."))
|
|
597
|
+
} else {
|
|
598
|
+
output.clone().unwrap_or_else(|| PathBuf::from("out.raw"))
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
let should_try_streaming = file_size >= streaming_preference_threshold_bytes(ram_budget_mb);
|
|
602
|
+
|
|
603
|
+
if should_try_streaming {
|
|
604
|
+
let streaming_result = if let Some(ref selected) = requested_files {
|
|
605
|
+
streaming_decode::streaming_decode_selected_to_dir_encrypted_with_progress(
|
|
606
|
+
&input,
|
|
607
|
+
&out_dir,
|
|
608
|
+
Some(selected.as_slice()),
|
|
609
|
+
passphrase.as_deref(),
|
|
610
|
+
Some(Box::new(|current, total, step| {
|
|
611
|
+
eprintln!("PROGRESS:{}:{}:{}", current, total, step);
|
|
612
|
+
})),
|
|
613
|
+
)
|
|
614
|
+
} else {
|
|
615
|
+
streaming_decode::streaming_decode_to_dir_encrypted_with_progress(
|
|
616
|
+
&input,
|
|
617
|
+
&out_dir,
|
|
618
|
+
passphrase.as_deref(),
|
|
619
|
+
Some(Box::new(|current, total, step| {
|
|
620
|
+
eprintln!("PROGRESS:{}:{}:{}", current, total, step);
|
|
621
|
+
})),
|
|
622
|
+
)
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
match streaming_result {
|
|
626
|
+
Ok(written) => {
|
|
627
|
+
eprintln!("PROGRESS:100:100:done");
|
|
628
|
+
println!("Unpacked {} files", written.len());
|
|
629
|
+
return Ok(());
|
|
630
|
+
}
|
|
631
|
+
Err(e) => {
|
|
632
|
+
eprintln!("PROGRESS:12:100:streaming_fallback");
|
|
633
|
+
eprintln!("Streaming decode failed, falling back to reconstruction: {}", e);
|
|
634
|
+
}
|
|
381
635
|
}
|
|
382
636
|
}
|
|
383
|
-
}
|
|
384
637
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
638
|
+
if file_size > ram_budget_bytes(ram_budget_mb) {
|
|
639
|
+
return Err(anyhow::anyhow!(
|
|
640
|
+
"PNG fallback requires in-memory reconstruction ({} MB file > {} MB RAM budget). Increase --ram-budget-mb.",
|
|
641
|
+
file_size / MB_U64,
|
|
642
|
+
ram_budget_mb
|
|
643
|
+
));
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
let buf = read_all(&input)?;
|
|
647
|
+
|
|
648
|
+
eprintln!("PROGRESS:20:100:decompressing");
|
|
649
|
+
let progress_cb = |current: u64, total: u64, step: &str| {
|
|
650
|
+
eprintln!("PROGRESS:{}:{}:{}", current, total, step);
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
let normalized = normalize_png_archive_bytes(&buf, passphrase.as_deref())?;
|
|
654
|
+
let written = unpack_archive_bytes(
|
|
655
|
+
normalized,
|
|
390
656
|
&out_dir,
|
|
391
657
|
requested_files.as_deref(),
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
eprintln!("PROGRESS:{}:{}:{}", current, total, step);
|
|
395
|
-
})),
|
|
396
|
-
).map_err(|e| anyhow::anyhow!(e))?;
|
|
658
|
+
Some(&progress_cb),
|
|
659
|
+
)?;
|
|
397
660
|
eprintln!("PROGRESS:100:100:done");
|
|
398
661
|
println!("Unpacked {} files", written.len());
|
|
399
662
|
return Ok(());
|
|
400
663
|
}
|
|
401
664
|
|
|
665
|
+
if file_size > ram_budget_bytes(ram_budget_mb) {
|
|
666
|
+
return Err(anyhow::anyhow!(
|
|
667
|
+
"Input is {} MB but RAM budget is {} MB. Increase --ram-budget-mb for non-streaming decode paths.",
|
|
668
|
+
file_size / MB_U64,
|
|
669
|
+
ram_budget_mb
|
|
670
|
+
));
|
|
671
|
+
}
|
|
672
|
+
|
|
402
673
|
let buf = read_all(&input)?;
|
|
674
|
+
|
|
403
675
|
eprintln!("PROGRESS:20:100:decompressing");
|
|
404
676
|
let dict_bytes: Option<Vec<u8>> = match dict {
|
|
405
677
|
Some(path) => Some(read_all(&path)?),
|