roxify 1.13.8 → 1.13.9
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 -8
- package/dist/stub-progress.d.ts +4 -4
- package/dist/stub-progress.js +4 -4
- package/dist/utils/decoder.d.ts +10 -1
- package/dist/utils/decoder.js +111 -7
- package/dist/utils/ecc.js +0 -1
- package/dist/utils/inspection.d.ts +1 -1
- package/dist/utils/inspection.js +2 -2
- package/dist/utils/robust-audio.js +0 -13
- package/dist/utils/robust-image.js +0 -26
- package/package.json +12 -29
- 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/{dist/rox-macos-universal → roxify_native-universal-apple-darwin.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/scripts/postinstall.cjs +23 -2
- package/Cargo.toml +0 -91
- 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/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 -842
- package/native/mtf.rs +0 -106
- package/native/packer.rs +0 -604
- 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 -625
- package/native/streaming_encode.rs +0 -684
- package/native/test_small_bwt.rs +0 -31
- package/native/test_stages.rs +0 -70
package/native/main.rs
DELETED
|
@@ -1,842 +0,0 @@
|
|
|
1
|
-
#![allow(dead_code)]
|
|
2
|
-
use clap::{Parser, Subcommand};
|
|
3
|
-
use std::fs::File;
|
|
4
|
-
use std::io::{Read, Write};
|
|
5
|
-
|
|
6
|
-
#[global_allocator]
|
|
7
|
-
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
|
8
|
-
|
|
9
|
-
mod core;
|
|
10
|
-
mod encoder;
|
|
11
|
-
mod packer;
|
|
12
|
-
mod crypto;
|
|
13
|
-
mod png_utils;
|
|
14
|
-
mod png_chunk_writer;
|
|
15
|
-
mod io_advice;
|
|
16
|
-
mod audio;
|
|
17
|
-
mod reconstitution;
|
|
18
|
-
mod archive;
|
|
19
|
-
mod streaming;
|
|
20
|
-
mod streaming_decode;
|
|
21
|
-
mod streaming_encode;
|
|
22
|
-
mod progress;
|
|
23
|
-
|
|
24
|
-
use crate::encoder::ImageFormat;
|
|
25
|
-
use std::path::PathBuf;
|
|
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
|
-
|
|
55
|
-
#[derive(Parser)]
|
|
56
|
-
#[command(author, version)]
|
|
57
|
-
struct Cli {
|
|
58
|
-
#[command(subcommand)]
|
|
59
|
-
command: Commands,
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
#[derive(Subcommand)]
|
|
63
|
-
enum Commands {
|
|
64
|
-
Encode {
|
|
65
|
-
input: PathBuf,
|
|
66
|
-
output: PathBuf,
|
|
67
|
-
#[arg(short, long, default_value_t = 3)]
|
|
68
|
-
level: i32,
|
|
69
|
-
#[arg(short, long)]
|
|
70
|
-
passphrase: Option<String>,
|
|
71
|
-
#[arg(short, long, default_value = "aes")]
|
|
72
|
-
encrypt: String,
|
|
73
|
-
#[arg(short, long)]
|
|
74
|
-
name: Option<String>,
|
|
75
|
-
/// optional zstd dictionary file for payload compression
|
|
76
|
-
#[arg(long, value_name = "FILE")]
|
|
77
|
-
dict: Option<PathBuf>,
|
|
78
|
-
#[arg(long, value_name = "MB")]
|
|
79
|
-
ram_budget_mb: Option<u64>,
|
|
80
|
-
},
|
|
81
|
-
|
|
82
|
-
List {
|
|
83
|
-
input: PathBuf,
|
|
84
|
-
},
|
|
85
|
-
Havepassphrase {
|
|
86
|
-
input: PathBuf,
|
|
87
|
-
},
|
|
88
|
-
Scan {
|
|
89
|
-
input: PathBuf,
|
|
90
|
-
#[arg(short, long, value_name = "FILE")]
|
|
91
|
-
#[arg(short, long, default_value_t = 4)]
|
|
92
|
-
channels: usize,
|
|
93
|
-
#[arg(short, long, value_delimiter = ',')]
|
|
94
|
-
markers: Vec<String>,
|
|
95
|
-
},
|
|
96
|
-
DeltaEncode {
|
|
97
|
-
input: PathBuf,
|
|
98
|
-
output: Option<PathBuf>,
|
|
99
|
-
},
|
|
100
|
-
DeltaDecode {
|
|
101
|
-
input: PathBuf,
|
|
102
|
-
output: Option<PathBuf>,
|
|
103
|
-
},
|
|
104
|
-
Compress {
|
|
105
|
-
input: PathBuf,
|
|
106
|
-
output: Option<PathBuf>,
|
|
107
|
-
#[arg(short, long, default_value_t = 19)]
|
|
108
|
-
level: i32,
|
|
109
|
-
/// optional zstd dictionary file to use for compression
|
|
110
|
-
#[arg(long, value_name = "FILE")]
|
|
111
|
-
dict: Option<PathBuf>,
|
|
112
|
-
},
|
|
113
|
-
TrainDict {
|
|
114
|
-
/// sample files used to train the dictionary
|
|
115
|
-
#[arg(short, long, value_name = "FILE", required = true)]
|
|
116
|
-
samples: Vec<PathBuf>,
|
|
117
|
-
/// desired dictionary size in bytes
|
|
118
|
-
#[arg(short, long, default_value_t = 112640)]
|
|
119
|
-
size: usize,
|
|
120
|
-
/// output dictionary file
|
|
121
|
-
output: PathBuf,
|
|
122
|
-
},
|
|
123
|
-
Decompress {
|
|
124
|
-
input: PathBuf,
|
|
125
|
-
output: Option<PathBuf>,
|
|
126
|
-
#[arg(long)]
|
|
127
|
-
files: Option<String>,
|
|
128
|
-
#[arg(short, long)]
|
|
129
|
-
passphrase: Option<String>,
|
|
130
|
-
/// optional dictionary file used during decompression
|
|
131
|
-
#[arg(long, value_name = "FILE")]
|
|
132
|
-
dict: Option<PathBuf>,
|
|
133
|
-
#[arg(long, value_name = "MB")]
|
|
134
|
-
ram_budget_mb: Option<u64>,
|
|
135
|
-
},
|
|
136
|
-
Crc32 {
|
|
137
|
-
input: PathBuf,
|
|
138
|
-
},
|
|
139
|
-
Adler32 {
|
|
140
|
-
input: PathBuf,
|
|
141
|
-
},
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
fn read_all(path: &PathBuf) -> anyhow::Result<Vec<u8>> {
|
|
145
|
-
let metadata = std::fs::metadata(path)?;
|
|
146
|
-
let size = metadata.len() as usize;
|
|
147
|
-
let mut f = File::open(path)?;
|
|
148
|
-
let mut buf = Vec::with_capacity(size);
|
|
149
|
-
f.read_to_end(&mut buf)?;
|
|
150
|
-
Ok(buf)
|
|
151
|
-
}
|
|
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
|
-
|
|
333
|
-
fn write_all(path: &PathBuf, data: &[u8]) -> anyhow::Result<()> {
|
|
334
|
-
let f = File::create(path)?;
|
|
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));
|
|
339
|
-
let mut writer = std::io::BufWriter::with_capacity(buf_size, f);
|
|
340
|
-
writer.write_all(data)?;
|
|
341
|
-
writer.flush()?;
|
|
342
|
-
Ok(())
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
fn parse_markers(v: &[String]) -> Option<Vec<u8>> {
|
|
346
|
-
if v.is_empty() {
|
|
347
|
-
return None;
|
|
348
|
-
}
|
|
349
|
-
let mut out = Vec::new();
|
|
350
|
-
for s in v {
|
|
351
|
-
let parts: Vec<&str> = s.split(|c| c == ':' || c == ',' ).collect();
|
|
352
|
-
if parts.len() >= 3 {
|
|
353
|
-
if let (Ok(r), Ok(g), Ok(b)) = (parts[0].parse::<u8>(), parts[1].parse::<u8>(), parts[2].parse::<u8>()) {
|
|
354
|
-
out.push(r); out.push(g); out.push(b);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
if out.is_empty() { None } else { Some(out) }
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
fn main() -> anyhow::Result<()> {
|
|
362
|
-
let cli = Cli::parse();
|
|
363
|
-
|
|
364
|
-
fn parse_requested_files(files: &str) -> anyhow::Result<Vec<String>> {
|
|
365
|
-
if files.trim_start().starts_with('[') {
|
|
366
|
-
serde_json::from_str::<Vec<String>>(files)
|
|
367
|
-
.map_err(|e| anyhow::anyhow!("Invalid JSON for --files: {}", e))
|
|
368
|
-
} else {
|
|
369
|
-
Ok(files
|
|
370
|
-
.split(',')
|
|
371
|
-
.map(|file| file.trim().to_string())
|
|
372
|
-
.filter(|file| !file.is_empty())
|
|
373
|
-
.collect())
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
match cli.command {
|
|
377
|
-
Commands::TrainDict { samples, size, output } => {
|
|
378
|
-
let dict = core::train_zstd_dictionary(&samples, size)?;
|
|
379
|
-
write_all(&output, &dict)?;
|
|
380
|
-
println!("wrote {} bytes dictionary to {:?}", dict.len(), output);
|
|
381
|
-
return Ok(());
|
|
382
|
-
}
|
|
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());
|
|
386
|
-
let is_dir = input.is_dir();
|
|
387
|
-
|
|
388
|
-
let file_name = name.as_deref()
|
|
389
|
-
.or_else(|| input.file_name().and_then(|n| n.to_str()));
|
|
390
|
-
|
|
391
|
-
if is_dir && dict.is_none() {
|
|
392
|
-
streaming_encode::encode_dir_to_png_encrypted_with_progress(
|
|
393
|
-
&input,
|
|
394
|
-
&output,
|
|
395
|
-
level,
|
|
396
|
-
file_name,
|
|
397
|
-
passphrase.as_deref(),
|
|
398
|
-
Some(&encrypt),
|
|
399
|
-
Some(Box::new(|current, total, step| {
|
|
400
|
-
eprintln!("PROGRESS:{}:{}:{}", current, total, step);
|
|
401
|
-
})),
|
|
402
|
-
)?;
|
|
403
|
-
println!("(directory payload, rXFL chunk embedded)");
|
|
404
|
-
return Ok(());
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
let (payload, file_list_json) = if is_dir {
|
|
408
|
-
let result = archive::tar_pack_directory_with_list(&input)
|
|
409
|
-
.map_err(|e| anyhow::anyhow!(e))?;
|
|
410
|
-
let json_list: Vec<serde_json::Value> = result.file_list.iter()
|
|
411
|
-
.map(|(name, size)| serde_json::json!({"name": name, "size": size}))
|
|
412
|
-
.collect();
|
|
413
|
-
(result.data, Some(serde_json::to_string(&json_list)?))
|
|
414
|
-
} else {
|
|
415
|
-
let pack_result = packer::pack_path_with_metadata(&input)?;
|
|
416
|
-
(pack_result.data, pack_result.file_list_json)
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
let dict_bytes: Option<Vec<u8>> = match dict {
|
|
420
|
-
Some(path) => Some(read_all(&path)?),
|
|
421
|
-
None => None,
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
let use_streaming = (payload.len() as u64) > streaming_preference_threshold_bytes(ram_budget_mb);
|
|
425
|
-
eprintln!("PROGRESS:50:100:encoding");
|
|
426
|
-
|
|
427
|
-
if use_streaming {
|
|
428
|
-
streaming::encode_to_png_file(
|
|
429
|
-
&payload,
|
|
430
|
-
&output,
|
|
431
|
-
level,
|
|
432
|
-
passphrase.as_deref(),
|
|
433
|
-
Some(&encrypt),
|
|
434
|
-
file_name,
|
|
435
|
-
file_list_json.as_deref(),
|
|
436
|
-
dict_bytes.as_deref(),
|
|
437
|
-
)?;
|
|
438
|
-
} else {
|
|
439
|
-
let png = if let Some(ref pass) = passphrase {
|
|
440
|
-
encoder::encode_to_png_with_encryption_name_and_format_and_filelist(
|
|
441
|
-
&payload,
|
|
442
|
-
level,
|
|
443
|
-
Some(pass),
|
|
444
|
-
Some(&encrypt),
|
|
445
|
-
ImageFormat::Png,
|
|
446
|
-
file_name,
|
|
447
|
-
file_list_json.as_deref(),
|
|
448
|
-
dict_bytes.as_deref(),
|
|
449
|
-
)?
|
|
450
|
-
} else {
|
|
451
|
-
encoder::encode_to_png_with_encryption_name_and_format_and_filelist(
|
|
452
|
-
&payload,
|
|
453
|
-
level,
|
|
454
|
-
None,
|
|
455
|
-
None,
|
|
456
|
-
ImageFormat::Png,
|
|
457
|
-
file_name,
|
|
458
|
-
file_list_json.as_deref(),
|
|
459
|
-
dict_bytes.as_deref(),
|
|
460
|
-
)?
|
|
461
|
-
};
|
|
462
|
-
write_all(&output, &png)?;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
if file_list_json.is_some() {
|
|
466
|
-
eprintln!("PROGRESS:100:100:done");
|
|
467
|
-
if is_dir {
|
|
468
|
-
println!("(directory payload, rXFL chunk embedded)");
|
|
469
|
-
} else {
|
|
470
|
-
println!("(rXFL chunk embedded)");
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
Commands::List { input } => {
|
|
475
|
-
let mut file = File::open(&input)?;
|
|
476
|
-
let mut chunk_scan_error: Option<anyhow::Error> = None;
|
|
477
|
-
|
|
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
|
-
}
|
|
484
|
-
|
|
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
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
Err(err) => {
|
|
507
|
-
chunk_scan_error = Some(anyhow::anyhow!(err));
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
let png_data = std::fs::read(&input)?;
|
|
512
|
-
match png_utils::extract_file_list_from_pixels(&png_data) {
|
|
513
|
-
Ok(json) => {
|
|
514
|
-
println!("{}", json);
|
|
515
|
-
return Ok(());
|
|
516
|
-
}
|
|
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
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
return Err(anyhow::anyhow!("No file list found in PNG"));
|
|
525
|
-
}
|
|
526
|
-
Commands::Havepassphrase { input } => {
|
|
527
|
-
let buf = read_all(&input)?;
|
|
528
|
-
let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
|
|
529
|
-
if is_png {
|
|
530
|
-
let payload = png_utils::extract_payload_from_png(&buf).map_err(|e| anyhow::anyhow!(e))?;
|
|
531
|
-
if !payload.is_empty() && (payload[0] == 0x01 || payload[0] == 0x02 || payload[0] == 0x03) {
|
|
532
|
-
println!("Passphrase detected.");
|
|
533
|
-
} else {
|
|
534
|
-
println!("No passphrase detected.");
|
|
535
|
-
}
|
|
536
|
-
} else {
|
|
537
|
-
if !buf.is_empty() && (buf[0] == 0x01 || buf[0] == 0x02 || buf[0] == 0x03) {
|
|
538
|
-
println!("Passphrase detected.");
|
|
539
|
-
} else {
|
|
540
|
-
println!("No passphrase detected.");
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
Commands::Scan { input, channels, markers } => {
|
|
545
|
-
let buf = read_all(&input)?;
|
|
546
|
-
let marker_bytes = parse_markers(&markers);
|
|
547
|
-
let res = crate::core::scan_pixels_bytes(&buf, channels, marker_bytes.as_deref());
|
|
548
|
-
println!("magic_positions: {:?}", res.magic_positions);
|
|
549
|
-
println!("marker_positions: {:?}", res.marker_positions);
|
|
550
|
-
}
|
|
551
|
-
Commands::DeltaEncode { input, output } => {
|
|
552
|
-
let buf = read_all(&input)?;
|
|
553
|
-
let out = crate::core::delta_encode_bytes(&buf);
|
|
554
|
-
let dest = output.unwrap_or_else(|| PathBuf::from("delta.bin"));
|
|
555
|
-
write_all(&dest, &out)?;
|
|
556
|
-
}
|
|
557
|
-
Commands::DeltaDecode { input, output } => {
|
|
558
|
-
let buf = read_all(&input)?;
|
|
559
|
-
let out = crate::core::delta_decode_bytes(&buf);
|
|
560
|
-
let dest = output.unwrap_or_else(|| PathBuf::from("raw.bin"));
|
|
561
|
-
write_all(&dest, &out)?;
|
|
562
|
-
}
|
|
563
|
-
Commands::Compress { input, output, level, dict } => {
|
|
564
|
-
let buf = read_all(&input)?;
|
|
565
|
-
let dict_bytes: Option<Vec<u8>> = match dict {
|
|
566
|
-
Some(path) => Some(read_all(&path)?),
|
|
567
|
-
None => None,
|
|
568
|
-
};
|
|
569
|
-
let out = crate::core::zstd_compress_bytes(
|
|
570
|
-
&buf,
|
|
571
|
-
level,
|
|
572
|
-
dict_bytes.as_deref(),
|
|
573
|
-
)
|
|
574
|
-
.map_err(|e: String| anyhow::anyhow!(e))?;
|
|
575
|
-
let dest = output.unwrap_or_else(|| PathBuf::from("out.zst"));
|
|
576
|
-
write_all(&dest, &out)?;
|
|
577
|
-
}
|
|
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());
|
|
581
|
-
let file_size = std::fs::metadata(&input).map(|m| m.len()).unwrap_or(0);
|
|
582
|
-
let is_png_file = input.extension().map(|e| e == "png").unwrap_or(false)
|
|
583
|
-
|| (file_size >= 8 && {
|
|
584
|
-
let mut sig = [0u8; 8];
|
|
585
|
-
std::fs::File::open(&input).and_then(|mut f| { use std::io::Read; f.read_exact(&mut sig) }).is_ok()
|
|
586
|
-
&& sig == [137, 80, 78, 71, 13, 10, 26, 10]
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
let requested_files = match files.as_deref() {
|
|
590
|
-
Some(files_str) => Some(parse_requested_files(files_str)?),
|
|
591
|
-
None => None,
|
|
592
|
-
};
|
|
593
|
-
|
|
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
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
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,
|
|
656
|
-
&out_dir,
|
|
657
|
-
requested_files.as_deref(),
|
|
658
|
-
Some(&progress_cb),
|
|
659
|
-
)?;
|
|
660
|
-
eprintln!("PROGRESS:100:100:done");
|
|
661
|
-
println!("Unpacked {} files", written.len());
|
|
662
|
-
return Ok(());
|
|
663
|
-
}
|
|
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
|
-
|
|
673
|
-
let buf = read_all(&input)?;
|
|
674
|
-
|
|
675
|
-
eprintln!("PROGRESS:20:100:decompressing");
|
|
676
|
-
let dict_bytes: Option<Vec<u8>> = match dict {
|
|
677
|
-
Some(path) => Some(read_all(&path)?),
|
|
678
|
-
None => None,
|
|
679
|
-
};
|
|
680
|
-
if requested_files.is_some() {
|
|
681
|
-
let file_list = requested_files;
|
|
682
|
-
|
|
683
|
-
let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
|
|
684
|
-
|
|
685
|
-
use std::io::Cursor;
|
|
686
|
-
let normalized: Vec<u8> = if is_png {
|
|
687
|
-
let payload = png_utils::extract_payload_from_png(&buf).map_err(|e| anyhow::anyhow!(e))?;
|
|
688
|
-
if payload.is_empty() { return Err(anyhow::anyhow!("Empty payload")); }
|
|
689
|
-
if payload[0] == 0x00u8 {
|
|
690
|
-
payload[1..].to_vec()
|
|
691
|
-
} else {
|
|
692
|
-
let pass = passphrase.as_ref().map(|s: &String| s.as_str());
|
|
693
|
-
match crate::crypto::try_decrypt(&payload, pass) {
|
|
694
|
-
Ok(v) => v,
|
|
695
|
-
Err(e) => return Err(anyhow::anyhow!("Encrypted payload: {}", e)),
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
} else {
|
|
699
|
-
if buf[0] == 0x00u8 {
|
|
700
|
-
buf[1..].to_vec()
|
|
701
|
-
} else if buf.starts_with(b"ROX1") {
|
|
702
|
-
buf[4..].to_vec()
|
|
703
|
-
} else if buf[0] == 0x01u8 || buf[0] == 0x02u8 || buf[0] == 0x03u8 {
|
|
704
|
-
let pass = passphrase.as_ref().map(|s: &String| s.as_str());
|
|
705
|
-
match crate::crypto::try_decrypt(&buf, pass) {
|
|
706
|
-
Ok(v) => v,
|
|
707
|
-
Err(e) => return Err(anyhow::anyhow!("Encrypted payload: {}", e)),
|
|
708
|
-
}
|
|
709
|
-
} else {
|
|
710
|
-
buf.to_vec()
|
|
711
|
-
}
|
|
712
|
-
};
|
|
713
|
-
|
|
714
|
-
let mut reader: Box<dyn std::io::Read> = if normalized.starts_with(b"ROX1") {
|
|
715
|
-
Box::new(Cursor::new(normalized[4..].to_vec()))
|
|
716
|
-
} else {
|
|
717
|
-
let mut dec = zstd::stream::Decoder::new(Cursor::new(normalized)).map_err(|e| anyhow::anyhow!("zstd decoder init: {}", e))?;
|
|
718
|
-
dec.window_log_max(31).map_err(|e| anyhow::anyhow!("zstd window_log_max: {}", e))?;
|
|
719
|
-
Box::new(dec)
|
|
720
|
-
};
|
|
721
|
-
|
|
722
|
-
let out_dir = output.unwrap_or_else(|| PathBuf::from("."));
|
|
723
|
-
std::fs::create_dir_all(&out_dir).map_err(|e| anyhow::anyhow!("Cannot create output directory {:?}: {}", out_dir, e))?;
|
|
724
|
-
let files_slice = file_list.as_ref().map(|v| v.as_slice());
|
|
725
|
-
|
|
726
|
-
let written = packer::unpack_stream_to_dir(&mut reader, &out_dir, files_slice, Some(&|current, total, step| {
|
|
727
|
-
eprintln!("PROGRESS:{}:{}:{}", current, total, step);
|
|
728
|
-
}), 0).map_err(|e| anyhow::anyhow!(e))?;
|
|
729
|
-
eprintln!("PROGRESS:100:100:done");
|
|
730
|
-
println!("Unpacked {} files", written.len());
|
|
731
|
-
} else {
|
|
732
|
-
let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
|
|
733
|
-
let out_bytes = if is_png {
|
|
734
|
-
let payload = png_utils::extract_payload_from_png(&buf).map_err(|e| anyhow::anyhow!(e))?;
|
|
735
|
-
if payload.is_empty() { return Err(anyhow::anyhow!("Empty payload")); }
|
|
736
|
-
let first = payload[0];
|
|
737
|
-
if first == 0x00u8 {
|
|
738
|
-
let compressed = payload[1..].to_vec();
|
|
739
|
-
match crate::core::zstd_decompress_bytes(&compressed, dict_bytes.as_deref()) {
|
|
740
|
-
Ok(mut o) => {
|
|
741
|
-
if o.starts_with(b"ROX1") { o = o[4..].to_vec(); }
|
|
742
|
-
o
|
|
743
|
-
}
|
|
744
|
-
Err(e) => {
|
|
745
|
-
if compressed.starts_with(b"ROX1") {
|
|
746
|
-
eprintln!("⚠️ zstd decompress failed ({}), but payload starts with ROX1: falling back to raw pack", e);
|
|
747
|
-
compressed[4..].to_vec()
|
|
748
|
-
} else {
|
|
749
|
-
return Err(anyhow::anyhow!("zstd decompress error: {}", e));
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
} else {
|
|
754
|
-
let pass = passphrase.as_ref().map(|s| s.as_str());
|
|
755
|
-
match crate::crypto::try_decrypt(&payload, pass) {
|
|
756
|
-
Ok(v) => {
|
|
757
|
-
let inner = if v.starts_with(b"ROX1") {
|
|
758
|
-
v[4..].to_vec()
|
|
759
|
-
} else {
|
|
760
|
-
v
|
|
761
|
-
};
|
|
762
|
-
match crate::core::zstd_decompress_bytes(&inner, dict_bytes.as_deref()) {
|
|
763
|
-
Ok(mut o) => {
|
|
764
|
-
if o.starts_with(b"ROX1") { o = o[4..].to_vec(); }
|
|
765
|
-
o
|
|
766
|
-
}
|
|
767
|
-
Err(_) => inner,
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
Err(e) => return Err(anyhow::anyhow!("Encrypted payload: {}", e)),
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
} else {
|
|
774
|
-
match crate::core::zstd_decompress_bytes(&buf, dict_bytes.as_deref()) {
|
|
775
|
-
Ok(mut x) => { if x.starts_with(b"ROX1") { x = x[4..].to_vec(); } x },
|
|
776
|
-
Err(e) => {
|
|
777
|
-
if buf.starts_with(b"ROX1") {
|
|
778
|
-
eprintln!("⚠️ zstd decompress failed ({}), but input already starts with ROX1: using raw pack", e);
|
|
779
|
-
buf[4..].to_vec()
|
|
780
|
-
} else {
|
|
781
|
-
return Err(anyhow::anyhow!("zstd decompress error: {}", e));
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
};
|
|
786
|
-
|
|
787
|
-
let dest = output.unwrap_or_else(|| PathBuf::from("out.raw"));
|
|
788
|
-
|
|
789
|
-
if archive::is_tar(&out_bytes) {
|
|
790
|
-
let out_dir = if dest.extension().is_none() || dest.is_dir() {
|
|
791
|
-
dest
|
|
792
|
-
} else {
|
|
793
|
-
PathBuf::from(".")
|
|
794
|
-
};
|
|
795
|
-
std::fs::create_dir_all(&out_dir)
|
|
796
|
-
.map_err(|e| anyhow::anyhow!("mkdir {:?}: {}", out_dir, e))?;
|
|
797
|
-
let written = archive::tar_unpack(&out_bytes, &out_dir)
|
|
798
|
-
.map_err(|e| anyhow::anyhow!(e))?;
|
|
799
|
-
println!("Unpacked {} files to {:?}", written.len(), out_dir);
|
|
800
|
-
} else if out_bytes.len() >= 4
|
|
801
|
-
&& (u32::from_be_bytes(out_bytes[0..4].try_into().unwrap()) == 0x524f5850u32
|
|
802
|
-
|| u32::from_be_bytes(out_bytes[0..4].try_into().unwrap()) == 0x524f5849u32)
|
|
803
|
-
{
|
|
804
|
-
let out_dir = if dest.extension().is_none() || dest.is_dir() {
|
|
805
|
-
dest
|
|
806
|
-
} else {
|
|
807
|
-
PathBuf::from(".")
|
|
808
|
-
};
|
|
809
|
-
std::fs::create_dir_all(&out_dir)
|
|
810
|
-
.map_err(|e| anyhow::anyhow!("mkdir {:?}: {}", out_dir, e))?;
|
|
811
|
-
let written = packer::unpack_buffer_to_dir(&out_bytes, &out_dir, None)
|
|
812
|
-
.map_err(|e| anyhow::anyhow!(e))?;
|
|
813
|
-
println!("Unpacked {} files to {:?}", written.len(), out_dir);
|
|
814
|
-
} else if dest.is_dir() {
|
|
815
|
-
let fname = if is_png {
|
|
816
|
-
png_utils::extract_name_from_png(&buf)
|
|
817
|
-
} else {
|
|
818
|
-
None
|
|
819
|
-
}.unwrap_or_else(|| {
|
|
820
|
-
input.file_stem()
|
|
821
|
-
.map(|s| s.to_string_lossy().to_string())
|
|
822
|
-
.unwrap_or_else(|| "out.raw".to_string())
|
|
823
|
-
});
|
|
824
|
-
write_all(&dest.join(&fname), &out_bytes)?;
|
|
825
|
-
} else {
|
|
826
|
-
write_all(&dest, &out_bytes)?;
|
|
827
|
-
}
|
|
828
|
-
eprintln!("PROGRESS:100:100:done");
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
Commands::Crc32 { input } => {
|
|
832
|
-
let buf = read_all(&input)?;
|
|
833
|
-
println!("crc32: {}", crate::core::crc32_bytes(&buf));
|
|
834
|
-
}
|
|
835
|
-
Commands::Adler32 { input } => {
|
|
836
|
-
let buf = read_all(&input)?;
|
|
837
|
-
println!("adler32: {}", crate::core::adler32_bytes(&buf));
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
Ok(())
|
|
841
|
-
}
|
|
842
|
-
|