roxify 1.11.1 → 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.exe +0 -0
- package/native/crypto.rs +85 -0
- package/native/encoder.rs +1 -3
- package/native/main.rs +43 -6
- package/native/png_utils.rs +1 -1
- package/native/streaming_decode.rs +338 -0
- package/native/streaming_encode.rs +494 -0
- package/package.json +1 -1
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 {
|
package/dist/roxify_native.exe
CHANGED
|
Binary file
|
package/native/crypto.rs
CHANGED
|
@@ -7,9 +7,17 @@ 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;
|
|
20
|
+
const ENC_AES_CTR: u8 = 0x03;
|
|
13
21
|
const PBKDF2_ITERS: u32 = 600_000;
|
|
14
22
|
|
|
15
23
|
pub fn encrypt_xor(data: &[u8], passphrase: &str) -> Vec<u8> {
|
|
@@ -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
|
@@ -226,12 +226,11 @@ fn encode_to_png_with_encryption_name_and_filelist_internal(
|
|
|
226
226
|
|
|
227
227
|
fn build_flat_pixel_buffer(
|
|
228
228
|
meta_pixel: &[u8],
|
|
229
|
-
|
|
229
|
+
_padding_needed: usize,
|
|
230
230
|
marker_end_pos: usize,
|
|
231
231
|
marker_end_len: usize,
|
|
232
232
|
total_data_bytes: usize,
|
|
233
233
|
) -> Vec<u8> {
|
|
234
|
-
let header_len = 12 + PIXEL_MAGIC.len() + meta_pixel.len() + padding_needed;
|
|
235
234
|
let mut flat = vec![0u8; total_data_bytes];
|
|
236
235
|
|
|
237
236
|
let mut pos = 0;
|
|
@@ -246,7 +245,6 @@ fn build_flat_pixel_buffer(
|
|
|
246
245
|
pos += PIXEL_MAGIC.len();
|
|
247
246
|
|
|
248
247
|
flat[pos..pos + meta_pixel.len()].copy_from_slice(meta_pixel);
|
|
249
|
-
pos += meta_pixel.len();
|
|
250
248
|
|
|
251
249
|
if marker_end_pos + marker_end_len <= total_data_bytes {
|
|
252
250
|
for (i, m) in MARKER_END.iter().enumerate() {
|
package/native/main.rs
CHANGED
|
@@ -15,6 +15,8 @@ mod audio;
|
|
|
15
15
|
mod reconstitution;
|
|
16
16
|
mod archive;
|
|
17
17
|
mod streaming;
|
|
18
|
+
mod streaming_decode;
|
|
19
|
+
mod streaming_encode;
|
|
18
20
|
|
|
19
21
|
use crate::encoder::ImageFormat;
|
|
20
22
|
use std::path::PathBuf;
|
|
@@ -157,6 +159,23 @@ fn main() -> anyhow::Result<()> {
|
|
|
157
159
|
}
|
|
158
160
|
Commands::Encode { input, output, level, passphrase, encrypt, name, dict } => {
|
|
159
161
|
let is_dir = input.is_dir();
|
|
162
|
+
|
|
163
|
+
let file_name = name.as_deref()
|
|
164
|
+
.or_else(|| input.file_name().and_then(|n| n.to_str()));
|
|
165
|
+
|
|
166
|
+
if is_dir && dict.is_none() {
|
|
167
|
+
streaming_encode::encode_dir_to_png_encrypted(
|
|
168
|
+
&input,
|
|
169
|
+
&output,
|
|
170
|
+
level,
|
|
171
|
+
file_name,
|
|
172
|
+
passphrase.as_deref(),
|
|
173
|
+
Some(&encrypt),
|
|
174
|
+
)?;
|
|
175
|
+
println!("(TAR archive, rXFL chunk embedded)");
|
|
176
|
+
return Ok(());
|
|
177
|
+
}
|
|
178
|
+
|
|
160
179
|
let (payload, file_list_json) = if is_dir {
|
|
161
180
|
let result = archive::tar_pack_directory_with_list(&input)
|
|
162
181
|
.map_err(|e| anyhow::anyhow!(e))?;
|
|
@@ -169,9 +188,6 @@ fn main() -> anyhow::Result<()> {
|
|
|
169
188
|
(pack_result.data, pack_result.file_list_json)
|
|
170
189
|
};
|
|
171
190
|
|
|
172
|
-
let file_name = name.as_deref()
|
|
173
|
-
.or_else(|| input.file_name().and_then(|n| n.to_str()));
|
|
174
|
-
|
|
175
191
|
let dict_bytes: Option<Vec<u8>> = match dict {
|
|
176
192
|
Some(path) => Some(read_all(&path)?),
|
|
177
193
|
None => None,
|
|
@@ -272,13 +288,13 @@ fn main() -> anyhow::Result<()> {
|
|
|
272
288
|
let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
|
|
273
289
|
if is_png {
|
|
274
290
|
let payload = png_utils::extract_payload_from_png(&buf).map_err(|e| anyhow::anyhow!(e))?;
|
|
275
|
-
if !payload.is_empty() && (payload[0] == 0x01 || payload[0] == 0x02) {
|
|
291
|
+
if !payload.is_empty() && (payload[0] == 0x01 || payload[0] == 0x02 || payload[0] == 0x03) {
|
|
276
292
|
println!("Passphrase detected.");
|
|
277
293
|
} else {
|
|
278
294
|
println!("No passphrase detected.");
|
|
279
295
|
}
|
|
280
296
|
} else {
|
|
281
|
-
if !buf.is_empty() && (buf[0] == 0x01 || buf[0] == 0x02) {
|
|
297
|
+
if !buf.is_empty() && (buf[0] == 0x01 || buf[0] == 0x02 || buf[0] == 0x03) {
|
|
282
298
|
println!("Passphrase detected.");
|
|
283
299
|
} else {
|
|
284
300
|
println!("No passphrase detected.");
|
|
@@ -320,6 +336,27 @@ fn main() -> anyhow::Result<()> {
|
|
|
320
336
|
write_all(&dest, &out)?;
|
|
321
337
|
}
|
|
322
338
|
Commands::Decompress { input, output, files, passphrase, dict } => {
|
|
339
|
+
let file_size = std::fs::metadata(&input).map(|m| m.len()).unwrap_or(0);
|
|
340
|
+
let is_png_file = input.extension().map(|e| e == "png").unwrap_or(false)
|
|
341
|
+
|| (file_size >= 8 && {
|
|
342
|
+
let mut sig = [0u8; 8];
|
|
343
|
+
std::fs::File::open(&input).and_then(|mut f| { use std::io::Read; f.read_exact(&mut sig) }).is_ok()
|
|
344
|
+
&& sig == [137, 80, 78, 71, 13, 10, 26, 10]
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
if is_png_file && files.is_none() && dict.is_none() && file_size > 100_000_000 {
|
|
348
|
+
let out_dir = output.clone().unwrap_or_else(|| PathBuf::from("out.raw"));
|
|
349
|
+
match streaming_decode::streaming_decode_to_dir_encrypted(&input, &out_dir, passphrase.as_deref()) {
|
|
350
|
+
Ok(written) => {
|
|
351
|
+
println!("Unpacked {} files (TAR)", written.len());
|
|
352
|
+
return Ok(());
|
|
353
|
+
}
|
|
354
|
+
Err(e) => {
|
|
355
|
+
eprintln!("Streaming decode failed ({}), falling back to in-memory", e);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
323
360
|
let buf = read_all(&input)?;
|
|
324
361
|
let dict_bytes: Option<Vec<u8>> = match dict {
|
|
325
362
|
Some(path) => Some(read_all(&path)?),
|
|
@@ -359,7 +396,7 @@ fn main() -> anyhow::Result<()> {
|
|
|
359
396
|
buf[1..].to_vec()
|
|
360
397
|
} else if buf.starts_with(b"ROX1") {
|
|
361
398
|
buf[4..].to_vec()
|
|
362
|
-
} else if buf[0] == 0x01u8 || buf[0] == 0x02u8 {
|
|
399
|
+
} else if buf[0] == 0x01u8 || buf[0] == 0x02u8 || buf[0] == 0x03u8 {
|
|
363
400
|
let pass = passphrase.as_ref().map(|s: &String| s.as_str());
|
|
364
401
|
match crate::crypto::try_decrypt(&buf, pass) {
|
|
365
402
|
Ok(v) => v,
|
package/native/png_utils.rs
CHANGED
|
@@ -174,7 +174,7 @@ pub fn extract_payload_from_png(png_data: &[u8]) -> Result<Vec<u8>, String> {
|
|
|
174
174
|
|
|
175
175
|
fn validate_payload_deep(payload: &[u8]) -> bool {
|
|
176
176
|
if payload.len() < 5 { return false; }
|
|
177
|
-
if payload[0] == 0x01 || payload[0] == 0x02 { return true; }
|
|
177
|
+
if payload[0] == 0x01 || payload[0] == 0x02 || payload[0] == 0x03 { return true; }
|
|
178
178
|
let compressed = if payload[0] == 0x00 { &payload[1..] } else { payload };
|
|
179
179
|
if compressed.starts_with(b"ROX1") { return true; }
|
|
180
180
|
crate::core::zstd_decompress_bytes(compressed, None).is_ok()
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
use std::io::Read;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
use cipher::{KeyIvInit, StreamCipher};
|
|
4
|
+
|
|
5
|
+
const PIXEL_MAGIC: &[u8] = b"PXL1";
|
|
6
|
+
const MARKER_BYTES: usize = 12;
|
|
7
|
+
|
|
8
|
+
type Aes256Ctr = ctr::Ctr64BE<aes::Aes256>;
|
|
9
|
+
|
|
10
|
+
pub fn streaming_decode_to_dir(png_path: &Path, out_dir: &Path) -> Result<Vec<String>, String> {
|
|
11
|
+
streaming_decode_to_dir_encrypted(png_path, out_dir, None)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
pub fn streaming_decode_to_dir_encrypted(
|
|
15
|
+
png_path: &Path,
|
|
16
|
+
out_dir: &Path,
|
|
17
|
+
passphrase: Option<&str>,
|
|
18
|
+
) -> Result<Vec<String>, String> {
|
|
19
|
+
let file = std::fs::File::open(png_path).map_err(|e| format!("open: {}", e))?;
|
|
20
|
+
let mmap = unsafe { memmap2::Mmap::map(&file).map_err(|e| format!("mmap: {}", e))? };
|
|
21
|
+
let data = &mmap[..];
|
|
22
|
+
|
|
23
|
+
if data.len() < 8 || &data[0..8] != &[137, 80, 78, 71, 13, 10, 26, 10] {
|
|
24
|
+
return Err("Not a PNG file".into());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let (width, height, idat_data_start, idat_data_end) = parse_png_header(data)?;
|
|
28
|
+
|
|
29
|
+
let mut reader = DeflatePixelReader::new(data, width, height, idat_data_start, idat_data_end);
|
|
30
|
+
|
|
31
|
+
let mut marker_buf = [0u8; MARKER_BYTES];
|
|
32
|
+
reader.read_exact(&mut marker_buf).map_err(|e| format!("read markers: {}", e))?;
|
|
33
|
+
|
|
34
|
+
let mut pxl1 = [0u8; 4];
|
|
35
|
+
reader.read_exact(&mut pxl1).map_err(|e| format!("read PXL1: {}", e))?;
|
|
36
|
+
if &pxl1 != PIXEL_MAGIC {
|
|
37
|
+
return Err(format!("Expected PXL1, got {:?}", pxl1));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let mut hdr = [0u8; 2];
|
|
41
|
+
reader.read_exact(&mut hdr).map_err(|e| format!("read hdr: {}", e))?;
|
|
42
|
+
let _version = hdr[0];
|
|
43
|
+
let name_len = hdr[1] as usize;
|
|
44
|
+
|
|
45
|
+
if name_len > 0 {
|
|
46
|
+
let mut name_buf = vec![0u8; name_len];
|
|
47
|
+
reader.read_exact(&mut name_buf).map_err(|e| format!("read name: {}", e))?;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let mut plen_buf = [0u8; 4];
|
|
51
|
+
reader.read_exact(&mut plen_buf).map_err(|e| format!("read payload_len: {}", e))?;
|
|
52
|
+
let payload_len = u32::from_be_bytes(plen_buf) as u64;
|
|
53
|
+
|
|
54
|
+
let payload_reader = reader.take(payload_len);
|
|
55
|
+
|
|
56
|
+
let first_byte_reader = FirstByteReader::new(payload_reader);
|
|
57
|
+
let (enc_byte, remaining_reader) = first_byte_reader.into_parts()?;
|
|
58
|
+
|
|
59
|
+
match enc_byte {
|
|
60
|
+
0x00 => {
|
|
61
|
+
let mut decoder = zstd::stream::Decoder::new(remaining_reader)
|
|
62
|
+
.map_err(|e| format!("zstd decoder: {}", e))?;
|
|
63
|
+
decoder.window_log_max(31).map_err(|e| format!("zstd window_log_max: {}", e))?;
|
|
64
|
+
read_rox1_and_untar(decoder, out_dir)
|
|
65
|
+
}
|
|
66
|
+
0x03 => {
|
|
67
|
+
let pass = passphrase.ok_or("Passphrase required for AES-CTR decryption")?;
|
|
68
|
+
let mut salt = [0u8; 16];
|
|
69
|
+
let mut iv = [0u8; 16];
|
|
70
|
+
let mut r = remaining_reader;
|
|
71
|
+
r.read_exact(&mut salt).map_err(|e| format!("read salt: {}", e))?;
|
|
72
|
+
r.read_exact(&mut iv).map_err(|e| format!("read iv: {}", e))?;
|
|
73
|
+
|
|
74
|
+
let key = crate::crypto::derive_aes_ctr_key(pass, &salt);
|
|
75
|
+
let cipher = Aes256Ctr::new_from_slices(&key, &iv)
|
|
76
|
+
.map_err(|e| format!("AES-CTR init: {}", e))?;
|
|
77
|
+
|
|
78
|
+
let hmac_size = 32u64;
|
|
79
|
+
let encrypted_data_len = payload_len - 1 - 16 - 16 - hmac_size;
|
|
80
|
+
let ctr_reader = CtrDecryptReader::new(r.take(encrypted_data_len), cipher);
|
|
81
|
+
|
|
82
|
+
let mut decoder = zstd::stream::Decoder::new(ctr_reader)
|
|
83
|
+
.map_err(|e| format!("zstd decoder: {}", e))?;
|
|
84
|
+
decoder.window_log_max(31).map_err(|e| format!("zstd window_log_max: {}", e))?;
|
|
85
|
+
read_rox1_and_untar(decoder, out_dir)
|
|
86
|
+
}
|
|
87
|
+
_ => Err(format!("Unsupported encryption (enc=0x{:02x}) in streaming decode", enc_byte)),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fn read_rox1_and_untar<R: Read>(mut decoder: R, out_dir: &Path) -> Result<Vec<String>, String> {
|
|
92
|
+
let mut magic = [0u8; 4];
|
|
93
|
+
decoder.read_exact(&mut magic).map_err(|e| format!("read ROX1: {}", e))?;
|
|
94
|
+
if &magic != b"ROX1" {
|
|
95
|
+
return Err(format!("Expected ROX1, got {:?}", magic));
|
|
96
|
+
}
|
|
97
|
+
std::fs::create_dir_all(out_dir).map_err(|e| format!("mkdir: {}", e))?;
|
|
98
|
+
tar_unpack_from_reader(decoder, out_dir)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fn parse_png_header(data: &[u8]) -> Result<(usize, usize, usize, usize), String> {
|
|
102
|
+
let mut pos = 8;
|
|
103
|
+
|
|
104
|
+
let mut width = 0usize;
|
|
105
|
+
let mut height = 0usize;
|
|
106
|
+
let mut idat_start = 0usize;
|
|
107
|
+
let mut idat_end = 0usize;
|
|
108
|
+
|
|
109
|
+
while pos + 12 <= data.len() {
|
|
110
|
+
let chunk_len = u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
|
|
111
|
+
let chunk_type = &data[pos + 4..pos + 8];
|
|
112
|
+
let chunk_data_start = pos + 8;
|
|
113
|
+
|
|
114
|
+
if chunk_type == b"IHDR" {
|
|
115
|
+
if chunk_len < 13 {
|
|
116
|
+
return Err("Invalid IHDR".into());
|
|
117
|
+
}
|
|
118
|
+
width = u32::from_be_bytes([
|
|
119
|
+
data[chunk_data_start],
|
|
120
|
+
data[chunk_data_start + 1],
|
|
121
|
+
data[chunk_data_start + 2],
|
|
122
|
+
data[chunk_data_start + 3],
|
|
123
|
+
]) as usize;
|
|
124
|
+
height = u32::from_be_bytes([
|
|
125
|
+
data[chunk_data_start + 4],
|
|
126
|
+
data[chunk_data_start + 5],
|
|
127
|
+
data[chunk_data_start + 6],
|
|
128
|
+
data[chunk_data_start + 7],
|
|
129
|
+
]) as usize;
|
|
130
|
+
} else if chunk_type == b"IDAT" {
|
|
131
|
+
idat_start = chunk_data_start;
|
|
132
|
+
idat_end = chunk_data_start + chunk_len;
|
|
133
|
+
} else if chunk_type == b"IEND" {
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pos = chunk_data_start + chunk_len + 4;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if width == 0 || height == 0 {
|
|
141
|
+
return Err("IHDR not found".into());
|
|
142
|
+
}
|
|
143
|
+
if idat_start == 0 {
|
|
144
|
+
return Err("IDAT not found".into());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
Ok((width, height, idat_start, idat_end))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
struct DeflatePixelReader<'a> {
|
|
151
|
+
data: &'a [u8],
|
|
152
|
+
height: usize,
|
|
153
|
+
offset: usize,
|
|
154
|
+
idat_end: usize,
|
|
155
|
+
block_remaining: usize,
|
|
156
|
+
current_row: usize,
|
|
157
|
+
col_in_row: usize,
|
|
158
|
+
scanline_filter_pending: bool,
|
|
159
|
+
row_bytes: usize,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
impl<'a> DeflatePixelReader<'a> {
|
|
163
|
+
fn new(data: &'a [u8], width: usize, height: usize, idat_data_start: usize, idat_data_end: usize) -> Self {
|
|
164
|
+
let row_bytes = width * 3;
|
|
165
|
+
Self {
|
|
166
|
+
data,
|
|
167
|
+
height,
|
|
168
|
+
offset: idat_data_start + 2,
|
|
169
|
+
idat_end: idat_data_end,
|
|
170
|
+
block_remaining: 0,
|
|
171
|
+
current_row: 0,
|
|
172
|
+
col_in_row: 0,
|
|
173
|
+
scanline_filter_pending: true,
|
|
174
|
+
row_bytes,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fn ensure_block(&mut self) -> Result<(), std::io::Error> {
|
|
179
|
+
if self.block_remaining > 0 {
|
|
180
|
+
return Ok(());
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if self.offset + 5 > self.idat_end {
|
|
184
|
+
return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "No more deflate blocks"));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let len_lo = self.data[self.offset + 1] as usize;
|
|
188
|
+
let len_hi = self.data[self.offset + 2] as usize;
|
|
189
|
+
self.offset += 5;
|
|
190
|
+
|
|
191
|
+
self.block_remaining = len_lo | (len_hi << 8);
|
|
192
|
+
Ok(())
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fn copy_raw_bytes(&mut self, buf: &mut [u8], count: usize) -> Result<usize, std::io::Error> {
|
|
196
|
+
let mut written = 0;
|
|
197
|
+
while written < count {
|
|
198
|
+
self.ensure_block()?;
|
|
199
|
+
let avail = self.block_remaining.min(count - written).min(self.idat_end - self.offset);
|
|
200
|
+
if avail == 0 {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
buf[written..written + avail].copy_from_slice(&self.data[self.offset..self.offset + avail]);
|
|
204
|
+
self.offset += avail;
|
|
205
|
+
self.block_remaining -= avail;
|
|
206
|
+
written += avail;
|
|
207
|
+
}
|
|
208
|
+
Ok(written)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
fn skip_raw_bytes(&mut self, count: usize) -> Result<(), std::io::Error> {
|
|
212
|
+
let mut remaining = count;
|
|
213
|
+
while remaining > 0 {
|
|
214
|
+
self.ensure_block()?;
|
|
215
|
+
let skip = self.block_remaining.min(remaining).min(self.idat_end - self.offset);
|
|
216
|
+
if skip == 0 {
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
self.offset += skip;
|
|
220
|
+
self.block_remaining -= skip;
|
|
221
|
+
remaining -= skip;
|
|
222
|
+
}
|
|
223
|
+
Ok(())
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
impl<'a> Read for DeflatePixelReader<'a> {
|
|
228
|
+
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
|
229
|
+
let mut filled = 0;
|
|
230
|
+
|
|
231
|
+
while filled < buf.len() {
|
|
232
|
+
if self.current_row >= self.height {
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if self.scanline_filter_pending {
|
|
237
|
+
self.skip_raw_bytes(1)?;
|
|
238
|
+
self.scanline_filter_pending = false;
|
|
239
|
+
self.col_in_row = 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if self.col_in_row >= self.row_bytes {
|
|
243
|
+
self.current_row += 1;
|
|
244
|
+
self.scanline_filter_pending = true;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let remaining_in_row = self.row_bytes - self.col_in_row;
|
|
249
|
+
let remaining_in_buf = buf.len() - filled;
|
|
250
|
+
let to_read = remaining_in_row.min(remaining_in_buf);
|
|
251
|
+
|
|
252
|
+
let got = self.copy_raw_bytes(&mut buf[filled..filled + to_read], to_read)?;
|
|
253
|
+
filled += got;
|
|
254
|
+
self.col_in_row += got;
|
|
255
|
+
if got == 0 {
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
Ok(filled)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
struct FirstByteReader<R: Read> {
|
|
265
|
+
inner: R,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
impl<R: Read> FirstByteReader<R> {
|
|
269
|
+
fn new(inner: R) -> Self {
|
|
270
|
+
Self { inner }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fn into_parts(mut self) -> Result<(u8, impl Read), String> {
|
|
274
|
+
let mut byte = [0u8; 1];
|
|
275
|
+
self.inner.read_exact(&mut byte).map_err(|e| format!("read first byte: {}", e))?;
|
|
276
|
+
Ok((byte[0], self.inner))
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
struct CtrDecryptReader<R: Read> {
|
|
281
|
+
inner: R,
|
|
282
|
+
cipher: Aes256Ctr,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
impl<R: Read> CtrDecryptReader<R> {
|
|
286
|
+
fn new(inner: R, cipher: Aes256Ctr) -> Self {
|
|
287
|
+
Self { inner, cipher }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
impl<R: Read> Read for CtrDecryptReader<R> {
|
|
292
|
+
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
|
293
|
+
let n = self.inner.read(buf)?;
|
|
294
|
+
if n > 0 {
|
|
295
|
+
self.cipher.apply_keystream(&mut buf[..n]);
|
|
296
|
+
}
|
|
297
|
+
Ok(n)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
fn tar_unpack_from_reader<R: Read>(reader: R, output_dir: &Path) -> Result<Vec<String>, String> {
|
|
302
|
+
let buf_reader = std::io::BufReader::with_capacity(8 * 1024 * 1024, reader);
|
|
303
|
+
let mut archive = tar::Archive::new(buf_reader);
|
|
304
|
+
let mut written = Vec::new();
|
|
305
|
+
let mut created_dirs = std::collections::HashSet::new();
|
|
306
|
+
|
|
307
|
+
let entries = archive.entries().map_err(|e| format!("tar entries: {}", e))?;
|
|
308
|
+
for entry in entries {
|
|
309
|
+
let mut entry = entry.map_err(|e| format!("tar entry: {}", e))?;
|
|
310
|
+
let path = entry.path().map_err(|e| format!("tar path: {}", e))?.to_path_buf();
|
|
311
|
+
|
|
312
|
+
let mut safe = std::path::PathBuf::new();
|
|
313
|
+
for comp in path.components() {
|
|
314
|
+
if let std::path::Component::Normal(osstr) = comp {
|
|
315
|
+
safe.push(osstr);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if safe.as_os_str().is_empty() {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let dest = output_dir.join(&safe);
|
|
323
|
+
if let Some(parent) = dest.parent() {
|
|
324
|
+
if created_dirs.insert(parent.to_path_buf()) {
|
|
325
|
+
std::fs::create_dir_all(parent).map_err(|e| format!("mkdir {:?}: {}", parent, e))?;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let mut f = std::io::BufWriter::with_capacity(
|
|
330
|
+
(entry.size() as usize).min(4 * 1024 * 1024).max(8192),
|
|
331
|
+
std::fs::File::create(&dest).map_err(|e| format!("create {:?}: {}", dest, e))?,
|
|
332
|
+
);
|
|
333
|
+
std::io::copy(&mut entry, &mut f).map_err(|e| format!("write {:?}: {}", dest, e))?;
|
|
334
|
+
written.push(safe.to_string_lossy().to_string());
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
Ok(written)
|
|
338
|
+
}
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
use std::io::{Write, BufWriter, Read};
|
|
2
|
+
use std::fs::File;
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
use walkdir::WalkDir;
|
|
5
|
+
use tar::{Builder, Header};
|
|
6
|
+
|
|
7
|
+
const PNG_HEADER: &[u8] = &[137, 80, 78, 71, 13, 10, 26, 10];
|
|
8
|
+
const PIXEL_MAGIC: &[u8] = b"PXL1";
|
|
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
|
+
const MAGIC: &[u8] = b"ROX1";
|
|
13
|
+
|
|
14
|
+
pub fn encode_dir_to_png(
|
|
15
|
+
dir_path: &Path,
|
|
16
|
+
output_path: &Path,
|
|
17
|
+
compression_level: i32,
|
|
18
|
+
name: Option<&str>,
|
|
19
|
+
) -> anyhow::Result<()> {
|
|
20
|
+
encode_dir_to_png_encrypted(dir_path, output_path, compression_level, name, None, None)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
pub fn encode_dir_to_png_encrypted(
|
|
24
|
+
dir_path: &Path,
|
|
25
|
+
output_path: &Path,
|
|
26
|
+
compression_level: i32,
|
|
27
|
+
name: Option<&str>,
|
|
28
|
+
passphrase: Option<&str>,
|
|
29
|
+
encrypt_type: Option<&str>,
|
|
30
|
+
) -> anyhow::Result<()> {
|
|
31
|
+
let tmp_zst = output_path.with_extension("tmp.zst");
|
|
32
|
+
|
|
33
|
+
let file_list = compress_dir_to_zst(dir_path, &tmp_zst, compression_level)?;
|
|
34
|
+
let file_list_json = serde_json::to_string(&file_list)?;
|
|
35
|
+
|
|
36
|
+
let result = write_png_from_zst(
|
|
37
|
+
&tmp_zst, output_path, name, Some(&file_list_json),
|
|
38
|
+
passphrase, encrypt_type,
|
|
39
|
+
);
|
|
40
|
+
let _ = std::fs::remove_file(&tmp_zst);
|
|
41
|
+
result
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn compress_dir_to_zst(
|
|
45
|
+
dir_path: &Path,
|
|
46
|
+
zst_path: &Path,
|
|
47
|
+
compression_level: i32,
|
|
48
|
+
) -> anyhow::Result<Vec<serde_json::Value>> {
|
|
49
|
+
let base = dir_path;
|
|
50
|
+
|
|
51
|
+
let entries: Vec<_> = WalkDir::new(dir_path)
|
|
52
|
+
.follow_links(false)
|
|
53
|
+
.into_iter()
|
|
54
|
+
.filter_map(|e| e.ok())
|
|
55
|
+
.filter(|e| e.file_type().is_file())
|
|
56
|
+
.collect();
|
|
57
|
+
|
|
58
|
+
let zst_file = File::create(zst_path)?;
|
|
59
|
+
let buf_writer = BufWriter::with_capacity(16 * 1024 * 1024, zst_file);
|
|
60
|
+
|
|
61
|
+
let actual_level = compression_level.min(3);
|
|
62
|
+
let mut encoder = zstd::stream::Encoder::new(buf_writer, actual_level)
|
|
63
|
+
.map_err(|e| anyhow::anyhow!("zstd init: {}", e))?;
|
|
64
|
+
|
|
65
|
+
let threads = num_cpus::get() as u32;
|
|
66
|
+
if threads > 1 {
|
|
67
|
+
let _ = encoder.multithread(threads);
|
|
68
|
+
}
|
|
69
|
+
let _ = encoder.long_distance_matching(true);
|
|
70
|
+
let _ = encoder.window_log(30);
|
|
71
|
+
|
|
72
|
+
encoder.write_all(MAGIC)?;
|
|
73
|
+
|
|
74
|
+
let mut file_list = Vec::new();
|
|
75
|
+
{
|
|
76
|
+
let mut tar_builder = Builder::new(&mut encoder);
|
|
77
|
+
for entry in &entries {
|
|
78
|
+
let full = entry.path();
|
|
79
|
+
let rel = full.strip_prefix(base).unwrap_or(full);
|
|
80
|
+
let rel_str = rel.to_string_lossy().replace('\\', "/");
|
|
81
|
+
|
|
82
|
+
let metadata = match std::fs::metadata(full) {
|
|
83
|
+
Ok(m) => m,
|
|
84
|
+
Err(_) => continue,
|
|
85
|
+
};
|
|
86
|
+
let size = metadata.len();
|
|
87
|
+
|
|
88
|
+
let mut header = Header::new_gnu();
|
|
89
|
+
header.set_size(size);
|
|
90
|
+
header.set_mode(0o644);
|
|
91
|
+
header.set_cksum();
|
|
92
|
+
|
|
93
|
+
let file = match File::open(full) {
|
|
94
|
+
Ok(f) => f,
|
|
95
|
+
Err(_) => continue,
|
|
96
|
+
};
|
|
97
|
+
let buf_reader = std::io::BufReader::with_capacity(
|
|
98
|
+
(size as usize).min(4 * 1024 * 1024).max(8192),
|
|
99
|
+
file,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
tar_builder.append_data(&mut header, &rel_str, buf_reader)
|
|
103
|
+
.map_err(|e| anyhow::anyhow!("tar append {}: {}", rel_str, e))?;
|
|
104
|
+
|
|
105
|
+
file_list.push(serde_json::json!({"name": rel_str, "size": size}));
|
|
106
|
+
}
|
|
107
|
+
tar_builder.finish().map_err(|e| anyhow::anyhow!("tar finish: {}", e))?;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
encoder.finish().map_err(|e| anyhow::anyhow!("zstd finish: {}", e))?;
|
|
111
|
+
|
|
112
|
+
Ok(file_list)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fn write_png_from_zst(
|
|
116
|
+
zst_path: &Path,
|
|
117
|
+
output_path: &Path,
|
|
118
|
+
name: Option<&str>,
|
|
119
|
+
file_list: Option<&str>,
|
|
120
|
+
passphrase: Option<&str>,
|
|
121
|
+
_encrypt_type: Option<&str>,
|
|
122
|
+
) -> anyhow::Result<()> {
|
|
123
|
+
let zst_size = std::fs::metadata(zst_path)?.len() as usize;
|
|
124
|
+
|
|
125
|
+
let mut encryptor = match passphrase {
|
|
126
|
+
Some(pass) if !pass.is_empty() => Some(crate::crypto::StreamingEncryptor::new(pass)?),
|
|
127
|
+
_ => None,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
let enc_header_len = encryptor.as_ref().map(|e| e.header_len()).unwrap_or(1);
|
|
131
|
+
let hmac_trailer_len: usize = if encryptor.is_some() { 32 } else { 0 };
|
|
132
|
+
|
|
133
|
+
let encrypted_payload_len = enc_header_len + zst_size + hmac_trailer_len;
|
|
134
|
+
|
|
135
|
+
let version = 1u8;
|
|
136
|
+
let name_bytes = name.map(|n| n.as_bytes()).unwrap_or(&[]);
|
|
137
|
+
let name_len = name_bytes.len().min(255) as u8;
|
|
138
|
+
let payload_len_bytes = (encrypted_payload_len as u32).to_be_bytes();
|
|
139
|
+
|
|
140
|
+
let mut meta_header = Vec::with_capacity(1 + 1 + name_len as usize + 4);
|
|
141
|
+
meta_header.push(version);
|
|
142
|
+
meta_header.push(name_len);
|
|
143
|
+
if name_len > 0 {
|
|
144
|
+
meta_header.extend_from_slice(&name_bytes[..name_len as usize]);
|
|
145
|
+
}
|
|
146
|
+
meta_header.extend_from_slice(&payload_len_bytes);
|
|
147
|
+
|
|
148
|
+
let meta_header_len = meta_header.len();
|
|
149
|
+
|
|
150
|
+
let file_list_chunk = file_list.map(|fl| {
|
|
151
|
+
let json_bytes = fl.as_bytes();
|
|
152
|
+
let mut chunk = Vec::with_capacity(4 + 4 + json_bytes.len());
|
|
153
|
+
chunk.extend_from_slice(b"rXFL");
|
|
154
|
+
chunk.extend_from_slice(&(json_bytes.len() as u32).to_be_bytes());
|
|
155
|
+
chunk.extend_from_slice(json_bytes);
|
|
156
|
+
chunk
|
|
157
|
+
});
|
|
158
|
+
let file_list_inline_len = file_list_chunk.as_ref().map(|c| c.len()).unwrap_or(0);
|
|
159
|
+
|
|
160
|
+
let total_meta_pixel_len = meta_header_len + encrypted_payload_len + file_list_inline_len;
|
|
161
|
+
let raw_payload_len = PIXEL_MAGIC.len() + total_meta_pixel_len;
|
|
162
|
+
let padding_needed = (3 - (raw_payload_len % 3)) % 3;
|
|
163
|
+
let padded_len = raw_payload_len + padding_needed;
|
|
164
|
+
|
|
165
|
+
let marker_start_len = 12;
|
|
166
|
+
let data_with_markers_len = marker_start_len + padded_len;
|
|
167
|
+
let data_pixels = (data_with_markers_len + 2) / 3;
|
|
168
|
+
let end_marker_pixels = 3;
|
|
169
|
+
let total_pixels = data_pixels + end_marker_pixels;
|
|
170
|
+
|
|
171
|
+
let side = (total_pixels as f64).sqrt().ceil() as usize;
|
|
172
|
+
let side = side.max(end_marker_pixels);
|
|
173
|
+
let width = side;
|
|
174
|
+
let height = side;
|
|
175
|
+
let row_bytes = width * 3;
|
|
176
|
+
let total_data_bytes = width * height * 3;
|
|
177
|
+
let marker_end_pos = (height - 1) * width * 3 + (width - end_marker_pixels) * 3;
|
|
178
|
+
|
|
179
|
+
let enc_header_bytes = if let Some(ref enc) = encryptor {
|
|
180
|
+
enc.header.clone()
|
|
181
|
+
} else {
|
|
182
|
+
vec![0x00]
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
let header_bytes = build_header_bytes(&meta_header, &enc_header_bytes);
|
|
186
|
+
|
|
187
|
+
let stride = row_bytes + 1;
|
|
188
|
+
let scanlines_total = height * stride;
|
|
189
|
+
|
|
190
|
+
const MAX_BLOCK: usize = 65535;
|
|
191
|
+
let num_blocks = (scanlines_total + MAX_BLOCK - 1) / MAX_BLOCK;
|
|
192
|
+
let idat_len = 2 + num_blocks * 5 + scanlines_total + 4;
|
|
193
|
+
|
|
194
|
+
let out_file = File::create(output_path)?;
|
|
195
|
+
let mut w = BufWriter::with_capacity(16 * 1024 * 1024, out_file);
|
|
196
|
+
|
|
197
|
+
w.write_all(PNG_HEADER)?;
|
|
198
|
+
|
|
199
|
+
let mut ihdr = [0u8; 13];
|
|
200
|
+
ihdr[0..4].copy_from_slice(&(width as u32).to_be_bytes());
|
|
201
|
+
ihdr[4..8].copy_from_slice(&(height as u32).to_be_bytes());
|
|
202
|
+
ihdr[8] = 8;
|
|
203
|
+
ihdr[9] = 2;
|
|
204
|
+
write_chunk_hdr(&mut w, b"IHDR", &ihdr)?;
|
|
205
|
+
|
|
206
|
+
let mut zst_file = File::open(zst_path)?;
|
|
207
|
+
let mut zst_reader = std::io::BufReader::with_capacity(16 * 1024 * 1024, &mut zst_file);
|
|
208
|
+
|
|
209
|
+
write_idat_streaming(
|
|
210
|
+
&mut w,
|
|
211
|
+
&header_bytes,
|
|
212
|
+
&mut zst_reader,
|
|
213
|
+
zst_size,
|
|
214
|
+
file_list_chunk.as_deref(),
|
|
215
|
+
&mut encryptor,
|
|
216
|
+
hmac_trailer_len,
|
|
217
|
+
height,
|
|
218
|
+
row_bytes,
|
|
219
|
+
marker_end_pos,
|
|
220
|
+
idat_len,
|
|
221
|
+
total_data_bytes,
|
|
222
|
+
)?;
|
|
223
|
+
|
|
224
|
+
if let Some(fl) = file_list {
|
|
225
|
+
write_chunk_hdr(&mut w, b"rXFL", fl.as_bytes())?;
|
|
226
|
+
}
|
|
227
|
+
write_chunk_hdr(&mut w, b"IEND", &[])?;
|
|
228
|
+
w.flush()?;
|
|
229
|
+
|
|
230
|
+
Ok(())
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
fn build_header_bytes(meta_header: &[u8], enc_header: &[u8]) -> Vec<u8> {
|
|
234
|
+
let mut header = Vec::with_capacity(12 + PIXEL_MAGIC.len() + meta_header.len() + enc_header.len());
|
|
235
|
+
for m in &MARKER_START {
|
|
236
|
+
header.push(m.0); header.push(m.1); header.push(m.2);
|
|
237
|
+
}
|
|
238
|
+
header.push(MARKER_ZSTD.0); header.push(MARKER_ZSTD.1); header.push(MARKER_ZSTD.2);
|
|
239
|
+
header.extend_from_slice(PIXEL_MAGIC);
|
|
240
|
+
header.extend_from_slice(meta_header);
|
|
241
|
+
header.extend_from_slice(enc_header);
|
|
242
|
+
header
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
fn write_idat_streaming<W: Write, R: Read>(
|
|
246
|
+
w: &mut W,
|
|
247
|
+
header_bytes: &[u8],
|
|
248
|
+
zst_reader: &mut R,
|
|
249
|
+
zst_size: usize,
|
|
250
|
+
file_list_chunk: Option<&[u8]>,
|
|
251
|
+
encryptor: &mut Option<crate::crypto::StreamingEncryptor>,
|
|
252
|
+
hmac_trailer_len: usize,
|
|
253
|
+
height: usize,
|
|
254
|
+
row_bytes: usize,
|
|
255
|
+
marker_end_pos: usize,
|
|
256
|
+
idat_len: usize,
|
|
257
|
+
total_data_bytes: usize,
|
|
258
|
+
) -> anyhow::Result<()> {
|
|
259
|
+
w.write_all(&(idat_len as u32).to_be_bytes())?;
|
|
260
|
+
w.write_all(b"IDAT")?;
|
|
261
|
+
|
|
262
|
+
let mut crc = crc32fast::Hasher::new();
|
|
263
|
+
crc.update(b"IDAT");
|
|
264
|
+
|
|
265
|
+
let stride = row_bytes + 1;
|
|
266
|
+
let scanlines_total = height * stride;
|
|
267
|
+
|
|
268
|
+
let zlib = [0x78u8, 0x01];
|
|
269
|
+
w.write_all(&zlib)?;
|
|
270
|
+
crc.update(&zlib);
|
|
271
|
+
|
|
272
|
+
let fl_chunk_data = file_list_chunk.unwrap_or(&[]);
|
|
273
|
+
let payload_total = header_bytes.len() + zst_size + hmac_trailer_len + fl_chunk_data.len();
|
|
274
|
+
let padding_after = total_data_bytes - payload_total.min(total_data_bytes);
|
|
275
|
+
|
|
276
|
+
let marker_end_bytes = build_marker_end_bytes();
|
|
277
|
+
|
|
278
|
+
let mut flat_pos: usize = 0;
|
|
279
|
+
let mut scanline_pos: usize = 0;
|
|
280
|
+
let mut deflate_block_remaining: usize = 0;
|
|
281
|
+
|
|
282
|
+
let mut header_pos: usize = 0;
|
|
283
|
+
let mut zst_remaining = zst_size;
|
|
284
|
+
let mut hmac_pos: usize = 0;
|
|
285
|
+
let mut hmac_written = hmac_trailer_len == 0;
|
|
286
|
+
let mut hmac_finalized: Option<[u8; 32]> = None;
|
|
287
|
+
let mut fl_pos: usize = 0;
|
|
288
|
+
let mut zero_remaining = padding_after;
|
|
289
|
+
|
|
290
|
+
let mut adler_a: u32 = 1;
|
|
291
|
+
let mut adler_b: u32 = 0;
|
|
292
|
+
|
|
293
|
+
let buf_size = 1024 * 1024;
|
|
294
|
+
let mut transfer_buf = vec![0u8; buf_size];
|
|
295
|
+
|
|
296
|
+
for _row in 0..height {
|
|
297
|
+
if deflate_block_remaining == 0 {
|
|
298
|
+
let remaining_scanlines = scanlines_total - scanline_pos;
|
|
299
|
+
let block_size = remaining_scanlines.min(65535);
|
|
300
|
+
let is_last = scanline_pos + block_size >= scanlines_total;
|
|
301
|
+
let header = [
|
|
302
|
+
if is_last { 0x01 } else { 0x00 },
|
|
303
|
+
block_size as u8,
|
|
304
|
+
(block_size >> 8) as u8,
|
|
305
|
+
!block_size as u8,
|
|
306
|
+
(!(block_size >> 8)) as u8,
|
|
307
|
+
];
|
|
308
|
+
w.write_all(&header)?;
|
|
309
|
+
crc.update(&header);
|
|
310
|
+
deflate_block_remaining = block_size;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
let filter_byte = [0u8];
|
|
314
|
+
w.write_all(&filter_byte)?;
|
|
315
|
+
crc.update(&filter_byte);
|
|
316
|
+
adler_a = (adler_a + 0) % 65521;
|
|
317
|
+
adler_b = (adler_b + adler_a) % 65521;
|
|
318
|
+
scanline_pos += 1;
|
|
319
|
+
deflate_block_remaining -= 1;
|
|
320
|
+
|
|
321
|
+
let mut cols_written = 0;
|
|
322
|
+
while cols_written < row_bytes {
|
|
323
|
+
if deflate_block_remaining == 0 {
|
|
324
|
+
let remaining_scanlines = scanlines_total - scanline_pos;
|
|
325
|
+
let block_size = remaining_scanlines.min(65535);
|
|
326
|
+
let is_last = scanline_pos + block_size >= scanlines_total;
|
|
327
|
+
let header = [
|
|
328
|
+
if is_last { 0x01 } else { 0x00 },
|
|
329
|
+
block_size as u8,
|
|
330
|
+
(block_size >> 8) as u8,
|
|
331
|
+
!block_size as u8,
|
|
332
|
+
(!(block_size >> 8)) as u8,
|
|
333
|
+
];
|
|
334
|
+
w.write_all(&header)?;
|
|
335
|
+
crc.update(&header);
|
|
336
|
+
deflate_block_remaining = block_size;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
let can_write = (row_bytes - cols_written).min(deflate_block_remaining);
|
|
340
|
+
|
|
341
|
+
let mut chunk_written = 0;
|
|
342
|
+
while chunk_written < can_write {
|
|
343
|
+
let need = can_write - chunk_written;
|
|
344
|
+
|
|
345
|
+
let is_marker_end_region = flat_pos >= marker_end_pos && flat_pos < marker_end_pos + 9;
|
|
346
|
+
|
|
347
|
+
if is_marker_end_region {
|
|
348
|
+
let me_offset = flat_pos - marker_end_pos;
|
|
349
|
+
let me_remaining = 9 - me_offset;
|
|
350
|
+
let take = need.min(me_remaining);
|
|
351
|
+
let slice = &marker_end_bytes[me_offset..me_offset + take];
|
|
352
|
+
w.write_all(slice)?;
|
|
353
|
+
crc.update(slice);
|
|
354
|
+
for &b in slice {
|
|
355
|
+
adler_a = (adler_a + b as u32) % 65521;
|
|
356
|
+
adler_b = (adler_b + adler_a) % 65521;
|
|
357
|
+
}
|
|
358
|
+
flat_pos += take;
|
|
359
|
+
chunk_written += take;
|
|
360
|
+
scanline_pos += take;
|
|
361
|
+
deflate_block_remaining -= take;
|
|
362
|
+
cols_written += take;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if header_pos < header_bytes.len() {
|
|
367
|
+
let avail = header_bytes.len() - header_pos;
|
|
368
|
+
let take = need.min(avail);
|
|
369
|
+
let slice = &header_bytes[header_pos..header_pos + take];
|
|
370
|
+
w.write_all(slice)?;
|
|
371
|
+
crc.update(slice);
|
|
372
|
+
for &b in slice {
|
|
373
|
+
adler_a = (adler_a + b as u32) % 65521;
|
|
374
|
+
adler_b = (adler_b + adler_a) % 65521;
|
|
375
|
+
}
|
|
376
|
+
header_pos += take;
|
|
377
|
+
flat_pos += take;
|
|
378
|
+
chunk_written += take;
|
|
379
|
+
scanline_pos += take;
|
|
380
|
+
deflate_block_remaining -= take;
|
|
381
|
+
cols_written += take;
|
|
382
|
+
} else if zst_remaining > 0 {
|
|
383
|
+
let take = need.min(zst_remaining).min(buf_size);
|
|
384
|
+
let got = zst_reader.read(&mut transfer_buf[..take])
|
|
385
|
+
.map_err(|e| anyhow::anyhow!("read zst: {}", e))?;
|
|
386
|
+
if got == 0 { break; }
|
|
387
|
+
if let Some(ref mut enc) = encryptor {
|
|
388
|
+
enc.encrypt_chunk(&mut transfer_buf[..got]);
|
|
389
|
+
}
|
|
390
|
+
w.write_all(&transfer_buf[..got])?;
|
|
391
|
+
crc.update(&transfer_buf[..got]);
|
|
392
|
+
for &b in &transfer_buf[..got] {
|
|
393
|
+
adler_a = (adler_a + b as u32) % 65521;
|
|
394
|
+
adler_b = (adler_b + adler_a) % 65521;
|
|
395
|
+
}
|
|
396
|
+
zst_remaining -= got;
|
|
397
|
+
flat_pos += got;
|
|
398
|
+
chunk_written += got;
|
|
399
|
+
scanline_pos += got;
|
|
400
|
+
deflate_block_remaining -= got;
|
|
401
|
+
cols_written += got;
|
|
402
|
+
} else if !hmac_written {
|
|
403
|
+
if hmac_finalized.is_none() {
|
|
404
|
+
if let Some(enc) = encryptor.take() {
|
|
405
|
+
hmac_finalized = Some(enc.finalize_hmac());
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if let Some(ref hmac_bytes) = hmac_finalized {
|
|
409
|
+
let avail = hmac_trailer_len - hmac_pos;
|
|
410
|
+
let take = need.min(avail);
|
|
411
|
+
let slice = &hmac_bytes[hmac_pos..hmac_pos + take];
|
|
412
|
+
w.write_all(slice)?;
|
|
413
|
+
crc.update(slice);
|
|
414
|
+
for &b in slice {
|
|
415
|
+
adler_a = (adler_a + b as u32) % 65521;
|
|
416
|
+
adler_b = (adler_b + adler_a) % 65521;
|
|
417
|
+
}
|
|
418
|
+
hmac_pos += take;
|
|
419
|
+
flat_pos += take;
|
|
420
|
+
chunk_written += take;
|
|
421
|
+
scanline_pos += take;
|
|
422
|
+
deflate_block_remaining -= take;
|
|
423
|
+
cols_written += take;
|
|
424
|
+
if hmac_pos >= hmac_trailer_len {
|
|
425
|
+
hmac_written = true;
|
|
426
|
+
}
|
|
427
|
+
} else {
|
|
428
|
+
hmac_written = true;
|
|
429
|
+
}
|
|
430
|
+
} else if fl_pos < fl_chunk_data.len() {
|
|
431
|
+
let avail = fl_chunk_data.len() - fl_pos;
|
|
432
|
+
let take = need.min(avail);
|
|
433
|
+
let slice = &fl_chunk_data[fl_pos..fl_pos + take];
|
|
434
|
+
w.write_all(slice)?;
|
|
435
|
+
crc.update(slice);
|
|
436
|
+
for &b in slice {
|
|
437
|
+
adler_a = (adler_a + b as u32) % 65521;
|
|
438
|
+
adler_b = (adler_b + adler_a) % 65521;
|
|
439
|
+
}
|
|
440
|
+
fl_pos += take;
|
|
441
|
+
flat_pos += take;
|
|
442
|
+
chunk_written += take;
|
|
443
|
+
scanline_pos += take;
|
|
444
|
+
deflate_block_remaining -= take;
|
|
445
|
+
cols_written += take;
|
|
446
|
+
} else {
|
|
447
|
+
let take = need.min(zero_remaining).min(buf_size);
|
|
448
|
+
if take == 0 { break; }
|
|
449
|
+
let zeros = vec![0u8; take];
|
|
450
|
+
w.write_all(&zeros)?;
|
|
451
|
+
crc.update(&zeros);
|
|
452
|
+
for _ in 0..take {
|
|
453
|
+
adler_b = (adler_b + adler_a) % 65521;
|
|
454
|
+
}
|
|
455
|
+
zero_remaining -= take;
|
|
456
|
+
flat_pos += take;
|
|
457
|
+
chunk_written += take;
|
|
458
|
+
scanline_pos += take;
|
|
459
|
+
deflate_block_remaining -= take;
|
|
460
|
+
cols_written += take;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
let adler = (adler_b << 16) | adler_a;
|
|
467
|
+
let adler_bytes = adler.to_be_bytes();
|
|
468
|
+
w.write_all(&adler_bytes)?;
|
|
469
|
+
crc.update(&adler_bytes);
|
|
470
|
+
|
|
471
|
+
w.write_all(&crc.finalize().to_be_bytes())?;
|
|
472
|
+
Ok(())
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
fn build_marker_end_bytes() -> [u8; 9] {
|
|
476
|
+
let mut buf = [0u8; 9];
|
|
477
|
+
for (i, m) in MARKER_END.iter().enumerate() {
|
|
478
|
+
buf[i * 3] = m.0;
|
|
479
|
+
buf[i * 3 + 1] = m.1;
|
|
480
|
+
buf[i * 3 + 2] = m.2;
|
|
481
|
+
}
|
|
482
|
+
buf
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
fn write_chunk_hdr<W: Write>(w: &mut W, chunk_type: &[u8; 4], data: &[u8]) -> anyhow::Result<()> {
|
|
486
|
+
w.write_all(&(data.len() as u32).to_be_bytes())?;
|
|
487
|
+
w.write_all(chunk_type)?;
|
|
488
|
+
w.write_all(data)?;
|
|
489
|
+
let mut h = crc32fast::Hasher::new();
|
|
490
|
+
h.update(chunk_type);
|
|
491
|
+
h.update(data);
|
|
492
|
+
w.write_all(&h.finalize().to_be_bytes())?;
|
|
493
|
+
Ok(())
|
|
494
|
+
}
|
package/package.json
CHANGED