roxify 1.7.5 → 1.8.0
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 +293 -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 +461 -0
- package/native/packer.rs +444 -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/main.rs
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
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 audio;
|
|
15
|
+
mod reconstitution;
|
|
16
|
+
mod archive;
|
|
17
|
+
|
|
18
|
+
use crate::encoder::ImageFormat;
|
|
19
|
+
use std::path::PathBuf;
|
|
20
|
+
|
|
21
|
+
#[derive(Parser)]
|
|
22
|
+
#[command(author, version)]
|
|
23
|
+
struct Cli {
|
|
24
|
+
#[command(subcommand)]
|
|
25
|
+
command: Commands,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#[derive(Subcommand)]
|
|
29
|
+
enum Commands {
|
|
30
|
+
Encode {
|
|
31
|
+
input: PathBuf,
|
|
32
|
+
output: PathBuf,
|
|
33
|
+
#[arg(short, long, default_value_t = 3)]
|
|
34
|
+
level: i32,
|
|
35
|
+
#[arg(short, long)]
|
|
36
|
+
passphrase: Option<String>,
|
|
37
|
+
#[arg(short, long, default_value = "aes")]
|
|
38
|
+
encrypt: String,
|
|
39
|
+
#[arg(short, long)]
|
|
40
|
+
name: Option<String>,
|
|
41
|
+
/// optional zstd dictionary file for payload compression
|
|
42
|
+
#[arg(long, value_name = "FILE")]
|
|
43
|
+
dict: Option<PathBuf>,
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
List {
|
|
47
|
+
input: PathBuf,
|
|
48
|
+
},
|
|
49
|
+
Havepassphrase {
|
|
50
|
+
input: PathBuf,
|
|
51
|
+
},
|
|
52
|
+
Scan {
|
|
53
|
+
input: PathBuf,
|
|
54
|
+
#[arg(short, long, value_name = "FILE")]
|
|
55
|
+
#[arg(short, long, default_value_t = 4)]
|
|
56
|
+
channels: usize,
|
|
57
|
+
#[arg(short, long, value_delimiter = ',')]
|
|
58
|
+
markers: Vec<String>,
|
|
59
|
+
},
|
|
60
|
+
DeltaEncode {
|
|
61
|
+
input: PathBuf,
|
|
62
|
+
output: Option<PathBuf>,
|
|
63
|
+
},
|
|
64
|
+
DeltaDecode {
|
|
65
|
+
input: PathBuf,
|
|
66
|
+
output: Option<PathBuf>,
|
|
67
|
+
},
|
|
68
|
+
Compress {
|
|
69
|
+
input: PathBuf,
|
|
70
|
+
output: Option<PathBuf>,
|
|
71
|
+
#[arg(short, long, default_value_t = 19)]
|
|
72
|
+
level: i32,
|
|
73
|
+
/// optional zstd dictionary file to use for compression
|
|
74
|
+
#[arg(long, value_name = "FILE")]
|
|
75
|
+
dict: Option<PathBuf>,
|
|
76
|
+
},
|
|
77
|
+
TrainDict {
|
|
78
|
+
/// sample files used to train the dictionary
|
|
79
|
+
#[arg(short, long, value_name = "FILE", required = true)]
|
|
80
|
+
samples: Vec<PathBuf>,
|
|
81
|
+
/// desired dictionary size in bytes
|
|
82
|
+
#[arg(short, long, default_value_t = 112640)]
|
|
83
|
+
size: usize,
|
|
84
|
+
/// output dictionary file
|
|
85
|
+
output: PathBuf,
|
|
86
|
+
},
|
|
87
|
+
Decompress {
|
|
88
|
+
input: PathBuf,
|
|
89
|
+
output: Option<PathBuf>,
|
|
90
|
+
#[arg(long)]
|
|
91
|
+
files: Option<String>,
|
|
92
|
+
#[arg(short, long)]
|
|
93
|
+
passphrase: Option<String>,
|
|
94
|
+
/// optional dictionary file used during decompression
|
|
95
|
+
#[arg(long, value_name = "FILE")]
|
|
96
|
+
dict: Option<PathBuf>,
|
|
97
|
+
},
|
|
98
|
+
Crc32 {
|
|
99
|
+
input: PathBuf,
|
|
100
|
+
},
|
|
101
|
+
Adler32 {
|
|
102
|
+
input: PathBuf,
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn read_all(path: &PathBuf) -> anyhow::Result<Vec<u8>> {
|
|
107
|
+
let mut f = File::open(path)?;
|
|
108
|
+
let mut buf = Vec::new();
|
|
109
|
+
f.read_to_end(&mut buf)?;
|
|
110
|
+
Ok(buf)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fn write_all(path: &PathBuf, data: &[u8]) -> anyhow::Result<()> {
|
|
114
|
+
let mut f = File::create(path)?;
|
|
115
|
+
f.write_all(data)?;
|
|
116
|
+
Ok(())
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn parse_markers(v: &[String]) -> Option<Vec<u8>> {
|
|
120
|
+
if v.is_empty() {
|
|
121
|
+
return None;
|
|
122
|
+
}
|
|
123
|
+
let mut out = Vec::new();
|
|
124
|
+
for s in v {
|
|
125
|
+
let parts: Vec<&str> = s.split(|c| c == ':' || c == ',' ).collect();
|
|
126
|
+
if parts.len() >= 3 {
|
|
127
|
+
if let (Ok(r), Ok(g), Ok(b)) = (parts[0].parse::<u8>(), parts[1].parse::<u8>(), parts[2].parse::<u8>()) {
|
|
128
|
+
out.push(r); out.push(g); out.push(b);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if out.is_empty() { None } else { Some(out) }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fn main() -> anyhow::Result<()> {
|
|
136
|
+
let cli = Cli::parse();
|
|
137
|
+
match cli.command {
|
|
138
|
+
Commands::TrainDict { samples, size, output } => {
|
|
139
|
+
let dict = core::train_zstd_dictionary(&samples, size)?;
|
|
140
|
+
write_all(&output, &dict)?;
|
|
141
|
+
println!("wrote {} bytes dictionary to {:?}", dict.len(), output);
|
|
142
|
+
return Ok(());
|
|
143
|
+
}
|
|
144
|
+
Commands::Encode { input, output, level, passphrase, encrypt, name, dict } => {
|
|
145
|
+
let is_dir = input.is_dir();
|
|
146
|
+
let (payload, file_list_json) = if is_dir {
|
|
147
|
+
let tar_data = archive::tar_pack_directory(&input)
|
|
148
|
+
.map_err(|e| anyhow::anyhow!(e))?;
|
|
149
|
+
let list = archive::tar_file_list(&tar_data)
|
|
150
|
+
.map_err(|e| anyhow::anyhow!(e))?;
|
|
151
|
+
let json_list: Vec<serde_json::Value> = list.iter()
|
|
152
|
+
.map(|(name, size)| serde_json::json!({"name": name, "size": size}))
|
|
153
|
+
.collect();
|
|
154
|
+
(tar_data, Some(serde_json::to_string(&json_list)?))
|
|
155
|
+
} else {
|
|
156
|
+
let pack_result = packer::pack_path_with_metadata(&input)?;
|
|
157
|
+
(pack_result.data, pack_result.file_list_json)
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
let file_name = name.as_deref()
|
|
161
|
+
.or_else(|| input.file_name().and_then(|n| n.to_str()));
|
|
162
|
+
|
|
163
|
+
let dict_bytes: Option<Vec<u8>> = match dict {
|
|
164
|
+
Some(path) => Some(read_all(&path)?),
|
|
165
|
+
None => None,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
let png = if let Some(ref pass) = passphrase {
|
|
169
|
+
encoder::encode_to_png_with_encryption_name_and_format_and_filelist(
|
|
170
|
+
&payload,
|
|
171
|
+
level,
|
|
172
|
+
Some(pass),
|
|
173
|
+
Some(&encrypt),
|
|
174
|
+
ImageFormat::Png,
|
|
175
|
+
file_name,
|
|
176
|
+
file_list_json.as_deref(),
|
|
177
|
+
dict_bytes.as_deref(),
|
|
178
|
+
)?
|
|
179
|
+
} else {
|
|
180
|
+
encoder::encode_to_png_with_encryption_name_and_format_and_filelist(
|
|
181
|
+
&payload,
|
|
182
|
+
level,
|
|
183
|
+
None,
|
|
184
|
+
None,
|
|
185
|
+
ImageFormat::Png,
|
|
186
|
+
file_name,
|
|
187
|
+
file_list_json.as_deref(),
|
|
188
|
+
dict_bytes.as_deref(),
|
|
189
|
+
)?
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
write_all(&output, &png)?;
|
|
193
|
+
|
|
194
|
+
if file_list_json.is_some() {
|
|
195
|
+
if is_dir {
|
|
196
|
+
println!("(TAR archive, rXFL chunk embedded)");
|
|
197
|
+
} else {
|
|
198
|
+
println!("(rXFL chunk embedded)");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
Commands::List { input } => {
|
|
203
|
+
let mut file = File::open(&input)?;
|
|
204
|
+
let chunks = png_utils::extract_png_chunks_streaming(&mut file).map_err(|e| anyhow::anyhow!(e))?;
|
|
205
|
+
|
|
206
|
+
if let Some(rxfl_chunk) = chunks.iter().find(|c| c.name == "rXFL") {
|
|
207
|
+
println!("{}", String::from_utf8_lossy(&rxfl_chunk.data));
|
|
208
|
+
return Ok(());
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if let Some(meta_chunk) = chunks.iter().find(|c| c.name == "rOXm") {
|
|
212
|
+
if let Some(pos) = meta_chunk.data.windows(4).position(|w| w == b"rXFL") {
|
|
213
|
+
if pos + 8 <= meta_chunk.data.len() {
|
|
214
|
+
let json_len = u32::from_be_bytes([
|
|
215
|
+
meta_chunk.data[pos + 4],
|
|
216
|
+
meta_chunk.data[pos + 5],
|
|
217
|
+
meta_chunk.data[pos + 6],
|
|
218
|
+
meta_chunk.data[pos + 7],
|
|
219
|
+
]) as usize;
|
|
220
|
+
|
|
221
|
+
let json_start = pos + 8;
|
|
222
|
+
let json_end = json_start + json_len;
|
|
223
|
+
|
|
224
|
+
if json_end <= meta_chunk.data.len() {
|
|
225
|
+
println!("{}", String::from_utf8_lossy(&meta_chunk.data[json_start..json_end]));
|
|
226
|
+
return Ok(());
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
eprintln!("No file list found in PNG");
|
|
233
|
+
std::process::exit(1);
|
|
234
|
+
}
|
|
235
|
+
Commands::Havepassphrase { input } => {
|
|
236
|
+
let buf = read_all(&input)?;
|
|
237
|
+
let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
|
|
238
|
+
if is_png {
|
|
239
|
+
let payload = png_utils::extract_payload_from_png(&buf).map_err(|e| anyhow::anyhow!(e))?;
|
|
240
|
+
if !payload.is_empty() && (payload[0] == 0x01 || payload[0] == 0x02) {
|
|
241
|
+
println!("Passphrase detected.");
|
|
242
|
+
} else {
|
|
243
|
+
println!("No passphrase detected.");
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
if !buf.is_empty() && (buf[0] == 0x01 || buf[0] == 0x02) {
|
|
247
|
+
println!("Passphrase detected.");
|
|
248
|
+
} else {
|
|
249
|
+
println!("No passphrase detected.");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
Commands::Scan { input, channels, markers } => {
|
|
254
|
+
let buf = read_all(&input)?;
|
|
255
|
+
let marker_bytes = parse_markers(&markers);
|
|
256
|
+
let res = crate::core::scan_pixels_bytes(&buf, channels, marker_bytes.as_deref());
|
|
257
|
+
println!("magic_positions: {:?}", res.magic_positions);
|
|
258
|
+
println!("marker_positions: {:?}", res.marker_positions);
|
|
259
|
+
}
|
|
260
|
+
Commands::DeltaEncode { input, output } => {
|
|
261
|
+
let buf = read_all(&input)?;
|
|
262
|
+
let out = crate::core::delta_encode_bytes(&buf);
|
|
263
|
+
let dest = output.unwrap_or_else(|| PathBuf::from("delta.bin"));
|
|
264
|
+
write_all(&dest, &out)?;
|
|
265
|
+
}
|
|
266
|
+
Commands::DeltaDecode { input, output } => {
|
|
267
|
+
let buf = read_all(&input)?;
|
|
268
|
+
let out = crate::core::delta_decode_bytes(&buf);
|
|
269
|
+
let dest = output.unwrap_or_else(|| PathBuf::from("raw.bin"));
|
|
270
|
+
write_all(&dest, &out)?;
|
|
271
|
+
}
|
|
272
|
+
Commands::Compress { input, output, level, dict } => {
|
|
273
|
+
let buf = read_all(&input)?;
|
|
274
|
+
let dict_bytes: Option<Vec<u8>> = match dict {
|
|
275
|
+
Some(path) => Some(read_all(&path)?),
|
|
276
|
+
None => None,
|
|
277
|
+
};
|
|
278
|
+
let out = crate::core::zstd_compress_bytes(
|
|
279
|
+
&buf,
|
|
280
|
+
level,
|
|
281
|
+
dict_bytes.as_deref(),
|
|
282
|
+
)
|
|
283
|
+
.map_err(|e: String| anyhow::anyhow!(e))?;
|
|
284
|
+
let dest = output.unwrap_or_else(|| PathBuf::from("out.zst"));
|
|
285
|
+
write_all(&dest, &out)?;
|
|
286
|
+
}
|
|
287
|
+
Commands::Decompress { input, output, files, passphrase, dict } => {
|
|
288
|
+
let buf = read_all(&input)?;
|
|
289
|
+
let dict_bytes: Option<Vec<u8>> = match dict {
|
|
290
|
+
Some(path) => Some(read_all(&path)?),
|
|
291
|
+
None => None,
|
|
292
|
+
};
|
|
293
|
+
if let Some(files_str) = files {
|
|
294
|
+
let file_list: Option<Vec<String>> = if files_str.trim_start().starts_with('[') {
|
|
295
|
+
match serde_json::from_str::<Vec<String>>(&files_str) {
|
|
296
|
+
Ok(v) => Some(v),
|
|
297
|
+
Err(e) => {
|
|
298
|
+
eprintln!("Invalid JSON for --files: {}", e);
|
|
299
|
+
std::process::exit(1);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
let list = files_str.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect::<Vec<_>>();
|
|
304
|
+
Some(list)
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
|
|
308
|
+
|
|
309
|
+
use std::io::Cursor;
|
|
310
|
+
let normalized: Vec<u8> = if is_png {
|
|
311
|
+
let payload = png_utils::extract_payload_from_png(&buf).map_err(|e| anyhow::anyhow!(e))?;
|
|
312
|
+
if payload.is_empty() { return Err(anyhow::anyhow!("Empty payload")); }
|
|
313
|
+
if payload[0] == 0x00u8 {
|
|
314
|
+
payload[1..].to_vec()
|
|
315
|
+
} else {
|
|
316
|
+
let pass = passphrase.as_ref().map(|s: &String| s.as_str());
|
|
317
|
+
match crate::crypto::try_decrypt(&payload, pass) {
|
|
318
|
+
Ok(v) => v,
|
|
319
|
+
Err(e) => return Err(anyhow::anyhow!("Encrypted payload: {}", e)),
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
if buf[0] == 0x00u8 {
|
|
324
|
+
buf[1..].to_vec()
|
|
325
|
+
} else if buf.starts_with(b"ROX1") {
|
|
326
|
+
buf[4..].to_vec()
|
|
327
|
+
} else if buf[0] == 0x01u8 || buf[0] == 0x02u8 {
|
|
328
|
+
let pass = passphrase.as_ref().map(|s: &String| s.as_str());
|
|
329
|
+
match crate::crypto::try_decrypt(&buf, pass) {
|
|
330
|
+
Ok(v) => v,
|
|
331
|
+
Err(e) => return Err(anyhow::anyhow!("Encrypted payload: {}", e)),
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
buf.to_vec()
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
let mut reader: Box<dyn std::io::Read> = if normalized.starts_with(b"ROX1") {
|
|
339
|
+
Box::new(Cursor::new(normalized[4..].to_vec()))
|
|
340
|
+
} else {
|
|
341
|
+
let dec = zstd::stream::Decoder::new(Cursor::new(normalized)).map_err(|e| anyhow::anyhow!("zstd decoder init: {}", e))?;
|
|
342
|
+
Box::new(dec)
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
let out_dir = output.unwrap_or_else(|| PathBuf::from("."));
|
|
346
|
+
std::fs::create_dir_all(&out_dir).map_err(|e| anyhow::anyhow!("Cannot create output directory {:?}: {}", out_dir, e))?;
|
|
347
|
+
let files_slice = file_list.as_ref().map(|v| v.as_slice());
|
|
348
|
+
|
|
349
|
+
let written = packer::unpack_stream_to_dir(&mut reader, &out_dir, files_slice).map_err(|e| anyhow::anyhow!(e))?;
|
|
350
|
+
println!("Unpacked {} files", written.len());
|
|
351
|
+
} else {
|
|
352
|
+
let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
|
|
353
|
+
let out_bytes = if is_png {
|
|
354
|
+
let payload = png_utils::extract_payload_from_png(&buf).map_err(|e| anyhow::anyhow!(e))?;
|
|
355
|
+
if payload.is_empty() { return Err(anyhow::anyhow!("Empty payload")); }
|
|
356
|
+
let first = payload[0];
|
|
357
|
+
if first == 0x00u8 {
|
|
358
|
+
let compressed = payload[1..].to_vec();
|
|
359
|
+
match crate::core::zstd_decompress_bytes(&compressed, dict_bytes.as_deref()) {
|
|
360
|
+
Ok(mut o) => {
|
|
361
|
+
if o.starts_with(b"ROX1") { o = o[4..].to_vec(); }
|
|
362
|
+
o
|
|
363
|
+
}
|
|
364
|
+
Err(e) => {
|
|
365
|
+
if compressed.starts_with(b"ROX1") {
|
|
366
|
+
eprintln!("⚠️ zstd decompress failed ({}), but payload starts with ROX1: falling back to raw pack", e);
|
|
367
|
+
compressed[4..].to_vec()
|
|
368
|
+
} else {
|
|
369
|
+
return Err(anyhow::anyhow!("zstd decompress error: {}", e));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
let pass = passphrase.as_ref().map(|s| s.as_str());
|
|
375
|
+
match crate::crypto::try_decrypt(&payload, pass) {
|
|
376
|
+
Ok(v) => {
|
|
377
|
+
let inner = if v.starts_with(b"ROX1") {
|
|
378
|
+
v[4..].to_vec()
|
|
379
|
+
} else {
|
|
380
|
+
v
|
|
381
|
+
};
|
|
382
|
+
match crate::core::zstd_decompress_bytes(&inner, dict_bytes.as_deref()) {
|
|
383
|
+
Ok(mut o) => {
|
|
384
|
+
if o.starts_with(b"ROX1") { o = o[4..].to_vec(); }
|
|
385
|
+
o
|
|
386
|
+
}
|
|
387
|
+
Err(_) => inner,
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
Err(e) => return Err(anyhow::anyhow!("Encrypted payload: {}", e)),
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
match crate::core::zstd_decompress_bytes(&buf, dict_bytes.as_deref()) {
|
|
395
|
+
Ok(mut x) => { if x.starts_with(b"ROX1") { x = x[4..].to_vec(); } x },
|
|
396
|
+
Err(e) => {
|
|
397
|
+
if buf.starts_with(b"ROX1") {
|
|
398
|
+
eprintln!("⚠️ zstd decompress failed ({}), but input already starts with ROX1: using raw pack", e);
|
|
399
|
+
buf[4..].to_vec()
|
|
400
|
+
} else {
|
|
401
|
+
return Err(anyhow::anyhow!("zstd decompress error: {}", e));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
let dest = output.unwrap_or_else(|| PathBuf::from("out.raw"));
|
|
408
|
+
|
|
409
|
+
if archive::is_tar(&out_bytes) {
|
|
410
|
+
let out_dir = if dest.extension().is_none() || dest.is_dir() {
|
|
411
|
+
dest
|
|
412
|
+
} else {
|
|
413
|
+
PathBuf::from(".")
|
|
414
|
+
};
|
|
415
|
+
std::fs::create_dir_all(&out_dir)
|
|
416
|
+
.map_err(|e| anyhow::anyhow!("mkdir {:?}: {}", out_dir, e))?;
|
|
417
|
+
let written = archive::tar_unpack(&out_bytes, &out_dir)
|
|
418
|
+
.map_err(|e| anyhow::anyhow!(e))?;
|
|
419
|
+
println!("Unpacked {} files (TAR) to {:?}", written.len(), out_dir);
|
|
420
|
+
} else if out_bytes.len() >= 4
|
|
421
|
+
&& (u32::from_be_bytes(out_bytes[0..4].try_into().unwrap()) == 0x524f5850u32
|
|
422
|
+
|| u32::from_be_bytes(out_bytes[0..4].try_into().unwrap()) == 0x524f5849u32)
|
|
423
|
+
{
|
|
424
|
+
let out_dir = if dest.extension().is_none() || dest.is_dir() {
|
|
425
|
+
dest
|
|
426
|
+
} else {
|
|
427
|
+
PathBuf::from(".")
|
|
428
|
+
};
|
|
429
|
+
std::fs::create_dir_all(&out_dir)
|
|
430
|
+
.map_err(|e| anyhow::anyhow!("mkdir {:?}: {}", out_dir, e))?;
|
|
431
|
+
let written = packer::unpack_buffer_to_dir(&out_bytes, &out_dir, None)
|
|
432
|
+
.map_err(|e| anyhow::anyhow!(e))?;
|
|
433
|
+
println!("Unpacked {} files to {:?}", written.len(), out_dir);
|
|
434
|
+
} else if dest.is_dir() {
|
|
435
|
+
let fname = if is_png {
|
|
436
|
+
png_utils::extract_name_from_png(&buf)
|
|
437
|
+
} else {
|
|
438
|
+
None
|
|
439
|
+
}.unwrap_or_else(|| {
|
|
440
|
+
input.file_stem()
|
|
441
|
+
.map(|s| s.to_string_lossy().to_string())
|
|
442
|
+
.unwrap_or_else(|| "out.raw".to_string())
|
|
443
|
+
});
|
|
444
|
+
write_all(&dest.join(&fname), &out_bytes)?;
|
|
445
|
+
} else {
|
|
446
|
+
write_all(&dest, &out_bytes)?;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
Commands::Crc32 { input } => {
|
|
451
|
+
let buf = read_all(&input)?;
|
|
452
|
+
println!("crc32: {}", crate::core::crc32_bytes(&buf));
|
|
453
|
+
}
|
|
454
|
+
Commands::Adler32 { input } => {
|
|
455
|
+
let buf = read_all(&input)?;
|
|
456
|
+
println!("adler32: {}", crate::core::adler32_bytes(&buf));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
Ok(())
|
|
460
|
+
}
|
|
461
|
+
|