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 CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "roxify_native"
3
- version = "1.11.0"
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.2';
9
+ const VERSION = '1.12.0';
10
10
  function getDirectorySize(dirPath) {
11
11
  let totalSize = 0;
12
12
  try {
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 fn tar_pack_directory(dir_path: &Path) -> Result<Vec<u8>, String> {
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 written = Vec::new();
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 dest = output_dir.join(&safe);
72
- if let Some(parent) = dest.parent() {
73
- std::fs::create_dir_all(parent)
74
- .map_err(|e| format!("mkdir {:?}: {}", parent, e))?;
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
- entry
78
- .unpack(&dest)
79
- .map_err(|e| format!("tar unpack {:?}: {}", dest, e))?;
80
- written.push(safe.to_string_lossy().to_string());
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
- for &v in buf {
91
- a = (a + v as u32) % MOD;
92
- b = (b + a) % MOD;
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::new(), actual_level, d)
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::new(), actual_level)
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 actual_level >= 20 { threads.min(4) } else { threads };
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 buf.len() > 1024 * 1024 {
263
+ if total_len > 256 * 1024 && adaptive_level >= 3 {
194
264
  let _ = encoder.long_distance_matching(true);
195
- let wlog = if buf.len() > 512 * 1024 * 1024 { 28 }
196
- else if buf.len() > 64 * 1024 * 1024 { 27 }
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(buf.len() as u64));
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::new();
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 PBKDF2_ITERS: u32 = 1_000_000;
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 payload_input = [MAGIC, data].concat();
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 payload_input = [MAGIC, data].concat();
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
- let data_without_markers = [PIXEL_MAGIC, &meta_pixel].concat();
200
-
201
- let padding_needed = (3 - (data_without_markers.len() % 3)) % 3;
202
- let padded_data = if padding_needed > 0 {
203
- [&data_without_markers[..], &vec![0u8; padding_needed]].concat()
204
- } else {
205
- data_without_markers
206
- };
195
+ drop(encrypted);
207
196
 
208
- let mut marker_bytes = Vec::with_capacity(12);
209
- for m in &MARKER_START {
210
- marker_bytes.extend_from_slice(&[m.0, m.1, m.2]);
211
- }
212
- marker_bytes.extend_from_slice(&[MARKER_ZSTD.0, MARKER_ZSTD.1, MARKER_ZSTD.2]);
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 data_with_markers = [&marker_bytes[..], &padded_data[..]].concat();
201
+ let marker_start_len = 12;
202
+ let marker_end_len = 9;
215
203
 
216
- let mut marker_end_bytes = Vec::with_capacity(9);
217
- for m in &MARKER_END {
218
- marker_end_bytes.extend_from_slice(&[m.0, m.1, m.2]);
219
- }
220
-
221
- let data_pixels = (data_with_markers.len() + 2) / 3;
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 mut full_data = vec![0u8; total_data_bytes];
215
+ let marker_end_pos = (height - 1) * width * 3 + (width - end_marker_pixels) * 3;
233
216
 
234
- let marker_start_pos = (height - 1) * width * 3 + (width - end_marker_pixels) * 3;
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 copy_len = data_with_markers.len().min(marker_start_pos);
237
- full_data[..copy_len].copy_from_slice(&data_with_markers[..copy_len]);
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
- let end_len = marker_end_bytes.len().min(total_data_bytes - marker_start_pos);
240
- full_data[marker_start_pos..marker_start_pos + end_len]
241
- .copy_from_slice(&marker_end_bytes[..end_len]);
224
+ build_png(width, height, &idat_data, file_list)
225
+ }
242
226
 
243
- let stride = width * 3 + 1;
244
- let mut scanlines = Vec::with_capacity(height * stride);
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
- for row in 0..height {
247
- scanlines.push(0u8);
248
- let src_start = row * width * 3;
249
- let src_end = (row + 1) * width * 3;
250
- scanlines.extend_from_slice(&full_data[src_start..src_end]);
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
- let idat_data = create_raw_deflate(&scanlines);
244
+ flat[pos..pos + PIXEL_MAGIC.len()].copy_from_slice(PIXEL_MAGIC);
245
+ pos += PIXEL_MAGIC.len();
254
246
 
255
- build_png(width, height, &idat_data, file_list)
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 crc_data = Vec::with_capacity(chunk_type.len() + data.len());
327
- crc_data.extend_from_slice(chunk_type);
328
- crc_data.extend_from_slice(data);
329
- let crc = crate::core::crc32_bytes(&crc_data);
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
- let mut result = Vec::with_capacity(data.len() + 6 + (data.len() / 65535 + 1) * 5);
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(65535);
347
+ let chunk_size = (data.len() - offset).min(MAX_BLOCK);
344
348
  let is_last = offset + chunk_size >= data.len();
345
349
 
346
- result.push(if is_last { 0x01 } else { 0x00 });
347
-
348
- result.push(chunk_size as u8);
349
- result.push((chunk_size >> 8) as u8);
350
- result.push(!chunk_size as u8);
351
- result.push((!(chunk_size >> 8)) as u8);
352
-
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 = 256 * 1024;
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;
package/native/lib.rs CHANGED
@@ -24,6 +24,7 @@ mod audio;
24
24
  mod progress;
25
25
  mod reconstitution;
26
26
  mod archive;
27
+ mod streaming;
27
28
 
28
29
  pub use core::*;
29
30
  #[cfg(feature = "gpu")]