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
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
use anyhow::Result;
|
|
2
|
+
use std::process::{Command, Stdio};
|
|
3
|
+
|
|
4
|
+
const MAGIC: &[u8] = b"ROX1";
|
|
5
|
+
const ENC_NONE: u8 = 0x00;
|
|
6
|
+
const PIXEL_MAGIC: &[u8] = b"PXL1";
|
|
7
|
+
const PNG_HEADER: &[u8] = &[137, 80, 78, 71, 13, 10, 26, 10];
|
|
8
|
+
|
|
9
|
+
const MARKER_START: [(u8, u8, u8); 3] = [(255, 0, 0), (0, 255, 0), (0, 0, 255)];
|
|
10
|
+
const MARKER_END: [(u8, u8, u8); 3] = [(0, 0, 255), (0, 255, 0), (255, 0, 0)];
|
|
11
|
+
const MARKER_ZSTD: (u8, u8, u8) = (0, 255, 0);
|
|
12
|
+
|
|
13
|
+
#[derive(Debug, Clone, Copy)]
|
|
14
|
+
pub enum ImageFormat {
|
|
15
|
+
Png,
|
|
16
|
+
WebP,
|
|
17
|
+
JpegXL,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub fn encode_to_png(data: &[u8], compression_level: i32) -> Result<Vec<u8>> {
|
|
21
|
+
let format = predict_best_format_raw(data);
|
|
22
|
+
encode_to_png_with_encryption_name_and_format_and_filelist(data, compression_level, None, None, format, None, None, None)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
pub fn encode_to_png_with_name(data: &[u8], compression_level: i32, name: Option<&str>) -> Result<Vec<u8>> {
|
|
27
|
+
let format = predict_best_format_raw(data);
|
|
28
|
+
encode_to_png_with_encryption_name_and_format_and_filelist(data, compression_level, None, None, format, name, None, None)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
pub fn encode_to_png_with_name_and_filelist(data: &[u8], compression_level: i32, name: Option<&str>, file_list: Option<&str>) -> Result<Vec<u8>> {
|
|
32
|
+
encode_to_png_with_encryption_name_and_format_and_filelist(data, compression_level, None, None, ImageFormat::Png, name, file_list, None)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn predict_best_format_raw(data: &[u8]) -> ImageFormat {
|
|
36
|
+
if data.len() < 512 {
|
|
37
|
+
return ImageFormat::Png;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let sample_size = data.len().min(4096);
|
|
41
|
+
let sample = &data[..sample_size];
|
|
42
|
+
|
|
43
|
+
let entropy = calculate_shannon_entropy(sample);
|
|
44
|
+
let repetition_score = detect_repetition_patterns(sample);
|
|
45
|
+
let unique_bytes = count_unique_bytes(sample);
|
|
46
|
+
let unique_ratio = unique_bytes as f64 / 256.0;
|
|
47
|
+
let is_sequential = detect_sequential_pattern(sample);
|
|
48
|
+
|
|
49
|
+
if entropy > 7.8 {
|
|
50
|
+
ImageFormat::Png
|
|
51
|
+
} else if is_sequential || repetition_score > 0.15 {
|
|
52
|
+
ImageFormat::JpegXL
|
|
53
|
+
} else if unique_ratio < 0.4 && entropy < 6.5 {
|
|
54
|
+
ImageFormat::JpegXL
|
|
55
|
+
} else if entropy < 5.0 {
|
|
56
|
+
ImageFormat::JpegXL
|
|
57
|
+
} else {
|
|
58
|
+
ImageFormat::Png
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub fn encode_to_png_raw(data: &[u8], compression_level: i32) -> Result<Vec<u8>> {
|
|
63
|
+
encode_to_png_with_encryption_name_and_format_and_filelist(data, compression_level, None, None, ImageFormat::Png, None, None, None)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pub fn encode_to_png_with_encryption_and_name(
|
|
67
|
+
data: &[u8],
|
|
68
|
+
compression_level: i32,
|
|
69
|
+
passphrase: Option<&str>,
|
|
70
|
+
encrypt_type: Option<&str>,
|
|
71
|
+
name: Option<&str>,
|
|
72
|
+
) -> Result<Vec<u8>> {
|
|
73
|
+
let format = predict_best_format_raw(data);
|
|
74
|
+
encode_to_png_with_encryption_name_and_format_and_filelist(data, compression_level, passphrase, encrypt_type, format, name, None, None)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
pub fn encode_to_png_with_encryption_name_and_filelist(
|
|
78
|
+
data: &[u8],
|
|
79
|
+
compression_level: i32,
|
|
80
|
+
passphrase: Option<&str>,
|
|
81
|
+
encrypt_type: Option<&str>,
|
|
82
|
+
name: Option<&str>,
|
|
83
|
+
file_list: Option<&str>,
|
|
84
|
+
) -> Result<Vec<u8>> {
|
|
85
|
+
encode_to_png_with_encryption_name_and_format_and_filelist(data, compression_level, passphrase, encrypt_type, ImageFormat::Png, name, file_list, None)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── WAV container encoding ─────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
/// Encode data into WAV container (8-bit PCM).
|
|
91
|
+
/// Same compression/encryption pipeline as PNG, but wrapped in a WAV file
|
|
92
|
+
/// instead of pixel grid. Overhead: 44 bytes (constant) vs PNG's variable overhead.
|
|
93
|
+
pub fn encode_to_wav(data: &[u8], compression_level: i32) -> Result<Vec<u8>> {
|
|
94
|
+
encode_to_wav_with_encryption_name_and_filelist(data, compression_level, None, None, None, None)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
pub fn encode_to_wav_with_name_and_filelist(
|
|
98
|
+
data: &[u8],
|
|
99
|
+
compression_level: i32,
|
|
100
|
+
name: Option<&str>,
|
|
101
|
+
file_list: Option<&str>,
|
|
102
|
+
) -> Result<Vec<u8>> {
|
|
103
|
+
encode_to_wav_with_encryption_name_and_filelist(data, compression_level, None, None, name, file_list)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
pub fn encode_to_wav_with_encryption_name_and_filelist(
|
|
107
|
+
data: &[u8],
|
|
108
|
+
compression_level: i32,
|
|
109
|
+
passphrase: Option<&str>,
|
|
110
|
+
encrypt_type: Option<&str>,
|
|
111
|
+
name: Option<&str>,
|
|
112
|
+
file_list: Option<&str>,
|
|
113
|
+
) -> Result<Vec<u8>> {
|
|
114
|
+
// Same compression + encryption pipeline as PNG
|
|
115
|
+
let payload_input = [MAGIC, data].concat();
|
|
116
|
+
|
|
117
|
+
let compressed = crate::core::zstd_compress_bytes(&payload_input, compression_level, None)
|
|
118
|
+
.map_err(|e| anyhow::anyhow!("Compression failed: {}", e))?;
|
|
119
|
+
|
|
120
|
+
let encrypted = if let Some(pass) = passphrase {
|
|
121
|
+
match encrypt_type.unwrap_or("aes") {
|
|
122
|
+
"xor" => crate::crypto::encrypt_xor(&compressed, pass),
|
|
123
|
+
"aes" => crate::crypto::encrypt_aes(&compressed, pass)?,
|
|
124
|
+
_ => crate::crypto::encrypt_aes(&compressed, pass)?,
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
crate::crypto::no_encryption(&compressed)
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
let meta_pixel = build_meta_pixel_with_name_and_filelist(&encrypted, name, file_list)?;
|
|
131
|
+
|
|
132
|
+
// Prepend PIXEL_MAGIC so decoder can validate the payload
|
|
133
|
+
let wav_payload = [PIXEL_MAGIC, &meta_pixel].concat();
|
|
134
|
+
|
|
135
|
+
// Wrap in WAV container (44 bytes overhead, constant)
|
|
136
|
+
Ok(crate::audio::bytes_to_wav(&wav_payload))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Extract payload from a WAV file and return the raw meta_pixel bytes.
|
|
140
|
+
pub fn decode_wav_payload(wav_data: &[u8]) -> Result<Vec<u8>> {
|
|
141
|
+
crate::audio::wav_to_bytes(wav_data)
|
|
142
|
+
.map_err(|e| anyhow::anyhow!("WAV decode failed: {}", e))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
pub fn encode_to_png_with_encryption_name_and_format_and_filelist(
|
|
146
|
+
data: &[u8],
|
|
147
|
+
compression_level: i32,
|
|
148
|
+
passphrase: Option<&str>,
|
|
149
|
+
encrypt_type: Option<&str>,
|
|
150
|
+
format: ImageFormat,
|
|
151
|
+
name: Option<&str>,
|
|
152
|
+
file_list: Option<&str>,
|
|
153
|
+
dict: Option<&[u8]>,
|
|
154
|
+
) -> Result<Vec<u8>> {
|
|
155
|
+
let png = encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, passphrase, encrypt_type, name, file_list, dict)?;
|
|
156
|
+
|
|
157
|
+
match format {
|
|
158
|
+
ImageFormat::Png => Ok(png),
|
|
159
|
+
ImageFormat::WebP => {
|
|
160
|
+
match optimize_to_webp(&png) {
|
|
161
|
+
Ok(optimized) => reconvert_to_png(&optimized, "webp").or_else(|_| Ok(png)),
|
|
162
|
+
Err(_) => Ok(png),
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
ImageFormat::JpegXL => {
|
|
166
|
+
match optimize_to_jxl(&png) {
|
|
167
|
+
Ok(optimized) => reconvert_to_png(&optimized, "jxl").or_else(|_| Ok(png)),
|
|
168
|
+
Err(_) => Ok(png),
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fn encode_to_png_with_encryption_name_and_filelist_internal(
|
|
175
|
+
data: &[u8],
|
|
176
|
+
compression_level: i32,
|
|
177
|
+
passphrase: Option<&str>,
|
|
178
|
+
encrypt_type: Option<&str>,
|
|
179
|
+
name: Option<&str>,
|
|
180
|
+
file_list: Option<&str>,
|
|
181
|
+
dict: Option<&[u8]>,
|
|
182
|
+
) -> Result<Vec<u8>> {
|
|
183
|
+
let payload_input = [MAGIC, data].concat();
|
|
184
|
+
|
|
185
|
+
let compressed = crate::core::zstd_compress_bytes(&payload_input, compression_level, dict)
|
|
186
|
+
.map_err(|e| anyhow::anyhow!("Compression failed: {}", e))?;
|
|
187
|
+
|
|
188
|
+
let encrypted = if let Some(pass) = passphrase {
|
|
189
|
+
match encrypt_type.unwrap_or("aes") {
|
|
190
|
+
"xor" => crate::crypto::encrypt_xor(&compressed, pass),
|
|
191
|
+
"aes" => crate::crypto::encrypt_aes(&compressed, pass)?,
|
|
192
|
+
_ => crate::crypto::encrypt_aes(&compressed, pass)?,
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
crate::crypto::no_encryption(&compressed)
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
let meta_pixel = build_meta_pixel_with_name_and_filelist(&encrypted, name, file_list)?;
|
|
199
|
+
let data_without_markers = [PIXEL_MAGIC, &meta_pixel].concat();
|
|
200
|
+
|
|
201
|
+
let padding_needed = (3 - (data_without_markers.len() % 3)) % 3;
|
|
202
|
+
let padded_data = if padding_needed > 0 {
|
|
203
|
+
[&data_without_markers[..], &vec![0u8; padding_needed]].concat()
|
|
204
|
+
} else {
|
|
205
|
+
data_without_markers
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
let mut marker_bytes = Vec::with_capacity(12);
|
|
209
|
+
for m in &MARKER_START {
|
|
210
|
+
marker_bytes.extend_from_slice(&[m.0, m.1, m.2]);
|
|
211
|
+
}
|
|
212
|
+
marker_bytes.extend_from_slice(&[MARKER_ZSTD.0, MARKER_ZSTD.1, MARKER_ZSTD.2]);
|
|
213
|
+
|
|
214
|
+
let data_with_markers = [&marker_bytes[..], &padded_data[..]].concat();
|
|
215
|
+
|
|
216
|
+
let mut marker_end_bytes = Vec::with_capacity(9);
|
|
217
|
+
for m in &MARKER_END {
|
|
218
|
+
marker_end_bytes.extend_from_slice(&[m.0, m.1, m.2]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let data_pixels = (data_with_markers.len() + 2) / 3;
|
|
222
|
+
let end_marker_pixels = 3;
|
|
223
|
+
let total_pixels = data_pixels + end_marker_pixels;
|
|
224
|
+
|
|
225
|
+
let side = (total_pixels as f64).sqrt().ceil() as usize;
|
|
226
|
+
let side = side.max(end_marker_pixels);
|
|
227
|
+
|
|
228
|
+
let width = side;
|
|
229
|
+
let height = side;
|
|
230
|
+
|
|
231
|
+
let total_data_bytes = width * height * 3;
|
|
232
|
+
let mut full_data = vec![0u8; total_data_bytes];
|
|
233
|
+
|
|
234
|
+
let marker_start_pos = (height - 1) * width * 3 + (width - end_marker_pixels) * 3;
|
|
235
|
+
|
|
236
|
+
let copy_len = data_with_markers.len().min(marker_start_pos);
|
|
237
|
+
full_data[..copy_len].copy_from_slice(&data_with_markers[..copy_len]);
|
|
238
|
+
|
|
239
|
+
let end_len = marker_end_bytes.len().min(total_data_bytes - marker_start_pos);
|
|
240
|
+
full_data[marker_start_pos..marker_start_pos + end_len]
|
|
241
|
+
.copy_from_slice(&marker_end_bytes[..end_len]);
|
|
242
|
+
|
|
243
|
+
let stride = width * 3 + 1;
|
|
244
|
+
let mut scanlines = Vec::with_capacity(height * stride);
|
|
245
|
+
|
|
246
|
+
for row in 0..height {
|
|
247
|
+
scanlines.push(0u8);
|
|
248
|
+
let src_start = row * width * 3;
|
|
249
|
+
let src_end = (row + 1) * width * 3;
|
|
250
|
+
scanlines.extend_from_slice(&full_data[src_start..src_end]);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let idat_data = create_raw_deflate(&scanlines);
|
|
254
|
+
|
|
255
|
+
build_png(width, height, &idat_data, file_list)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fn build_meta_pixel(payload: &[u8]) -> Result<Vec<u8>> {
|
|
259
|
+
build_meta_pixel_with_name(payload, None)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
fn build_meta_pixel_with_name(payload: &[u8], name: Option<&str>) -> Result<Vec<u8>> {
|
|
263
|
+
build_meta_pixel_with_name_and_filelist(payload, name, None)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
fn build_meta_pixel_with_name_and_filelist(payload: &[u8], name: Option<&str>, file_list: Option<&str>) -> Result<Vec<u8>> {
|
|
267
|
+
let version = 1u8;
|
|
268
|
+
let name_bytes = name.map(|n| n.as_bytes()).unwrap_or(&[]);
|
|
269
|
+
let name_len = name_bytes.len().min(255) as u8;
|
|
270
|
+
let payload_len_bytes = (payload.len() as u32).to_be_bytes();
|
|
271
|
+
|
|
272
|
+
let mut result = Vec::with_capacity(1 + 1 + name_len as usize + 4 + payload.len() + 256);
|
|
273
|
+
result.push(version);
|
|
274
|
+
result.push(name_len);
|
|
275
|
+
|
|
276
|
+
if name_len > 0 {
|
|
277
|
+
result.extend_from_slice(&name_bytes[..name_len as usize]);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
result.extend_from_slice(&payload_len_bytes);
|
|
281
|
+
result.extend_from_slice(payload);
|
|
282
|
+
|
|
283
|
+
if let Some(file_list_json) = file_list {
|
|
284
|
+
result.extend_from_slice(b"rXFL");
|
|
285
|
+
let json_bytes = file_list_json.as_bytes();
|
|
286
|
+
let json_len = json_bytes.len() as u32;
|
|
287
|
+
result.extend_from_slice(&json_len.to_be_bytes());
|
|
288
|
+
result.extend_from_slice(json_bytes);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
Ok(result)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
fn build_png(width: usize, height: usize, idat_data: &[u8], file_list: Option<&str>) -> Result<Vec<u8>> {
|
|
295
|
+
let mut png = Vec::with_capacity(8 + 25 + 12 + idat_data.len() + 12 + 256);
|
|
296
|
+
|
|
297
|
+
png.extend_from_slice(PNG_HEADER);
|
|
298
|
+
|
|
299
|
+
let mut ihdr_data = [0u8; 13];
|
|
300
|
+
ihdr_data[0..4].copy_from_slice(&(width as u32).to_be_bytes());
|
|
301
|
+
ihdr_data[4..8].copy_from_slice(&(height as u32).to_be_bytes());
|
|
302
|
+
ihdr_data[8] = 8;
|
|
303
|
+
ihdr_data[9] = 2;
|
|
304
|
+
ihdr_data[10] = 0;
|
|
305
|
+
ihdr_data[11] = 0;
|
|
306
|
+
ihdr_data[12] = 0;
|
|
307
|
+
|
|
308
|
+
write_chunk(&mut png, b"IHDR", &ihdr_data)?;
|
|
309
|
+
write_chunk(&mut png, b"IDAT", idat_data)?;
|
|
310
|
+
|
|
311
|
+
if let Some(file_list_json) = file_list {
|
|
312
|
+
write_chunk(&mut png, b"rXFL", file_list_json.as_bytes())?;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
write_chunk(&mut png, b"IEND", &[])?;
|
|
316
|
+
|
|
317
|
+
Ok(png)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
fn write_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) -> Result<()> {
|
|
321
|
+
let len = data.len() as u32;
|
|
322
|
+
out.extend_from_slice(&len.to_be_bytes());
|
|
323
|
+
out.extend_from_slice(chunk_type);
|
|
324
|
+
out.extend_from_slice(data);
|
|
325
|
+
|
|
326
|
+
let mut crc_data = Vec::with_capacity(chunk_type.len() + data.len());
|
|
327
|
+
crc_data.extend_from_slice(chunk_type);
|
|
328
|
+
crc_data.extend_from_slice(data);
|
|
329
|
+
let crc = crate::core::crc32_bytes(&crc_data);
|
|
330
|
+
|
|
331
|
+
out.extend_from_slice(&crc.to_be_bytes());
|
|
332
|
+
Ok(())
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
fn create_raw_deflate(data: &[u8]) -> Vec<u8> {
|
|
336
|
+
let mut result = Vec::with_capacity(data.len() + 6 + (data.len() / 65535 + 1) * 5);
|
|
337
|
+
|
|
338
|
+
result.push(0x78);
|
|
339
|
+
result.push(0x01);
|
|
340
|
+
|
|
341
|
+
let mut offset = 0;
|
|
342
|
+
while offset < data.len() {
|
|
343
|
+
let chunk_size = (data.len() - offset).min(65535);
|
|
344
|
+
let is_last = offset + chunk_size >= data.len();
|
|
345
|
+
|
|
346
|
+
result.push(if is_last { 0x01 } else { 0x00 });
|
|
347
|
+
|
|
348
|
+
result.push(chunk_size as u8);
|
|
349
|
+
result.push((chunk_size >> 8) as u8);
|
|
350
|
+
result.push(!chunk_size as u8);
|
|
351
|
+
result.push((!(chunk_size >> 8)) as u8);
|
|
352
|
+
|
|
353
|
+
result.extend_from_slice(&data[offset..offset + chunk_size]);
|
|
354
|
+
offset += chunk_size;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let adler = crate::core::adler32_bytes(data);
|
|
358
|
+
result.extend_from_slice(&adler.to_be_bytes());
|
|
359
|
+
|
|
360
|
+
result
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
fn predict_best_format(data: &[u8]) -> ImageFormat {
|
|
364
|
+
if data.len() < 2048 {
|
|
365
|
+
return ImageFormat::Png;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
let sample_size = data.len().min(4096);
|
|
369
|
+
let sample = &data[..sample_size];
|
|
370
|
+
|
|
371
|
+
let entropy = calculate_shannon_entropy(sample);
|
|
372
|
+
let repetition_score = detect_repetition_patterns(sample);
|
|
373
|
+
|
|
374
|
+
let unique_bytes = count_unique_bytes(sample);
|
|
375
|
+
let unique_ratio = unique_bytes as f64 / 256.0;
|
|
376
|
+
|
|
377
|
+
let is_sequential = detect_sequential_pattern(sample);
|
|
378
|
+
|
|
379
|
+
if entropy > 7.8 {
|
|
380
|
+
ImageFormat::Png
|
|
381
|
+
} else if is_sequential || repetition_score > 0.2 {
|
|
382
|
+
ImageFormat::JpegXL
|
|
383
|
+
} else if unique_ratio < 0.3 && entropy < 6.5 {
|
|
384
|
+
ImageFormat::JpegXL
|
|
385
|
+
} else if entropy < 5.5 {
|
|
386
|
+
ImageFormat::JpegXL
|
|
387
|
+
} else {
|
|
388
|
+
ImageFormat::Png
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
fn detect_sequential_pattern(data: &[u8]) -> bool {
|
|
393
|
+
if data.len() < 256 {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
let check_len = data.len().min(256);
|
|
398
|
+
let mut sequential = 0;
|
|
399
|
+
|
|
400
|
+
for i in 0..check_len - 1 {
|
|
401
|
+
let diff = (data[i + 1] as i16 - data[i] as i16).abs();
|
|
402
|
+
if diff <= 1 {
|
|
403
|
+
sequential += 1;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
sequential as f64 / (check_len - 1) as f64 > 0.6
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
fn count_unique_bytes(data: &[u8]) -> usize {
|
|
411
|
+
let mut seen = [false; 256];
|
|
412
|
+
for &byte in data {
|
|
413
|
+
seen[byte as usize] = true;
|
|
414
|
+
}
|
|
415
|
+
seen.iter().filter(|&&x| x).count()
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
fn calculate_shannon_entropy(data: &[u8]) -> f64 {
|
|
419
|
+
let mut freq = [0u32; 256];
|
|
420
|
+
for &byte in data {
|
|
421
|
+
freq[byte as usize] += 1;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
let len = data.len() as f64;
|
|
425
|
+
let mut entropy = 0.0;
|
|
426
|
+
|
|
427
|
+
for &count in &freq {
|
|
428
|
+
if count > 0 {
|
|
429
|
+
let p = count as f64 / len;
|
|
430
|
+
entropy -= p * p.log2();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
entropy
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
#[cfg(test)]
|
|
438
|
+
mod tests {
|
|
439
|
+
use super::*;
|
|
440
|
+
use crate::png_utils;
|
|
441
|
+
|
|
442
|
+
#[test]
|
|
443
|
+
fn test_rxfl_chunk_present_when_file_list_provided() {
|
|
444
|
+
let sample_data = b"hello world".to_vec();
|
|
445
|
+
let file_list_json = Some("[{\"name\": \"a.txt\", \"size\": 11}]" as &str);
|
|
446
|
+
let png = encode_to_png_with_encryption_name_and_filelist_internal(&sample_data, 3, None, None, None, file_list_json, None)
|
|
447
|
+
.expect("encode should succeed");
|
|
448
|
+
|
|
449
|
+
let chunks = png_utils::extract_png_chunks(&png).expect("extract chunks");
|
|
450
|
+
let found = chunks.iter().any(|c| c.name == "rXFL");
|
|
451
|
+
assert!(found, "rXFL chunk must be present when file_list is provided");
|
|
452
|
+
|
|
453
|
+
let rxfl_chunk = chunks.into_iter().find(|c| c.name == "rXFL").expect("rXFL present");
|
|
454
|
+
let s = String::from_utf8_lossy(&rxfl_chunk.data);
|
|
455
|
+
assert!(s.contains("a.txt"), "rXFL chunk should contain the file name");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
#[test]
|
|
459
|
+
fn test_extract_payload_and_partial_unpack() {
|
|
460
|
+
use std::fs;
|
|
461
|
+
let base = std::env::temp_dir().join(format!("rox_test_{}", rand::random::<u32>()));
|
|
462
|
+
let dir = base.join("data");
|
|
463
|
+
fs::create_dir_all(dir.join("sub")).unwrap();
|
|
464
|
+
fs::write(dir.join("a.txt"), b"hello").unwrap();
|
|
465
|
+
fs::write(dir.join("sub").join("b.txt"), b"world").unwrap();
|
|
466
|
+
|
|
467
|
+
let pack_result = crate::packer::pack_path_with_metadata(&dir).expect("pack path");
|
|
468
|
+
let png = encode_to_png_with_encryption_name_and_filelist_internal(&pack_result.data, 3, None, None, None, pack_result.file_list_json.as_deref(), None)
|
|
469
|
+
.expect("encode should succeed");
|
|
470
|
+
|
|
471
|
+
let payload = crate::png_utils::extract_payload_from_png(&png).expect("extract payload");
|
|
472
|
+
assert!(payload.len() > 1);
|
|
473
|
+
assert_eq!(payload[0], 0x00u8);
|
|
474
|
+
|
|
475
|
+
let compressed = payload[1..].to_vec();
|
|
476
|
+
let mut decompressed = crate::core::zstd_decompress_bytes(&compressed, None).expect("decompress");
|
|
477
|
+
if decompressed.starts_with(b"ROX1") {
|
|
478
|
+
decompressed = decompressed[4..].to_vec();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
let out_dir = base.join("out");
|
|
482
|
+
fs::create_dir_all(&out_dir).unwrap();
|
|
483
|
+
|
|
484
|
+
let written = crate::packer::unpack_buffer_to_dir(&decompressed, &out_dir, Some(&["sub/b.txt".to_string()])).expect("unpack");
|
|
485
|
+
assert_eq!(written.len(), 1);
|
|
486
|
+
let got = fs::read_to_string(out_dir.join("sub").join("b.txt")).unwrap();
|
|
487
|
+
assert_eq!(got, "world");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
#[test]
|
|
491
|
+
fn test_encrypt_decrypt_roundtrip() {
|
|
492
|
+
let data = b"hello world".to_vec();
|
|
493
|
+
let png = encode_to_png_with_encryption_name_and_filelist(&data, 3, Some("password"), Some("aes"), None, None)
|
|
494
|
+
.expect("encode should succeed");
|
|
495
|
+
let payload = crate::png_utils::extract_payload_from_png(&png).expect("extract");
|
|
496
|
+
let decrypted = crate::crypto::try_decrypt(&payload, Some("password")).expect("decrypt");
|
|
497
|
+
let mut decompressed = crate::core::zstd_decompress_bytes(&decrypted, None).expect("decompress");
|
|
498
|
+
if decompressed.starts_with(b"ROX1") { decompressed = decompressed[4..].to_vec(); }
|
|
499
|
+
assert_eq!(decompressed, data);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
fn detect_repetition_patterns(data: &[u8]) -> f64 {
|
|
504
|
+
if data.len() < 4 {
|
|
505
|
+
return 0.0;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
let mut repetitions = 0;
|
|
509
|
+
let mut total_checks = 0;
|
|
510
|
+
|
|
511
|
+
for i in 0..data.len().min(1024) {
|
|
512
|
+
if i + 3 < data.len() {
|
|
513
|
+
let byte = data[i];
|
|
514
|
+
if data[i + 1] == byte && data[i + 2] == byte && data[i + 3] == byte {
|
|
515
|
+
repetitions += 1;
|
|
516
|
+
}
|
|
517
|
+
total_checks += 1;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if total_checks > 0 {
|
|
522
|
+
repetitions as f64 / total_checks as f64
|
|
523
|
+
} else {
|
|
524
|
+
0.0
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
fn optimize_format(png_data: &[u8]) -> Result<Vec<u8>> {
|
|
529
|
+
let formats = [
|
|
530
|
+
("webp", optimize_to_webp(png_data)),
|
|
531
|
+
("jxl", optimize_to_jxl(png_data)),
|
|
532
|
+
];
|
|
533
|
+
|
|
534
|
+
let mut best = png_data.to_vec();
|
|
535
|
+
let mut best_size = png_data.len();
|
|
536
|
+
|
|
537
|
+
for (_name, result) in formats {
|
|
538
|
+
if let Ok(optimized) = result {
|
|
539
|
+
if optimized.len() < best_size {
|
|
540
|
+
best = optimized;
|
|
541
|
+
best_size = best.len();
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
Ok(best)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
fn optimize_to_webp(png_data: &[u8]) -> Result<Vec<u8>> {
|
|
550
|
+
use std::fs;
|
|
551
|
+
|
|
552
|
+
let tmp_dir = std::env::temp_dir();
|
|
553
|
+
let tmp_in = tmp_dir.join("roxify_temp_in.png");
|
|
554
|
+
let tmp_out = tmp_dir.join("roxify_temp_out.webp");
|
|
555
|
+
|
|
556
|
+
fs::write(&tmp_in, png_data)?;
|
|
557
|
+
|
|
558
|
+
let status = Command::new("cwebp")
|
|
559
|
+
.args(&["-lossless", &tmp_in.to_string_lossy(), "-o", &tmp_out.to_string_lossy()])
|
|
560
|
+
.stderr(Stdio::null())
|
|
561
|
+
.stdout(Stdio::null())
|
|
562
|
+
.status()?;
|
|
563
|
+
|
|
564
|
+
if status.success() {
|
|
565
|
+
let result = fs::read(&tmp_out)?;
|
|
566
|
+
let _ = fs::remove_file(&tmp_in);
|
|
567
|
+
let _ = fs::remove_file(&tmp_out);
|
|
568
|
+
Ok(result)
|
|
569
|
+
} else {
|
|
570
|
+
let _ = fs::remove_file(&tmp_in);
|
|
571
|
+
let _ = fs::remove_file(&tmp_out);
|
|
572
|
+
Err(anyhow::anyhow!("WebP conversion failed"))
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
fn optimize_to_jxl(png_data: &[u8]) -> Result<Vec<u8>> {
|
|
577
|
+
use std::fs;
|
|
578
|
+
|
|
579
|
+
let tmp_dir = std::env::temp_dir();
|
|
580
|
+
let tmp_in = tmp_dir.join("roxify_temp_in.png");
|
|
581
|
+
let tmp_out = tmp_dir.join("roxify_temp_out.jxl");
|
|
582
|
+
|
|
583
|
+
fs::write(&tmp_in, png_data)?;
|
|
584
|
+
|
|
585
|
+
let status = Command::new("cjxl")
|
|
586
|
+
.args(&[&tmp_in.to_string_lossy() as &str, &tmp_out.to_string_lossy() as &str, "-d", "0", "-e", "9"])
|
|
587
|
+
.stderr(Stdio::null())
|
|
588
|
+
.stdout(Stdio::null())
|
|
589
|
+
.status()?;
|
|
590
|
+
|
|
591
|
+
if status.success() {
|
|
592
|
+
let result = fs::read(&tmp_out)?;
|
|
593
|
+
let _ = fs::remove_file(&tmp_in);
|
|
594
|
+
let _ = fs::remove_file(&tmp_out);
|
|
595
|
+
Ok(result)
|
|
596
|
+
} else {
|
|
597
|
+
let _ = fs::remove_file(&tmp_in);
|
|
598
|
+
let _ = fs::remove_file(&tmp_out);
|
|
599
|
+
Err(anyhow::anyhow!("JXL conversion failed"))
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
fn reconvert_to_png(data: &[u8], original_format: &str) -> Result<Vec<u8>> {
|
|
604
|
+
use std::fs;
|
|
605
|
+
|
|
606
|
+
let tmp_dir = std::env::temp_dir();
|
|
607
|
+
let tmp_in = match original_format {
|
|
608
|
+
"webp" => tmp_dir.join("roxify_reconvert_in.webp"),
|
|
609
|
+
"jxl" => tmp_dir.join("roxify_reconvert_in.jxl"),
|
|
610
|
+
_ => return Err(anyhow::anyhow!("Unknown format")),
|
|
611
|
+
};
|
|
612
|
+
let tmp_out = tmp_dir.join("roxify_reconvert_out.png");
|
|
613
|
+
|
|
614
|
+
fs::write(&tmp_in, data)?;
|
|
615
|
+
|
|
616
|
+
let status = match original_format {
|
|
617
|
+
"webp" => Command::new("dwebp")
|
|
618
|
+
.args(&[&tmp_in.to_string_lossy() as &str, "-o", &tmp_out.to_string_lossy() as &str])
|
|
619
|
+
.stderr(Stdio::null())
|
|
620
|
+
.stdout(Stdio::null())
|
|
621
|
+
.status()?,
|
|
622
|
+
"jxl" => Command::new("djxl")
|
|
623
|
+
.args(&[&tmp_in.to_string_lossy() as &str, &tmp_out.to_string_lossy() as &str])
|
|
624
|
+
.stderr(Stdio::null())
|
|
625
|
+
.stdout(Stdio::null())
|
|
626
|
+
.status()?,
|
|
627
|
+
_ => return Err(anyhow::anyhow!("Unknown format")),
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
if status.success() {
|
|
631
|
+
let result = fs::read(&tmp_out)?;
|
|
632
|
+
let _ = fs::remove_file(&tmp_in);
|
|
633
|
+
let _ = fs::remove_file(&tmp_out);
|
|
634
|
+
Ok(result)
|
|
635
|
+
} else {
|
|
636
|
+
let _ = fs::remove_file(&tmp_in);
|
|
637
|
+
let _ = fs::remove_file(&tmp_out);
|
|
638
|
+
Err(anyhow::anyhow!("Reconversion to PNG failed"))
|
|
639
|
+
}
|
|
640
|
+
}
|