roxify 1.13.8 → 1.14.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/dist/cli.js +23 -21
- 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/encoder.rs
DELETED
|
@@ -1,405 +0,0 @@
|
|
|
1
|
-
use anyhow::Result;
|
|
2
|
-
|
|
3
|
-
use crate::png_chunk_writer::{write_chunked_idat_bytes, write_png_chunk};
|
|
4
|
-
|
|
5
|
-
const MAGIC: &[u8] = b"ROX1";
|
|
6
|
-
const PIXEL_MAGIC: &[u8] = b"PXL1";
|
|
7
|
-
const PNG_HEADER: &[u8] = &[137, 80, 78, 71, 13, 10, 26, 10];
|
|
8
|
-
const HEADER_VERSION_V2: u8 = 2;
|
|
9
|
-
|
|
10
|
-
const MARKER_START: [(u8, u8, u8); 3] = [(255, 0, 0), (0, 255, 0), (0, 0, 255)];
|
|
11
|
-
const MARKER_END: [(u8, u8, u8); 3] = [(0, 0, 255), (0, 255, 0), (255, 0, 0)];
|
|
12
|
-
const MARKER_ZSTD: (u8, u8, u8) = (0, 255, 0);
|
|
13
|
-
|
|
14
|
-
#[derive(Debug, Clone, Copy)]
|
|
15
|
-
pub enum ImageFormat {
|
|
16
|
-
Png,
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
pub fn encode_to_png(data: &[u8], compression_level: i32) -> Result<Vec<u8>> {
|
|
20
|
-
encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, None, None, None, None, None)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
pub fn encode_to_png_with_name(data: &[u8], compression_level: i32, name: Option<&str>) -> Result<Vec<u8>> {
|
|
25
|
-
encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, None, None, name, None, None)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
pub fn encode_to_png_with_name_and_filelist(data: &[u8], compression_level: i32, name: Option<&str>, file_list: Option<&str>) -> Result<Vec<u8>> {
|
|
29
|
-
encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, None, None, name, file_list, None)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
pub fn encode_to_png_raw(data: &[u8], compression_level: i32) -> Result<Vec<u8>> {
|
|
33
|
-
encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, None, None, None, None, None)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
pub fn encode_to_png_with_encryption_and_name(
|
|
37
|
-
data: &[u8],
|
|
38
|
-
compression_level: i32,
|
|
39
|
-
passphrase: Option<&str>,
|
|
40
|
-
encrypt_type: Option<&str>,
|
|
41
|
-
name: Option<&str>,
|
|
42
|
-
) -> Result<Vec<u8>> {
|
|
43
|
-
encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, passphrase, encrypt_type, name, None, None)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
pub fn encode_to_png_with_encryption_name_and_filelist(
|
|
47
|
-
data: &[u8],
|
|
48
|
-
compression_level: i32,
|
|
49
|
-
passphrase: Option<&str>,
|
|
50
|
-
encrypt_type: Option<&str>,
|
|
51
|
-
name: Option<&str>,
|
|
52
|
-
file_list: Option<&str>,
|
|
53
|
-
) -> Result<Vec<u8>> {
|
|
54
|
-
encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, passphrase, encrypt_type, name, file_list, None)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ─── WAV container encoding ─────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
/// Encode data into WAV container (8-bit PCM).
|
|
60
|
-
/// Same compression/encryption pipeline as PNG, but wrapped in a WAV file
|
|
61
|
-
/// instead of pixel grid. Overhead: 44 bytes (constant) vs PNG's variable overhead.
|
|
62
|
-
pub fn encode_to_wav(data: &[u8], compression_level: i32) -> Result<Vec<u8>> {
|
|
63
|
-
encode_to_wav_with_encryption_name_and_filelist(data, compression_level, None, None, None, None)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
pub fn encode_to_wav_with_name_and_filelist(
|
|
67
|
-
data: &[u8],
|
|
68
|
-
compression_level: i32,
|
|
69
|
-
name: Option<&str>,
|
|
70
|
-
file_list: Option<&str>,
|
|
71
|
-
) -> Result<Vec<u8>> {
|
|
72
|
-
encode_to_wav_with_encryption_name_and_filelist(data, compression_level, None, None, name, file_list)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
pub fn encode_to_wav_with_encryption_name_and_filelist(
|
|
76
|
-
data: &[u8],
|
|
77
|
-
compression_level: i32,
|
|
78
|
-
passphrase: Option<&str>,
|
|
79
|
-
encrypt_type: Option<&str>,
|
|
80
|
-
name: Option<&str>,
|
|
81
|
-
file_list: Option<&str>,
|
|
82
|
-
) -> Result<Vec<u8>> {
|
|
83
|
-
// Same compression + encryption pipeline as PNG
|
|
84
|
-
let compressed = crate::core::zstd_compress_with_prefix(data, compression_level, None, MAGIC)
|
|
85
|
-
.map_err(|e| anyhow::anyhow!("Compression failed: {}", e))?;
|
|
86
|
-
|
|
87
|
-
let encrypted = if let Some(pass) = passphrase {
|
|
88
|
-
match encrypt_type.unwrap_or("aes") {
|
|
89
|
-
"xor" => crate::crypto::encrypt_xor(&compressed, pass),
|
|
90
|
-
"aes" => crate::crypto::encrypt_aes(&compressed, pass)?,
|
|
91
|
-
_ => crate::crypto::encrypt_aes(&compressed, pass)?,
|
|
92
|
-
}
|
|
93
|
-
} else {
|
|
94
|
-
crate::crypto::no_encryption_in_place(compressed)
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
let meta_pixel = build_meta_pixel_with_name_and_filelist(&encrypted, name, file_list)?;
|
|
98
|
-
|
|
99
|
-
let wav_payload = [PIXEL_MAGIC, &meta_pixel].concat();
|
|
100
|
-
|
|
101
|
-
Ok(crate::audio::bytes_to_wav(&wav_payload))
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/// Extract payload from a WAV file and return the raw meta_pixel bytes.
|
|
105
|
-
pub fn decode_wav_payload(wav_data: &[u8]) -> Result<Vec<u8>> {
|
|
106
|
-
crate::audio::wav_to_bytes(wav_data)
|
|
107
|
-
.map_err(|e| anyhow::anyhow!("WAV decode failed: {}", e))
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
pub fn encode_to_png_with_encryption_name_and_format_and_filelist(
|
|
111
|
-
data: &[u8],
|
|
112
|
-
compression_level: i32,
|
|
113
|
-
passphrase: Option<&str>,
|
|
114
|
-
encrypt_type: Option<&str>,
|
|
115
|
-
_format: ImageFormat,
|
|
116
|
-
name: Option<&str>,
|
|
117
|
-
file_list: Option<&str>,
|
|
118
|
-
dict: Option<&[u8]>,
|
|
119
|
-
) -> Result<Vec<u8>> {
|
|
120
|
-
encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, passphrase, encrypt_type, name, file_list, dict)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
fn encode_to_png_with_encryption_name_and_filelist_internal(
|
|
124
|
-
data: &[u8],
|
|
125
|
-
compression_level: i32,
|
|
126
|
-
passphrase: Option<&str>,
|
|
127
|
-
encrypt_type: Option<&str>,
|
|
128
|
-
name: Option<&str>,
|
|
129
|
-
file_list: Option<&str>,
|
|
130
|
-
dict: Option<&[u8]>,
|
|
131
|
-
) -> Result<Vec<u8>> {
|
|
132
|
-
let compressed = crate::core::zstd_compress_with_prefix(data, compression_level, dict, MAGIC)
|
|
133
|
-
.map_err(|e| anyhow::anyhow!("Compression failed: {}", e))?;
|
|
134
|
-
|
|
135
|
-
let encrypted = if let Some(pass) = passphrase {
|
|
136
|
-
match encrypt_type.unwrap_or("aes") {
|
|
137
|
-
"xor" => crate::crypto::encrypt_xor(&compressed, pass),
|
|
138
|
-
"aes" => crate::crypto::encrypt_aes(&compressed, pass)?,
|
|
139
|
-
_ => crate::crypto::encrypt_aes(&compressed, pass)?,
|
|
140
|
-
}
|
|
141
|
-
} else {
|
|
142
|
-
crate::crypto::no_encryption_in_place(compressed)
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
let meta_pixel = build_meta_pixel_with_name_and_filelist(&encrypted, name, file_list)?;
|
|
146
|
-
drop(encrypted);
|
|
147
|
-
|
|
148
|
-
let raw_payload_len = PIXEL_MAGIC.len() + meta_pixel.len();
|
|
149
|
-
let padding_needed = (3 - (raw_payload_len % 3)) % 3;
|
|
150
|
-
let padded_len = raw_payload_len + padding_needed;
|
|
151
|
-
|
|
152
|
-
let marker_start_len = 12;
|
|
153
|
-
let marker_end_bytes = 9;
|
|
154
|
-
|
|
155
|
-
let data_with_markers_len = marker_start_len + padded_len;
|
|
156
|
-
let data_pixels = (data_with_markers_len + 2) / 3;
|
|
157
|
-
let end_marker_pixels = 3;
|
|
158
|
-
let total_pixels = data_pixels + end_marker_pixels;
|
|
159
|
-
|
|
160
|
-
let side = (total_pixels as f64).sqrt().ceil() as usize;
|
|
161
|
-
let side = side.max(end_marker_pixels);
|
|
162
|
-
let width = side;
|
|
163
|
-
let height = side;
|
|
164
|
-
|
|
165
|
-
let total_data_bytes = width * height * 3;
|
|
166
|
-
let marker_end_pos = total_data_bytes - marker_end_bytes;
|
|
167
|
-
|
|
168
|
-
let flat = build_flat_pixel_buffer(&meta_pixel, marker_end_pos, total_data_bytes);
|
|
169
|
-
drop(meta_pixel);
|
|
170
|
-
|
|
171
|
-
let row_bytes = width * 3;
|
|
172
|
-
let idat_data = create_raw_deflate_from_rows(&flat, row_bytes, height);
|
|
173
|
-
drop(flat);
|
|
174
|
-
|
|
175
|
-
build_png(width, height, &idat_data, file_list)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
fn build_flat_pixel_buffer(
|
|
179
|
-
meta_pixel: &[u8],
|
|
180
|
-
marker_end_pos: usize,
|
|
181
|
-
total_data_bytes: usize,
|
|
182
|
-
) -> Vec<u8> {
|
|
183
|
-
let mut flat = Vec::with_capacity(total_data_bytes);
|
|
184
|
-
|
|
185
|
-
for m in &MARKER_START {
|
|
186
|
-
flat.push(m.0); flat.push(m.1); flat.push(m.2);
|
|
187
|
-
}
|
|
188
|
-
flat.push(MARKER_ZSTD.0); flat.push(MARKER_ZSTD.1); flat.push(MARKER_ZSTD.2);
|
|
189
|
-
|
|
190
|
-
flat.extend_from_slice(PIXEL_MAGIC);
|
|
191
|
-
flat.extend_from_slice(meta_pixel);
|
|
192
|
-
|
|
193
|
-
let payload_end = flat.len();
|
|
194
|
-
if payload_end < marker_end_pos {
|
|
195
|
-
flat.resize(marker_end_pos, 0);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
for m in &MARKER_END {
|
|
199
|
-
flat.push(m.0); flat.push(m.1); flat.push(m.2);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if flat.len() < total_data_bytes {
|
|
203
|
-
flat.resize(total_data_bytes, 0);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
flat
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
fn build_meta_pixel_with_name_and_filelist(payload: &[u8], name: Option<&str>, file_list: Option<&str>) -> Result<Vec<u8>> {
|
|
210
|
-
let version = HEADER_VERSION_V2;
|
|
211
|
-
let name_bytes = name.map(|n| n.as_bytes()).unwrap_or(&[]);
|
|
212
|
-
let name_len = name_bytes.len().min(255) as u8;
|
|
213
|
-
let payload_len_bytes = (payload.len() as u64).to_be_bytes();
|
|
214
|
-
|
|
215
|
-
let mut result = Vec::with_capacity(1 + 1 + name_len as usize + 8 + payload.len() + 256);
|
|
216
|
-
result.push(version);
|
|
217
|
-
result.push(name_len);
|
|
218
|
-
|
|
219
|
-
if name_len > 0 {
|
|
220
|
-
result.extend_from_slice(&name_bytes[..name_len as usize]);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
result.extend_from_slice(&payload_len_bytes);
|
|
224
|
-
result.extend_from_slice(payload);
|
|
225
|
-
|
|
226
|
-
if let Some(file_list_json) = file_list {
|
|
227
|
-
result.extend_from_slice(b"rXFL");
|
|
228
|
-
let json_bytes = file_list_json.as_bytes();
|
|
229
|
-
let json_len = json_bytes.len() as u32;
|
|
230
|
-
result.extend_from_slice(&json_len.to_be_bytes());
|
|
231
|
-
result.extend_from_slice(json_bytes);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
Ok(result)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
fn build_png(width: usize, height: usize, idat_data: &[u8], file_list: Option<&str>) -> Result<Vec<u8>> {
|
|
238
|
-
let mut png = Vec::with_capacity(8 + 25 + 12 + idat_data.len() + 12 + 256);
|
|
239
|
-
|
|
240
|
-
png.extend_from_slice(PNG_HEADER);
|
|
241
|
-
|
|
242
|
-
let mut ihdr_data = [0u8; 13];
|
|
243
|
-
ihdr_data[0..4].copy_from_slice(&(width as u32).to_be_bytes());
|
|
244
|
-
ihdr_data[4..8].copy_from_slice(&(height as u32).to_be_bytes());
|
|
245
|
-
ihdr_data[8] = 8;
|
|
246
|
-
ihdr_data[9] = 2;
|
|
247
|
-
ihdr_data[10] = 0;
|
|
248
|
-
ihdr_data[11] = 0;
|
|
249
|
-
ihdr_data[12] = 0;
|
|
250
|
-
|
|
251
|
-
write_png_chunk(&mut png, b"IHDR", &ihdr_data)?;
|
|
252
|
-
write_chunked_idat_bytes(&mut png, idat_data)?;
|
|
253
|
-
|
|
254
|
-
if let Some(file_list_json) = file_list {
|
|
255
|
-
write_png_chunk(&mut png, b"rXFL", file_list_json.as_bytes())?;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
write_png_chunk(&mut png, b"IEND", &[])?;
|
|
259
|
-
|
|
260
|
-
Ok(png)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
fn create_raw_deflate_from_rows(flat: &[u8], row_bytes: usize, height: usize) -> Vec<u8> {
|
|
264
|
-
let stride = row_bytes + 1;
|
|
265
|
-
let scanlines_total = height * stride;
|
|
266
|
-
|
|
267
|
-
let mut scanlines = vec![0u8; scanlines_total];
|
|
268
|
-
for row in 0..height {
|
|
269
|
-
let flat_start = row * row_bytes;
|
|
270
|
-
let flat_end = (flat_start + row_bytes).min(flat.len());
|
|
271
|
-
let copy_len = flat_end.saturating_sub(flat_start);
|
|
272
|
-
if copy_len > 0 {
|
|
273
|
-
let dst_start = row * stride + 1;
|
|
274
|
-
scanlines[dst_start..dst_start + copy_len].copy_from_slice(&flat[flat_start..flat_end]);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const MAX_BLOCK: usize = 65535;
|
|
279
|
-
let num_blocks = (scanlines_total + MAX_BLOCK - 1) / MAX_BLOCK;
|
|
280
|
-
let total_size = 2 + num_blocks * 5 + scanlines_total + 4;
|
|
281
|
-
let mut result = Vec::with_capacity(total_size);
|
|
282
|
-
|
|
283
|
-
result.push(0x78);
|
|
284
|
-
result.push(0x01);
|
|
285
|
-
|
|
286
|
-
let mut offset = 0;
|
|
287
|
-
while offset < scanlines.len() {
|
|
288
|
-
let chunk_size = (scanlines.len() - offset).min(MAX_BLOCK);
|
|
289
|
-
let is_last = offset + chunk_size >= scanlines.len();
|
|
290
|
-
let header = [
|
|
291
|
-
if is_last { 0x01 } else { 0x00 },
|
|
292
|
-
chunk_size as u8,
|
|
293
|
-
(chunk_size >> 8) as u8,
|
|
294
|
-
!chunk_size as u8,
|
|
295
|
-
(!(chunk_size >> 8)) as u8,
|
|
296
|
-
];
|
|
297
|
-
result.extend_from_slice(&header);
|
|
298
|
-
result.extend_from_slice(&scanlines[offset..offset + chunk_size]);
|
|
299
|
-
offset += chunk_size;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
let adler = crate::core::adler32_bytes(&scanlines);
|
|
303
|
-
result.extend_from_slice(&adler.to_be_bytes());
|
|
304
|
-
|
|
305
|
-
result
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
#[cfg(test)]
|
|
309
|
-
mod tests {
|
|
310
|
-
use super::*;
|
|
311
|
-
use crate::png_utils;
|
|
312
|
-
|
|
313
|
-
#[test]
|
|
314
|
-
fn test_rxfl_chunk_present_when_file_list_provided() {
|
|
315
|
-
let sample_data = b"hello world".to_vec();
|
|
316
|
-
let file_list_json = Some("[{\"name\": \"a.txt\", \"size\": 11}]" as &str);
|
|
317
|
-
let png = encode_to_png_with_encryption_name_and_filelist_internal(&sample_data, 3, None, None, None, file_list_json, None)
|
|
318
|
-
.expect("encode should succeed");
|
|
319
|
-
|
|
320
|
-
let chunks = png_utils::extract_png_chunks(&png).expect("extract chunks");
|
|
321
|
-
let found = chunks.iter().any(|c| c.name == "rXFL");
|
|
322
|
-
assert!(found, "rXFL chunk must be present when file_list is provided");
|
|
323
|
-
|
|
324
|
-
let rxfl_chunk = chunks.into_iter().find(|c| c.name == "rXFL").expect("rXFL present");
|
|
325
|
-
let s = String::from_utf8_lossy(&rxfl_chunk.data);
|
|
326
|
-
assert!(s.contains("a.txt"), "rXFL chunk should contain the file name");
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
#[test]
|
|
330
|
-
fn test_extract_payload_and_partial_unpack() {
|
|
331
|
-
use std::fs;
|
|
332
|
-
let base = std::env::temp_dir().join(format!("rox_test_{}", rand::random::<u32>()));
|
|
333
|
-
let dir = base.join("data");
|
|
334
|
-
fs::create_dir_all(dir.join("sub")).unwrap();
|
|
335
|
-
fs::write(dir.join("a.txt"), b"hello").unwrap();
|
|
336
|
-
fs::write(dir.join("sub").join("b.txt"), b"world").unwrap();
|
|
337
|
-
|
|
338
|
-
let pack_result = crate::packer::pack_path_with_metadata(&dir).expect("pack path");
|
|
339
|
-
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)
|
|
340
|
-
.expect("encode should succeed");
|
|
341
|
-
|
|
342
|
-
let payload = crate::png_utils::extract_payload_from_png(&png).expect("extract payload");
|
|
343
|
-
assert!(payload.len() > 1);
|
|
344
|
-
assert_eq!(payload[0], 0x00u8);
|
|
345
|
-
|
|
346
|
-
let compressed = payload[1..].to_vec();
|
|
347
|
-
let mut decompressed = crate::core::zstd_decompress_bytes(&compressed, None).expect("decompress");
|
|
348
|
-
if decompressed.starts_with(b"ROX1") {
|
|
349
|
-
decompressed = decompressed[4..].to_vec();
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
let out_dir = base.join("out");
|
|
353
|
-
fs::create_dir_all(&out_dir).unwrap();
|
|
354
|
-
|
|
355
|
-
let written = crate::packer::unpack_buffer_to_dir(&decompressed, &out_dir, Some(&["sub/b.txt".to_string()])).expect("unpack");
|
|
356
|
-
assert_eq!(written.len(), 1);
|
|
357
|
-
let got = fs::read_to_string(out_dir.join("sub").join("b.txt")).unwrap();
|
|
358
|
-
assert_eq!(got, "world");
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
#[test]
|
|
362
|
-
fn test_encrypt_decrypt_roundtrip() {
|
|
363
|
-
let data = b"hello world".to_vec();
|
|
364
|
-
let png = encode_to_png_with_encryption_name_and_filelist(&data, 3, Some("password"), Some("aes"), None, None)
|
|
365
|
-
.expect("encode should succeed");
|
|
366
|
-
let payload = crate::png_utils::extract_payload_from_png(&png).expect("extract");
|
|
367
|
-
let decrypted = crate::crypto::try_decrypt(&payload, Some("password")).expect("decrypt");
|
|
368
|
-
let mut decompressed = crate::core::zstd_decompress_bytes(&decrypted, None).expect("decompress");
|
|
369
|
-
if decompressed.starts_with(b"ROX1") { decompressed = decompressed[4..].to_vec(); }
|
|
370
|
-
assert_eq!(decompressed, data);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
#[test]
|
|
374
|
-
fn test_marker_end_in_last_3_pixels() {
|
|
375
|
-
use image::ImageReader;
|
|
376
|
-
use std::io::Cursor;
|
|
377
|
-
|
|
378
|
-
for size in &[11, 100, 1000, 5000, 50000] {
|
|
379
|
-
let data: Vec<u8> = (0..*size).map(|i| (i % 256) as u8).collect();
|
|
380
|
-
|
|
381
|
-
let png_raw = encode_to_png_raw(&data, 3).expect("encode raw");
|
|
382
|
-
let png_auto = encode_to_png(&data, 3).expect("encode auto");
|
|
383
|
-
|
|
384
|
-
for (label, png) in &[("raw", &png_raw), ("auto", &png_auto)] {
|
|
385
|
-
let reader = ImageReader::new(Cursor::new(*png))
|
|
386
|
-
.with_guessed_format().unwrap();
|
|
387
|
-
let img = reader.decode().unwrap();
|
|
388
|
-
let rgb = img.to_rgb8();
|
|
389
|
-
let w = rgb.width();
|
|
390
|
-
let h = rgb.height();
|
|
391
|
-
|
|
392
|
-
let p1 = rgb.get_pixel(w - 3, h - 1);
|
|
393
|
-
let p2 = rgb.get_pixel(w - 2, h - 1);
|
|
394
|
-
let p3 = rgb.get_pixel(w - 1, h - 1);
|
|
395
|
-
|
|
396
|
-
assert_eq!([p1[0], p1[1], p1[2]], [0, 0, 255],
|
|
397
|
-
"MARKER_END pixel 0 (blue) wrong for {}@size={}, got {:?}", label, size, p1);
|
|
398
|
-
assert_eq!([p2[0], p2[1], p2[2]], [0, 255, 0],
|
|
399
|
-
"MARKER_END pixel 1 (green) wrong for {}@size={}, got {:?}", label, size, p2);
|
|
400
|
-
assert_eq!([p3[0], p3[1], p3[2]], [255, 0, 0],
|
|
401
|
-
"MARKER_END pixel 2 (red) wrong for {}@size={}, got {:?}", label, size, p3);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
package/native/hybrid.rs
DELETED
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
use anyhow::Result;
|
|
2
|
-
use rayon::prelude::*;
|
|
3
|
-
use crate::bwt::{bwt_encode, bwt_decode};
|
|
4
|
-
use crate::mtf::{mtf_encode, mtf_decode, rle0_encode, rle0_decode};
|
|
5
|
-
use crate::rans_byte::{SymbolStats, rans_encode_block, rans_decode_block};
|
|
6
|
-
use crate::context_mixing::analyze_entropy;
|
|
7
|
-
|
|
8
|
-
const BLOCK_SIZE: usize = 1024 * 1024;
|
|
9
|
-
|
|
10
|
-
const BLOCK_FLAG_BWT: u8 = 0;
|
|
11
|
-
const BLOCK_FLAG_ZSTD: u8 = 1;
|
|
12
|
-
const BLOCK_FLAG_STORE: u8 = 2;
|
|
13
|
-
|
|
14
|
-
const ENTROPY_THRESHOLD_STORE: f32 = 7.95;
|
|
15
|
-
const ENTROPY_THRESHOLD_ZSTD: f32 = 7.5;
|
|
16
|
-
|
|
17
|
-
#[derive(Clone, Debug)]
|
|
18
|
-
pub struct CompressionStats {
|
|
19
|
-
pub original_size: u64,
|
|
20
|
-
pub compressed_size: u64,
|
|
21
|
-
pub ratio: f64,
|
|
22
|
-
pub entropy_bits: f32,
|
|
23
|
-
pub blocks_count: usize,
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
pub struct HybridCompressor {
|
|
27
|
-
block_size: usize,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
impl HybridCompressor {
|
|
31
|
-
pub fn new() -> Self {
|
|
32
|
-
HybridCompressor {
|
|
33
|
-
block_size: BLOCK_SIZE,
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
pub fn compress(&self, data: &[u8]) -> Result<(Vec<u8>, CompressionStats)> {
|
|
38
|
-
let original_size = data.len() as u64;
|
|
39
|
-
let blocks: Vec<&[u8]> = data.chunks(self.block_size).collect();
|
|
40
|
-
let blocks_count = blocks.len();
|
|
41
|
-
|
|
42
|
-
let compressed_blocks: Vec<Vec<u8>> = blocks
|
|
43
|
-
.par_iter()
|
|
44
|
-
.map(|block| compress_block(block))
|
|
45
|
-
.collect::<Result<Vec<_>, _>>()?;
|
|
46
|
-
|
|
47
|
-
let entropy = if data.len() > 4096 {
|
|
48
|
-
analyze_entropy(&data[..4096.min(data.len())])
|
|
49
|
-
} else {
|
|
50
|
-
analyze_entropy(data)
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
let total_compressed: usize = compressed_blocks.iter().map(|b| b.len() + 4).sum();
|
|
54
|
-
let mut result = Vec::with_capacity(16 + total_compressed);
|
|
55
|
-
result.extend_from_slice(b"RBW2");
|
|
56
|
-
result.extend_from_slice(&(blocks_count as u32).to_le_bytes());
|
|
57
|
-
result.extend_from_slice(&original_size.to_le_bytes());
|
|
58
|
-
|
|
59
|
-
for block in &compressed_blocks {
|
|
60
|
-
result.extend_from_slice(&(block.len() as u32).to_le_bytes());
|
|
61
|
-
result.extend_from_slice(block);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
let compressed_size = result.len() as u64;
|
|
65
|
-
let ratio = (compressed_size as f64) / (original_size as f64);
|
|
66
|
-
|
|
67
|
-
Ok((result, CompressionStats {
|
|
68
|
-
original_size,
|
|
69
|
-
compressed_size,
|
|
70
|
-
ratio,
|
|
71
|
-
entropy_bits: entropy,
|
|
72
|
-
blocks_count,
|
|
73
|
-
}))
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
pub fn decompress(&self, data: &[u8]) -> Result<Vec<u8>> {
|
|
77
|
-
if data.len() < 16 {
|
|
78
|
-
return Err(anyhow::anyhow!("Invalid compressed data"));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
let magic = &data[0..4];
|
|
82
|
-
let v2 = magic == b"RBW2";
|
|
83
|
-
if magic != b"RBW1" && !v2 {
|
|
84
|
-
return Err(anyhow::anyhow!("Invalid magic"));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
let blocks_count = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize;
|
|
88
|
-
let original_size = u64::from_le_bytes([
|
|
89
|
-
data[8], data[9], data[10], data[11],
|
|
90
|
-
data[12], data[13], data[14], data[15],
|
|
91
|
-
]) as usize;
|
|
92
|
-
|
|
93
|
-
let mut pos = 16;
|
|
94
|
-
let mut block_slices: Vec<&[u8]> = Vec::with_capacity(blocks_count);
|
|
95
|
-
|
|
96
|
-
for _ in 0..blocks_count {
|
|
97
|
-
if pos + 4 > data.len() {
|
|
98
|
-
return Err(anyhow::anyhow!("Truncated block header"));
|
|
99
|
-
}
|
|
100
|
-
let block_size = u32::from_le_bytes([
|
|
101
|
-
data[pos], data[pos + 1], data[pos + 2], data[pos + 3],
|
|
102
|
-
]) as usize;
|
|
103
|
-
pos += 4;
|
|
104
|
-
if pos + block_size > data.len() {
|
|
105
|
-
return Err(anyhow::anyhow!("Truncated block data"));
|
|
106
|
-
}
|
|
107
|
-
block_slices.push(&data[pos..pos + block_size]);
|
|
108
|
-
pos += block_size;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
let decompressed_blocks: Vec<Vec<u8>> = block_slices
|
|
112
|
-
.par_iter()
|
|
113
|
-
.map(|block_data| {
|
|
114
|
-
if v2 {
|
|
115
|
-
decompress_block_v2(block_data)
|
|
116
|
-
} else {
|
|
117
|
-
decompress_block_v1(block_data)
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
|
-
.collect::<Result<Vec<_>, _>>()?;
|
|
121
|
-
|
|
122
|
-
let mut result = Vec::with_capacity(original_size);
|
|
123
|
-
for block in decompressed_blocks {
|
|
124
|
-
result.extend_from_slice(&block);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
Ok(result)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
pub fn estimate_gain(&self, data: &[u8]) -> f64 {
|
|
131
|
-
let entropy = analyze_entropy(data);
|
|
132
|
-
let theoretical_min = (data.len() as f64) * (entropy as f64) / 8.0;
|
|
133
|
-
let ratio = theoretical_min / (data.len() as f64);
|
|
134
|
-
(1.0 - ratio) * 100.0
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
fn compress_block_with_entropy(block: &[u8], entropy: f32) -> Result<Vec<u8>> {
|
|
139
|
-
if block.is_empty() {
|
|
140
|
-
return Ok(vec![BLOCK_FLAG_STORE]);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if entropy >= ENTROPY_THRESHOLD_STORE {
|
|
144
|
-
let mut result = Vec::with_capacity(1 + block.len());
|
|
145
|
-
result.push(BLOCK_FLAG_STORE);
|
|
146
|
-
result.extend_from_slice(block);
|
|
147
|
-
return Ok(result);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if entropy >= ENTROPY_THRESHOLD_ZSTD {
|
|
151
|
-
let compressed = zstd::encode_all(block, 1)?;
|
|
152
|
-
if compressed.len() < block.len() {
|
|
153
|
-
let mut result = Vec::with_capacity(1 + 4 + compressed.len());
|
|
154
|
-
result.push(BLOCK_FLAG_ZSTD);
|
|
155
|
-
result.extend_from_slice(&(block.len() as u32).to_le_bytes());
|
|
156
|
-
result.extend_from_slice(&compressed);
|
|
157
|
-
return Ok(result);
|
|
158
|
-
}
|
|
159
|
-
let mut result = Vec::with_capacity(1 + block.len());
|
|
160
|
-
result.push(BLOCK_FLAG_STORE);
|
|
161
|
-
result.extend_from_slice(block);
|
|
162
|
-
return Ok(result);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
try_bwt_or_zstd(block)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
fn compress_block(block: &[u8]) -> Result<Vec<u8>> {
|
|
169
|
-
if block.is_empty() {
|
|
170
|
-
return Ok(vec![BLOCK_FLAG_STORE]);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
let entropy = analyze_entropy(block);
|
|
174
|
-
compress_block_with_entropy(block, entropy)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
fn try_bwt_or_zstd(block: &[u8]) -> Result<Vec<u8>> {
|
|
178
|
-
let bwt = bwt_encode(block)?;
|
|
179
|
-
let mtf_data = mtf_encode(&bwt.transformed);
|
|
180
|
-
let rle_data = rle0_encode(&mtf_data);
|
|
181
|
-
let stats = SymbolStats::from_data(&rle_data);
|
|
182
|
-
let encoded = rans_encode_block(&rle_data, &stats);
|
|
183
|
-
let stats_bytes = stats.serialize();
|
|
184
|
-
|
|
185
|
-
let bwt_total = 1 + 4 + 4 + 4 + stats_bytes.len() + encoded.len();
|
|
186
|
-
|
|
187
|
-
if bwt_total < block.len() {
|
|
188
|
-
let zstd_compressed = zstd::encode_all(block, 3)?;
|
|
189
|
-
let zstd_total = 1 + 4 + zstd_compressed.len();
|
|
190
|
-
|
|
191
|
-
if zstd_total < bwt_total {
|
|
192
|
-
let mut result = Vec::with_capacity(zstd_total);
|
|
193
|
-
result.push(BLOCK_FLAG_ZSTD);
|
|
194
|
-
result.extend_from_slice(&(block.len() as u32).to_le_bytes());
|
|
195
|
-
result.extend_from_slice(&zstd_compressed);
|
|
196
|
-
return Ok(result);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
let mut result = Vec::with_capacity(bwt_total);
|
|
200
|
-
result.push(BLOCK_FLAG_BWT);
|
|
201
|
-
result.extend_from_slice(&bwt.primary_index.to_le_bytes());
|
|
202
|
-
result.extend_from_slice(&(block.len() as u32).to_le_bytes());
|
|
203
|
-
result.extend_from_slice(&(rle_data.len() as u32).to_le_bytes());
|
|
204
|
-
result.extend_from_slice(&stats_bytes);
|
|
205
|
-
result.extend_from_slice(&encoded);
|
|
206
|
-
return Ok(result);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
let zstd_compressed = zstd::encode_all(block, 3)?;
|
|
210
|
-
if 1 + 4 + zstd_compressed.len() < block.len() {
|
|
211
|
-
let mut result = Vec::with_capacity(1 + 4 + zstd_compressed.len());
|
|
212
|
-
result.push(BLOCK_FLAG_ZSTD);
|
|
213
|
-
result.extend_from_slice(&(block.len() as u32).to_le_bytes());
|
|
214
|
-
result.extend_from_slice(&zstd_compressed);
|
|
215
|
-
return Ok(result);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
let mut result = Vec::with_capacity(1 + block.len());
|
|
219
|
-
result.push(BLOCK_FLAG_STORE);
|
|
220
|
-
result.extend_from_slice(block);
|
|
221
|
-
Ok(result)
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
fn decompress_block_v2(block: &[u8]) -> Result<Vec<u8>> {
|
|
225
|
-
if block.is_empty() {
|
|
226
|
-
return Err(anyhow::anyhow!("Empty block"));
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
match block[0] {
|
|
230
|
-
BLOCK_FLAG_STORE => Ok(block[1..].to_vec()),
|
|
231
|
-
BLOCK_FLAG_ZSTD => {
|
|
232
|
-
if block.len() < 5 {
|
|
233
|
-
return Err(anyhow::anyhow!("Truncated zstd block"));
|
|
234
|
-
}
|
|
235
|
-
let orig_len = u32::from_le_bytes([block[1], block[2], block[3], block[4]]) as usize;
|
|
236
|
-
let mut decoded = zstd::decode_all(&block[5..])?;
|
|
237
|
-
decoded.truncate(orig_len);
|
|
238
|
-
Ok(decoded)
|
|
239
|
-
}
|
|
240
|
-
BLOCK_FLAG_BWT => {
|
|
241
|
-
if block.len() < 13 {
|
|
242
|
-
return Err(anyhow::anyhow!("Truncated BWT block"));
|
|
243
|
-
}
|
|
244
|
-
let primary_index = u32::from_le_bytes([block[1], block[2], block[3], block[4]]);
|
|
245
|
-
let orig_len = u32::from_le_bytes([block[5], block[6], block[7], block[8]]) as usize;
|
|
246
|
-
let rle_len = u32::from_le_bytes([block[9], block[10], block[11], block[12]]) as usize;
|
|
247
|
-
|
|
248
|
-
let (stats, stats_size) = SymbolStats::deserialize(&block[13..])?;
|
|
249
|
-
let encoded = &block[13 + stats_size..];
|
|
250
|
-
|
|
251
|
-
let rle_data = rans_decode_block(encoded, &stats, rle_len)?;
|
|
252
|
-
let mtf_data = rle0_decode(&rle_data);
|
|
253
|
-
let bwt_data = mtf_decode(&mtf_data);
|
|
254
|
-
let original = bwt_decode(&bwt_data, primary_index)?;
|
|
255
|
-
|
|
256
|
-
if original.len() != orig_len {
|
|
257
|
-
return Err(anyhow::anyhow!("Size mismatch"));
|
|
258
|
-
}
|
|
259
|
-
Ok(original)
|
|
260
|
-
}
|
|
261
|
-
_ => Err(anyhow::anyhow!("Unknown block type: {}", block[0])),
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
fn decompress_block_v1(block: &[u8]) -> Result<Vec<u8>> {
|
|
266
|
-
if block.len() < 12 {
|
|
267
|
-
return Err(anyhow::anyhow!("Block too small"));
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
let primary_index = u32::from_le_bytes([block[0], block[1], block[2], block[3]]);
|
|
271
|
-
let orig_len = u32::from_le_bytes([block[4], block[5], block[6], block[7]]) as usize;
|
|
272
|
-
let rle_len = u32::from_le_bytes([block[8], block[9], block[10], block[11]]) as usize;
|
|
273
|
-
|
|
274
|
-
let (stats, stats_size) = SymbolStats::deserialize(&block[12..])?;
|
|
275
|
-
let encoded = &block[12 + stats_size..];
|
|
276
|
-
|
|
277
|
-
let rle_data = rans_decode_block(encoded, &stats, rle_len)?;
|
|
278
|
-
let mtf_data = rle0_decode(&rle_data);
|
|
279
|
-
let bwt_data = mtf_decode(&mtf_data);
|
|
280
|
-
let original = bwt_decode(&bwt_data, primary_index)?;
|
|
281
|
-
|
|
282
|
-
if original.len() != orig_len {
|
|
283
|
-
return Err(anyhow::anyhow!("Size mismatch"));
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
Ok(original)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
pub fn compress_high_performance(data: &[u8]) -> Result<(Vec<u8>, CompressionStats)> {
|
|
290
|
-
let compressor = HybridCompressor::new();
|
|
291
|
-
compressor.compress(data)
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
pub fn decompress_high_performance(data: &[u8]) -> Result<Vec<u8>> {
|
|
295
|
-
let compressor = HybridCompressor::new();
|
|
296
|
-
compressor.decompress(data)
|
|
297
|
-
}
|