roxify 1.11.0 → 1.12.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 +5 -1
- package/dist/cli.js +1 -1
- package/dist/roxify_native +0 -0
- package/dist/roxify_native.exe +0 -0
- package/libroxify_native-x86_64-unknown-linux-gnu.node +0 -0
- package/native/archive.rs +61 -17
- package/native/core.rs +97 -12
- package/native/crypto.rs +86 -1
- package/native/encoder.rs +107 -57
- package/native/hybrid.rs +1 -1
- package/native/lib.rs +1 -0
- package/native/main.rs +98 -35
- package/native/png_utils.rs +1 -1
- package/native/streaming.rs +214 -0
- package/native/streaming_decode.rs +338 -0
- package/native/streaming_encode.rs +494 -0
- package/package.json +1 -1
- package/roxify_native-x86_64-pc-windows-msvc.node +0 -0
- package/roxify_native-x86_64-unknown-linux-gnu.node +0 -0
package/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "roxify_native"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.12.0"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
publish = false
|
|
6
6
|
|
|
@@ -43,6 +43,10 @@ image = { version = "0.25", default-features = false, features = ["png"] }
|
|
|
43
43
|
walkdir = "2.5.0"
|
|
44
44
|
tar = "0.4"
|
|
45
45
|
aes-gcm = "0.10"
|
|
46
|
+
aes = "0.8"
|
|
47
|
+
ctr = "0.9"
|
|
48
|
+
cipher = { version = "0.4", features = ["std"] }
|
|
49
|
+
hmac = "0.12"
|
|
46
50
|
pbkdf2 = "0.12"
|
|
47
51
|
rand = "0.8"
|
|
48
52
|
sha2 = "0.10"
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { DataFormatError, decodePngToBinary, encodeBinaryToPng, hasPassphraseInP
|
|
|
6
6
|
import { packPathsGenerator, unpackBuffer } from './pack.js';
|
|
7
7
|
import * as cliProgress from './stub-progress.js';
|
|
8
8
|
import { encodeWithRustCLI, isRustBinaryAvailable, } from './utils/rust-cli-wrapper.js';
|
|
9
|
-
const VERSION = '1.
|
|
9
|
+
const VERSION = '1.12.0';
|
|
10
10
|
function getDirectorySize(dirPath) {
|
|
11
11
|
let totalSize = 0;
|
|
12
12
|
try {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/native/archive.rs
CHANGED
|
@@ -4,7 +4,12 @@ use rayon::prelude::*;
|
|
|
4
4
|
use tar::{Archive, Builder, Header};
|
|
5
5
|
use walkdir::WalkDir;
|
|
6
6
|
|
|
7
|
-
pub
|
|
7
|
+
pub struct TarPackResult {
|
|
8
|
+
pub data: Vec<u8>,
|
|
9
|
+
pub file_list: Vec<(String, u64)>,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
pub fn tar_pack_directory_with_list(dir_path: &Path) -> Result<TarPackResult, String> {
|
|
8
13
|
let base = dir_path;
|
|
9
14
|
|
|
10
15
|
let entries: Vec<_> = WalkDir::new(dir_path)
|
|
@@ -27,6 +32,10 @@ pub fn tar_pack_directory(dir_path: &Path) -> Result<Vec<u8>, String> {
|
|
|
27
32
|
})
|
|
28
33
|
.collect();
|
|
29
34
|
|
|
35
|
+
let file_list: Vec<(String, u64)> = file_data.iter()
|
|
36
|
+
.map(|(name, data)| (name.clone(), data.len() as u64))
|
|
37
|
+
.collect();
|
|
38
|
+
|
|
30
39
|
let total_estimate: usize = file_data.iter().map(|(n, d)| 512 + d.len() + 512 + n.len()).sum();
|
|
31
40
|
let mut buf = Vec::with_capacity(total_estimate + 1024);
|
|
32
41
|
{
|
|
@@ -42,20 +51,42 @@ pub fn tar_pack_directory(dir_path: &Path) -> Result<Vec<u8>, String> {
|
|
|
42
51
|
}
|
|
43
52
|
builder.finish().map_err(|e| format!("tar finish: {}", e))?;
|
|
44
53
|
}
|
|
45
|
-
Ok(buf)
|
|
54
|
+
Ok(TarPackResult { data: buf, file_list })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pub fn tar_pack_directory(dir_path: &Path) -> Result<Vec<u8>, String> {
|
|
58
|
+
tar_pack_directory_with_list(dir_path).map(|r| r.data)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pub fn tar_file_list_fast(tar_data: &[u8]) -> Vec<(String, u64)> {
|
|
62
|
+
let mut list = Vec::new();
|
|
63
|
+
let mut pos = 0;
|
|
64
|
+
while pos + 512 <= tar_data.len() {
|
|
65
|
+
let header = &tar_data[pos..pos + 512];
|
|
66
|
+
if header.iter().all(|&b| b == 0) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
let name_end = header[..100].iter().position(|&b| b == 0).unwrap_or(100);
|
|
70
|
+
let name = String::from_utf8_lossy(&header[..name_end]).to_string();
|
|
71
|
+
let size_str = String::from_utf8_lossy(&header[124..136]);
|
|
72
|
+
let size = u64::from_str_radix(size_str.trim().trim_matches('\0'), 8).unwrap_or(0);
|
|
73
|
+
if !name.is_empty() {
|
|
74
|
+
list.push((name, size));
|
|
75
|
+
}
|
|
76
|
+
let data_blocks = (size as usize + 511) / 512;
|
|
77
|
+
pos += 512 + data_blocks * 512;
|
|
78
|
+
}
|
|
79
|
+
list
|
|
46
80
|
}
|
|
47
81
|
|
|
48
82
|
pub fn tar_unpack(tar_data: &[u8], output_dir: &Path) -> Result<Vec<String>, String> {
|
|
49
83
|
let mut archive = Archive::new(Cursor::new(tar_data));
|
|
50
|
-
let mut
|
|
84
|
+
let mut entries_data: Vec<(std::path::PathBuf, Vec<u8>)> = Vec::new();
|
|
51
85
|
|
|
52
86
|
let entries = archive.entries().map_err(|e| format!("tar entries: {}", e))?;
|
|
53
87
|
for entry in entries {
|
|
54
88
|
let mut entry = entry.map_err(|e| format!("tar entry: {}", e))?;
|
|
55
|
-
let path = entry
|
|
56
|
-
.path()
|
|
57
|
-
.map_err(|e| format!("tar entry path: {}", e))?
|
|
58
|
-
.to_path_buf();
|
|
89
|
+
let path = entry.path().map_err(|e| format!("tar entry path: {}", e))?.to_path_buf();
|
|
59
90
|
|
|
60
91
|
let mut safe = std::path::PathBuf::new();
|
|
61
92
|
for comp in path.components() {
|
|
@@ -63,23 +94,36 @@ pub fn tar_unpack(tar_data: &[u8], output_dir: &Path) -> Result<Vec<String>, Str
|
|
|
63
94
|
safe.push(osstr);
|
|
64
95
|
}
|
|
65
96
|
}
|
|
66
|
-
|
|
67
97
|
if safe.as_os_str().is_empty() {
|
|
68
98
|
continue;
|
|
69
99
|
}
|
|
70
100
|
|
|
71
|
-
let
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
101
|
+
let mut data = Vec::with_capacity(entry.size() as usize);
|
|
102
|
+
std::io::Read::read_to_end(&mut entry, &mut data)
|
|
103
|
+
.map_err(|e| format!("tar read {:?}: {}", safe, e))?;
|
|
104
|
+
entries_data.push((safe, data));
|
|
105
|
+
}
|
|
76
106
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
107
|
+
let dirs: std::collections::HashSet<_> = entries_data.iter()
|
|
108
|
+
.filter_map(|(p, _)| {
|
|
109
|
+
let dest = output_dir.join(p);
|
|
110
|
+
dest.parent().map(|d| d.to_path_buf())
|
|
111
|
+
})
|
|
112
|
+
.collect();
|
|
113
|
+
for dir in &dirs {
|
|
114
|
+
std::fs::create_dir_all(dir).map_err(|e| format!("mkdir {:?}: {}", dir, e))?;
|
|
81
115
|
}
|
|
82
116
|
|
|
117
|
+
let written: Vec<String> = entries_data.par_iter()
|
|
118
|
+
.filter_map(|(safe, data)| {
|
|
119
|
+
let dest = output_dir.join(safe);
|
|
120
|
+
match std::fs::write(&dest, data) {
|
|
121
|
+
Ok(_) => Some(safe.to_string_lossy().to_string()),
|
|
122
|
+
Err(_) => None,
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
.collect();
|
|
126
|
+
|
|
83
127
|
Ok(written)
|
|
84
128
|
}
|
|
85
129
|
|
package/native/core.rs
CHANGED
|
@@ -85,15 +85,55 @@ pub fn crc32_bytes(buf: &[u8]) -> u32 {
|
|
|
85
85
|
|
|
86
86
|
pub fn adler32_bytes(buf: &[u8]) -> u32 {
|
|
87
87
|
const MOD: u32 = 65521;
|
|
88
|
+
const NMAX: usize = 5552;
|
|
89
|
+
|
|
90
|
+
if buf.len() > 4 * 1024 * 1024 {
|
|
91
|
+
return adler32_parallel(buf);
|
|
92
|
+
}
|
|
93
|
+
|
|
88
94
|
let mut a: u32 = 1;
|
|
89
95
|
let mut b: u32 = 0;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
|
|
97
|
+
for chunk in buf.chunks(NMAX) {
|
|
98
|
+
for &v in chunk {
|
|
99
|
+
a += v as u32;
|
|
100
|
+
b += a;
|
|
101
|
+
}
|
|
102
|
+
a %= MOD;
|
|
103
|
+
b %= MOD;
|
|
93
104
|
}
|
|
105
|
+
|
|
94
106
|
(b << 16) | a
|
|
95
107
|
}
|
|
96
108
|
|
|
109
|
+
fn adler32_parallel(buf: &[u8]) -> u32 {
|
|
110
|
+
use rayon::prelude::*;
|
|
111
|
+
const MOD: u32 = 65521;
|
|
112
|
+
const CHUNK: usize = 1024 * 1024;
|
|
113
|
+
|
|
114
|
+
let chunks: Vec<&[u8]> = buf.chunks(CHUNK).collect();
|
|
115
|
+
let partials: Vec<(u32, u32, usize)> = chunks.par_iter().map(|chunk| {
|
|
116
|
+
let mut a: u32 = 0;
|
|
117
|
+
let mut b: u32 = 0;
|
|
118
|
+
for &v in *chunk {
|
|
119
|
+
a += v as u32;
|
|
120
|
+
b += a;
|
|
121
|
+
}
|
|
122
|
+
a %= MOD;
|
|
123
|
+
b %= MOD;
|
|
124
|
+
(a, b, chunk.len())
|
|
125
|
+
}).collect();
|
|
126
|
+
|
|
127
|
+
let mut a: u64 = 1;
|
|
128
|
+
let mut b: u64 = 0;
|
|
129
|
+
for (pa, pb, len) in partials {
|
|
130
|
+
b = (b + pb as u64 + a * len as u64) % MOD as u64;
|
|
131
|
+
a = (a + pa as u64) % MOD as u64;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
((b as u32) << 16) | (a as u32)
|
|
135
|
+
}
|
|
136
|
+
|
|
97
137
|
pub fn delta_encode_bytes(buf: &[u8]) -> Vec<u8> {
|
|
98
138
|
let len = buf.len();
|
|
99
139
|
if len == 0 {
|
|
@@ -173,40 +213,85 @@ pub fn train_zstd_dictionary(sample_paths: &[PathBuf], dict_size: usize) -> Resu
|
|
|
173
213
|
/// For large buffers (>50 MiB) without a dictionary, multiple chunk sizes
|
|
174
214
|
/// are benchmarked on a sample and the best is selected automatically.
|
|
175
215
|
pub fn zstd_compress_bytes(buf: &[u8], level: i32, dict: Option<&[u8]>) -> std::result::Result<Vec<u8>, String> {
|
|
216
|
+
zstd_compress_with_prefix(buf, level, dict, &[])
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
pub fn zstd_compress_with_prefix(buf: &[u8], level: i32, dict: Option<&[u8]>, prefix: &[u8]) -> std::result::Result<Vec<u8>, String> {
|
|
176
220
|
use std::io::Write;
|
|
177
221
|
|
|
178
222
|
let actual_level = level.min(22).max(1);
|
|
223
|
+
let total_len = prefix.len() + buf.len();
|
|
224
|
+
|
|
225
|
+
let adaptive_level = if total_len > 2 * 1024 * 1024 * 1024 {
|
|
226
|
+
actual_level.min(1)
|
|
227
|
+
} else if total_len > 1024 * 1024 * 1024 {
|
|
228
|
+
actual_level.min(3)
|
|
229
|
+
} else if total_len > 256 * 1024 * 1024 {
|
|
230
|
+
actual_level.min(6)
|
|
231
|
+
} else if total_len > 64 * 1024 * 1024 {
|
|
232
|
+
actual_level.min(12)
|
|
233
|
+
} else {
|
|
234
|
+
actual_level
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
if dict.is_none() && total_len < 4 * 1024 * 1024 {
|
|
238
|
+
if prefix.is_empty() {
|
|
239
|
+
return zstd::bulk::compress(buf, adaptive_level)
|
|
240
|
+
.map_err(|e| format!("zstd bulk compress error: {}", e));
|
|
241
|
+
}
|
|
242
|
+
let mut combined = Vec::with_capacity(total_len);
|
|
243
|
+
combined.extend_from_slice(prefix);
|
|
244
|
+
combined.extend_from_slice(buf);
|
|
245
|
+
return zstd::bulk::compress(&combined, adaptive_level)
|
|
246
|
+
.map_err(|e| format!("zstd bulk compress error: {}", e));
|
|
247
|
+
}
|
|
248
|
+
|
|
179
249
|
let mut encoder = if let Some(d) = dict {
|
|
180
|
-
zstd::stream::Encoder::with_dictionary(Vec::
|
|
250
|
+
zstd::stream::Encoder::with_dictionary(Vec::with_capacity(total_len / 2), adaptive_level, d)
|
|
181
251
|
.map_err(|e| format!("zstd encoder init error: {}", e))?
|
|
182
252
|
} else {
|
|
183
|
-
zstd::stream::Encoder::new(Vec::
|
|
253
|
+
zstd::stream::Encoder::new(Vec::with_capacity(total_len / 2), adaptive_level)
|
|
184
254
|
.map_err(|e| format!("zstd encoder init error: {}", e))?
|
|
185
255
|
};
|
|
186
256
|
|
|
187
257
|
let threads = num_cpus::get() as u32;
|
|
188
258
|
if threads > 1 {
|
|
189
|
-
let max_threads = if
|
|
259
|
+
let max_threads = if adaptive_level >= 20 { threads.min(4) } else { threads };
|
|
190
260
|
let _ = encoder.multithread(max_threads);
|
|
191
261
|
}
|
|
192
262
|
|
|
193
|
-
if
|
|
263
|
+
if total_len > 256 * 1024 && adaptive_level >= 3 {
|
|
194
264
|
let _ = encoder.long_distance_matching(true);
|
|
195
|
-
|
|
196
|
-
|
|
265
|
+
}
|
|
266
|
+
if total_len > 256 * 1024 {
|
|
267
|
+
let wlog = if total_len > 1024 * 1024 * 1024 { 30 }
|
|
268
|
+
else if total_len > 512 * 1024 * 1024 { 29 }
|
|
269
|
+
else if total_len > 64 * 1024 * 1024 { 28 }
|
|
270
|
+
else if total_len > 8 * 1024 * 1024 { 27 }
|
|
197
271
|
else { 26 };
|
|
198
272
|
let _ = encoder.window_log(wlog);
|
|
199
273
|
}
|
|
200
274
|
|
|
201
|
-
let _ = encoder.set_pledged_src_size(Some(
|
|
275
|
+
let _ = encoder.set_pledged_src_size(Some(total_len as u64));
|
|
276
|
+
|
|
277
|
+
if !prefix.is_empty() {
|
|
278
|
+
encoder.write_all(prefix).map_err(|e| format!("zstd write prefix error: {}", e))?;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let chunk_size = if total_len > 256 * 1024 * 1024 { 16 * 1024 * 1024 }
|
|
282
|
+
else if total_len > 64 * 1024 * 1024 { 8 * 1024 * 1024 }
|
|
283
|
+
else { buf.len() };
|
|
284
|
+
|
|
285
|
+
for chunk in buf.chunks(chunk_size) {
|
|
286
|
+
encoder.write_all(chunk).map_err(|e| format!("zstd write error: {}", e))?;
|
|
287
|
+
}
|
|
202
288
|
|
|
203
|
-
encoder.write_all(buf).map_err(|e| format!("zstd write error: {}", e))?;
|
|
204
289
|
encoder.finish().map_err(|e| format!("zstd finish error: {}", e))
|
|
205
290
|
}
|
|
206
291
|
|
|
207
292
|
pub fn zstd_decompress_bytes(buf: &[u8], dict: Option<&[u8]>) -> std::result::Result<Vec<u8>, String> {
|
|
208
293
|
use std::io::Read;
|
|
209
|
-
let mut out = Vec::
|
|
294
|
+
let mut out = Vec::with_capacity(buf.len() * 2);
|
|
210
295
|
if let Some(d) = dict {
|
|
211
296
|
let mut decoder = zstd::stream::Decoder::with_dictionary(std::io::Cursor::new(buf), d)
|
|
212
297
|
.map_err(|e| format!("zstd decoder init error: {}", e))?;
|
package/native/crypto.rs
CHANGED
|
@@ -7,10 +7,18 @@ use pbkdf2::pbkdf2_hmac;
|
|
|
7
7
|
use rand::RngCore;
|
|
8
8
|
use sha2::Sha256;
|
|
9
9
|
|
|
10
|
+
use aes::Aes256;
|
|
11
|
+
use cipher::{KeyIvInit, StreamCipher};
|
|
12
|
+
use hmac::{Hmac, Mac};
|
|
13
|
+
|
|
14
|
+
type Aes256Ctr = ctr::Ctr64BE<Aes256>;
|
|
15
|
+
type HmacSha256 = Hmac<Sha256>;
|
|
16
|
+
|
|
10
17
|
const ENC_NONE: u8 = 0x00;
|
|
11
18
|
const ENC_AES: u8 = 0x01;
|
|
12
19
|
const ENC_XOR: u8 = 0x02;
|
|
13
|
-
const
|
|
20
|
+
const ENC_AES_CTR: u8 = 0x03;
|
|
21
|
+
const PBKDF2_ITERS: u32 = 600_000;
|
|
14
22
|
|
|
15
23
|
pub fn encrypt_xor(data: &[u8], passphrase: &str) -> Vec<u8> {
|
|
16
24
|
let key = passphrase.as_bytes();
|
|
@@ -114,6 +122,83 @@ pub fn try_decrypt(buf: &[u8], passphrase: Option<&str>) -> Result<Vec<u8>> {
|
|
|
114
122
|
let pass = passphrase.ok_or_else(|| anyhow!("Passphrase required for AES decryption"))?;
|
|
115
123
|
decrypt_aes(buf, pass)
|
|
116
124
|
}
|
|
125
|
+
ENC_AES_CTR => {
|
|
126
|
+
let pass = passphrase.ok_or_else(|| anyhow!("Passphrase required for AES-CTR decryption"))?;
|
|
127
|
+
decrypt_aes_ctr(buf, pass)
|
|
128
|
+
}
|
|
117
129
|
_ => Err(anyhow!("Unknown encryption flag: {}", flag)),
|
|
118
130
|
}
|
|
119
131
|
}
|
|
132
|
+
|
|
133
|
+
pub fn derive_aes_ctr_key(passphrase: &str, salt: &[u8]) -> [u8; 32] {
|
|
134
|
+
let mut key = [0u8; 32];
|
|
135
|
+
pbkdf2_hmac::<Sha256>(passphrase.as_bytes(), salt, PBKDF2_ITERS, &mut key);
|
|
136
|
+
key
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
pub struct StreamingEncryptor {
|
|
140
|
+
cipher: Aes256Ctr,
|
|
141
|
+
hmac: HmacSha256,
|
|
142
|
+
pub header: Vec<u8>,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
impl StreamingEncryptor {
|
|
146
|
+
pub fn new(passphrase: &str) -> Result<Self> {
|
|
147
|
+
let mut salt = [0u8; 16];
|
|
148
|
+
rand::thread_rng().fill_bytes(&mut salt);
|
|
149
|
+
let mut iv = [0u8; 16];
|
|
150
|
+
rand::thread_rng().fill_bytes(&mut iv);
|
|
151
|
+
|
|
152
|
+
let key = derive_aes_ctr_key(passphrase, &salt);
|
|
153
|
+
let cipher = Aes256Ctr::new_from_slices(&key, &iv)
|
|
154
|
+
.map_err(|e| anyhow!("AES-CTR init: {}", e))?;
|
|
155
|
+
let hmac = <HmacSha256 as Mac>::new_from_slice(&key)
|
|
156
|
+
.map_err(|e| anyhow!("HMAC init: {}", e))?;
|
|
157
|
+
|
|
158
|
+
let mut header = Vec::with_capacity(1 + 16 + 16);
|
|
159
|
+
header.push(ENC_AES_CTR);
|
|
160
|
+
header.extend_from_slice(&salt);
|
|
161
|
+
header.extend_from_slice(&iv);
|
|
162
|
+
|
|
163
|
+
Ok(Self { cipher, hmac, header })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
pub fn header_len(&self) -> usize {
|
|
167
|
+
self.header.len()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
pub fn encrypt_chunk(&mut self, buf: &mut [u8]) {
|
|
171
|
+
self.cipher.apply_keystream(buf);
|
|
172
|
+
self.hmac.update(buf);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
pub fn finalize_hmac(self) -> [u8; 32] {
|
|
176
|
+
let result = self.hmac.finalize();
|
|
177
|
+
result.into_bytes().into()
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
pub fn decrypt_aes_ctr(data: &[u8], passphrase: &str) -> Result<Vec<u8>> {
|
|
182
|
+
if data.len() < 1 + 16 + 16 + 32 {
|
|
183
|
+
return Err(anyhow!("Invalid AES-CTR payload length"));
|
|
184
|
+
}
|
|
185
|
+
let salt = &data[1..17];
|
|
186
|
+
let iv = &data[17..33];
|
|
187
|
+
let hmac_tag = &data[data.len() - 32..];
|
|
188
|
+
let ciphertext = &data[33..data.len() - 32];
|
|
189
|
+
|
|
190
|
+
let key = derive_aes_ctr_key(passphrase, salt);
|
|
191
|
+
|
|
192
|
+
let mut mac = <HmacSha256 as Mac>::new_from_slice(&key)
|
|
193
|
+
.map_err(|e| anyhow!("HMAC init: {}", e))?;
|
|
194
|
+
mac.update(ciphertext);
|
|
195
|
+
mac.verify_slice(hmac_tag)
|
|
196
|
+
.map_err(|_| anyhow!("HMAC verification failed - wrong passphrase or corrupted data"))?;
|
|
197
|
+
|
|
198
|
+
let mut decrypted = ciphertext.to_vec();
|
|
199
|
+
let mut cipher = Aes256Ctr::new_from_slices(&key, iv)
|
|
200
|
+
.map_err(|e| anyhow!("AES-CTR init: {}", e))?;
|
|
201
|
+
cipher.apply_keystream(&mut decrypted);
|
|
202
|
+
|
|
203
|
+
Ok(decrypted)
|
|
204
|
+
}
|
package/native/encoder.rs
CHANGED
|
@@ -2,7 +2,6 @@ use anyhow::Result;
|
|
|
2
2
|
use std::process::{Command, Stdio};
|
|
3
3
|
|
|
4
4
|
const MAGIC: &[u8] = b"ROX1";
|
|
5
|
-
const ENC_NONE: u8 = 0x00;
|
|
6
5
|
const PIXEL_MAGIC: &[u8] = b"PXL1";
|
|
7
6
|
const PNG_HEADER: &[u8] = &[137, 80, 78, 71, 13, 10, 26, 10];
|
|
8
7
|
|
|
@@ -112,9 +111,7 @@ pub fn encode_to_wav_with_encryption_name_and_filelist(
|
|
|
112
111
|
file_list: Option<&str>,
|
|
113
112
|
) -> Result<Vec<u8>> {
|
|
114
113
|
// Same compression + encryption pipeline as PNG
|
|
115
|
-
let
|
|
116
|
-
|
|
117
|
-
let compressed = crate::core::zstd_compress_bytes(&payload_input, compression_level, None)
|
|
114
|
+
let compressed = crate::core::zstd_compress_with_prefix(data, compression_level, None, MAGIC)
|
|
118
115
|
.map_err(|e| anyhow::anyhow!("Compression failed: {}", e))?;
|
|
119
116
|
|
|
120
117
|
let encrypted = if let Some(pass) = passphrase {
|
|
@@ -180,9 +177,7 @@ fn encode_to_png_with_encryption_name_and_filelist_internal(
|
|
|
180
177
|
file_list: Option<&str>,
|
|
181
178
|
dict: Option<&[u8]>,
|
|
182
179
|
) -> Result<Vec<u8>> {
|
|
183
|
-
let
|
|
184
|
-
|
|
185
|
-
let compressed = crate::core::zstd_compress_bytes(&payload_input, compression_level, dict)
|
|
180
|
+
let compressed = crate::core::zstd_compress_with_prefix(data, compression_level, dict, MAGIC)
|
|
186
181
|
.map_err(|e| anyhow::anyhow!("Compression failed: {}", e))?;
|
|
187
182
|
|
|
188
183
|
let encrypted = if let Some(pass) = passphrase {
|
|
@@ -194,65 +189,71 @@ fn encode_to_png_with_encryption_name_and_filelist_internal(
|
|
|
194
189
|
} else {
|
|
195
190
|
crate::crypto::no_encryption(&compressed)
|
|
196
191
|
};
|
|
192
|
+
drop(compressed);
|
|
197
193
|
|
|
198
194
|
let meta_pixel = build_meta_pixel_with_name_and_filelist(&encrypted, name, file_list)?;
|
|
199
|
-
|
|
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
|
-
};
|
|
195
|
+
drop(encrypted);
|
|
207
196
|
|
|
208
|
-
let
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
marker_bytes.extend_from_slice(&[MARKER_ZSTD.0, MARKER_ZSTD.1, MARKER_ZSTD.2]);
|
|
197
|
+
let raw_payload_len = PIXEL_MAGIC.len() + meta_pixel.len();
|
|
198
|
+
let padding_needed = (3 - (raw_payload_len % 3)) % 3;
|
|
199
|
+
let padded_len = raw_payload_len + padding_needed;
|
|
213
200
|
|
|
214
|
-
let
|
|
201
|
+
let marker_start_len = 12;
|
|
202
|
+
let marker_end_len = 9;
|
|
215
203
|
|
|
216
|
-
let
|
|
217
|
-
|
|
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;
|
|
204
|
+
let data_with_markers_len = marker_start_len + padded_len;
|
|
205
|
+
let data_pixels = (data_with_markers_len + 2) / 3;
|
|
222
206
|
let end_marker_pixels = 3;
|
|
223
207
|
let total_pixels = data_pixels + end_marker_pixels;
|
|
224
208
|
|
|
225
209
|
let side = (total_pixels as f64).sqrt().ceil() as usize;
|
|
226
210
|
let side = side.max(end_marker_pixels);
|
|
227
|
-
|
|
228
211
|
let width = side;
|
|
229
212
|
let height = side;
|
|
230
213
|
|
|
231
214
|
let total_data_bytes = width * height * 3;
|
|
232
|
-
let
|
|
215
|
+
let marker_end_pos = (height - 1) * width * 3 + (width - end_marker_pixels) * 3;
|
|
233
216
|
|
|
234
|
-
let
|
|
217
|
+
let flat = build_flat_pixel_buffer(&meta_pixel, padding_needed, marker_end_pos, marker_end_len, total_data_bytes);
|
|
218
|
+
drop(meta_pixel);
|
|
235
219
|
|
|
236
|
-
let
|
|
237
|
-
|
|
220
|
+
let row_bytes = width * 3;
|
|
221
|
+
let idat_data = create_raw_deflate_from_rows(&flat, row_bytes, height);
|
|
222
|
+
drop(flat);
|
|
238
223
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
.copy_from_slice(&marker_end_bytes[..end_len]);
|
|
224
|
+
build_png(width, height, &idat_data, file_list)
|
|
225
|
+
}
|
|
242
226
|
|
|
243
|
-
|
|
244
|
-
|
|
227
|
+
fn build_flat_pixel_buffer(
|
|
228
|
+
meta_pixel: &[u8],
|
|
229
|
+
_padding_needed: usize,
|
|
230
|
+
marker_end_pos: usize,
|
|
231
|
+
marker_end_len: usize,
|
|
232
|
+
total_data_bytes: usize,
|
|
233
|
+
) -> Vec<u8> {
|
|
234
|
+
let mut flat = vec![0u8; total_data_bytes];
|
|
245
235
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
scanlines.extend_from_slice(&full_data[src_start..src_end]);
|
|
236
|
+
let mut pos = 0;
|
|
237
|
+
for m in &MARKER_START {
|
|
238
|
+
flat[pos] = m.0; flat[pos + 1] = m.1; flat[pos + 2] = m.2;
|
|
239
|
+
pos += 3;
|
|
251
240
|
}
|
|
241
|
+
flat[pos] = MARKER_ZSTD.0; flat[pos + 1] = MARKER_ZSTD.1; flat[pos + 2] = MARKER_ZSTD.2;
|
|
242
|
+
pos += 3;
|
|
252
243
|
|
|
253
|
-
|
|
244
|
+
flat[pos..pos + PIXEL_MAGIC.len()].copy_from_slice(PIXEL_MAGIC);
|
|
245
|
+
pos += PIXEL_MAGIC.len();
|
|
254
246
|
|
|
255
|
-
|
|
247
|
+
flat[pos..pos + meta_pixel.len()].copy_from_slice(meta_pixel);
|
|
248
|
+
|
|
249
|
+
if marker_end_pos + marker_end_len <= total_data_bytes {
|
|
250
|
+
for (i, m) in MARKER_END.iter().enumerate() {
|
|
251
|
+
let off = marker_end_pos + i * 3;
|
|
252
|
+
flat[off] = m.0; flat[off + 1] = m.1; flat[off + 2] = m.2;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
flat
|
|
256
257
|
}
|
|
257
258
|
|
|
258
259
|
fn build_meta_pixel(payload: &[u8]) -> Result<Vec<u8>> {
|
|
@@ -323,33 +324,37 @@ fn write_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) -> Result<(
|
|
|
323
324
|
out.extend_from_slice(chunk_type);
|
|
324
325
|
out.extend_from_slice(data);
|
|
325
326
|
|
|
326
|
-
let mut
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
let crc =
|
|
327
|
+
let mut hasher = crc32fast::Hasher::new();
|
|
328
|
+
hasher.update(chunk_type);
|
|
329
|
+
hasher.update(data);
|
|
330
|
+
let crc = hasher.finalize();
|
|
330
331
|
|
|
331
332
|
out.extend_from_slice(&crc.to_be_bytes());
|
|
332
333
|
Ok(())
|
|
333
334
|
}
|
|
334
335
|
|
|
335
336
|
fn create_raw_deflate(data: &[u8]) -> Vec<u8> {
|
|
336
|
-
|
|
337
|
+
const MAX_BLOCK: usize = 65535;
|
|
338
|
+
let num_blocks = (data.len() + MAX_BLOCK - 1) / MAX_BLOCK;
|
|
339
|
+
let total_size = 2 + num_blocks * 5 + data.len() + 4;
|
|
340
|
+
let mut result = Vec::with_capacity(total_size);
|
|
337
341
|
|
|
338
342
|
result.push(0x78);
|
|
339
343
|
result.push(0x01);
|
|
340
344
|
|
|
341
345
|
let mut offset = 0;
|
|
342
346
|
while offset < data.len() {
|
|
343
|
-
let chunk_size = (data.len() - offset).min(
|
|
347
|
+
let chunk_size = (data.len() - offset).min(MAX_BLOCK);
|
|
344
348
|
let is_last = offset + chunk_size >= data.len();
|
|
345
349
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
350
|
+
let header = [
|
|
351
|
+
if is_last { 0x01 } else { 0x00 },
|
|
352
|
+
chunk_size as u8,
|
|
353
|
+
(chunk_size >> 8) as u8,
|
|
354
|
+
!chunk_size as u8,
|
|
355
|
+
(!(chunk_size >> 8)) as u8,
|
|
356
|
+
];
|
|
357
|
+
result.extend_from_slice(&header);
|
|
353
358
|
result.extend_from_slice(&data[offset..offset + chunk_size]);
|
|
354
359
|
offset += chunk_size;
|
|
355
360
|
}
|
|
@@ -360,6 +365,51 @@ fn create_raw_deflate(data: &[u8]) -> Vec<u8> {
|
|
|
360
365
|
result
|
|
361
366
|
}
|
|
362
367
|
|
|
368
|
+
fn create_raw_deflate_from_rows(flat: &[u8], row_bytes: usize, height: usize) -> Vec<u8> {
|
|
369
|
+
let stride = row_bytes + 1;
|
|
370
|
+
let scanlines_total = height * stride;
|
|
371
|
+
|
|
372
|
+
let mut scanlines = vec![0u8; scanlines_total];
|
|
373
|
+
for row in 0..height {
|
|
374
|
+
let flat_start = row * row_bytes;
|
|
375
|
+
let flat_end = (flat_start + row_bytes).min(flat.len());
|
|
376
|
+
let copy_len = flat_end.saturating_sub(flat_start);
|
|
377
|
+
if copy_len > 0 {
|
|
378
|
+
let dst_start = row * stride + 1;
|
|
379
|
+
scanlines[dst_start..dst_start + copy_len].copy_from_slice(&flat[flat_start..flat_end]);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const MAX_BLOCK: usize = 65535;
|
|
384
|
+
let num_blocks = (scanlines_total + MAX_BLOCK - 1) / MAX_BLOCK;
|
|
385
|
+
let total_size = 2 + num_blocks * 5 + scanlines_total + 4;
|
|
386
|
+
let mut result = Vec::with_capacity(total_size);
|
|
387
|
+
|
|
388
|
+
result.push(0x78);
|
|
389
|
+
result.push(0x01);
|
|
390
|
+
|
|
391
|
+
let mut offset = 0;
|
|
392
|
+
while offset < scanlines.len() {
|
|
393
|
+
let chunk_size = (scanlines.len() - offset).min(MAX_BLOCK);
|
|
394
|
+
let is_last = offset + chunk_size >= scanlines.len();
|
|
395
|
+
let header = [
|
|
396
|
+
if is_last { 0x01 } else { 0x00 },
|
|
397
|
+
chunk_size as u8,
|
|
398
|
+
(chunk_size >> 8) as u8,
|
|
399
|
+
!chunk_size as u8,
|
|
400
|
+
(!(chunk_size >> 8)) as u8,
|
|
401
|
+
];
|
|
402
|
+
result.extend_from_slice(&header);
|
|
403
|
+
result.extend_from_slice(&scanlines[offset..offset + chunk_size]);
|
|
404
|
+
offset += chunk_size;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
let adler = crate::core::adler32_bytes(&scanlines);
|
|
408
|
+
result.extend_from_slice(&adler.to_be_bytes());
|
|
409
|
+
|
|
410
|
+
result
|
|
411
|
+
}
|
|
412
|
+
|
|
363
413
|
fn predict_best_format(data: &[u8]) -> ImageFormat {
|
|
364
414
|
if data.len() < 2048 {
|
|
365
415
|
return ImageFormat::Png;
|
package/native/hybrid.rs
CHANGED
|
@@ -5,7 +5,7 @@ use crate::mtf::{mtf_encode, mtf_decode, rle0_encode, rle0_decode};
|
|
|
5
5
|
use crate::rans_byte::{SymbolStats, rans_encode_block, rans_decode_block};
|
|
6
6
|
use crate::context_mixing::analyze_entropy;
|
|
7
7
|
|
|
8
|
-
const BLOCK_SIZE: usize =
|
|
8
|
+
const BLOCK_SIZE: usize = 1024 * 1024;
|
|
9
9
|
|
|
10
10
|
const BLOCK_FLAG_BWT: u8 = 0;
|
|
11
11
|
const BLOCK_FLAG_ZSTD: u8 = 1;
|