roxify 1.13.7 → 1.13.9

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.
Files changed (56) hide show
  1. package/dist/cli.js +1 -8
  2. package/dist/stub-progress.d.ts +4 -4
  3. package/dist/stub-progress.js +4 -4
  4. package/dist/utils/decoder.d.ts +46 -2
  5. package/dist/utils/decoder.js +248 -38
  6. package/dist/utils/ecc.js +0 -1
  7. package/dist/utils/encoder.d.ts +30 -1
  8. package/dist/utils/encoder.js +34 -18
  9. package/dist/utils/inspection.d.ts +1 -1
  10. package/dist/utils/inspection.js +2 -2
  11. package/dist/utils/robust-audio.js +0 -13
  12. package/dist/utils/robust-image.js +0 -26
  13. package/package.json +12 -29
  14. package/roxify_native-aarch64-apple-darwin.node +0 -0
  15. package/roxify_native-aarch64-pc-windows-msvc.node +0 -0
  16. package/roxify_native-aarch64-unknown-linux-gnu.node +0 -0
  17. package/roxify_native-i686-pc-windows-msvc.node +0 -0
  18. package/roxify_native-i686-unknown-linux-gnu.node +0 -0
  19. package/{dist/rox-macos-universal → roxify_native-universal-apple-darwin.node} +0 -0
  20. package/roxify_native-x86_64-apple-darwin.node +0 -0
  21. package/roxify_native-x86_64-pc-windows-msvc.node +0 -0
  22. package/roxify_native-x86_64-unknown-linux-gnu.node +0 -0
  23. package/scripts/postinstall.cjs +23 -2
  24. package/Cargo.toml +0 -91
  25. package/dist/roxify_native +0 -0
  26. package/dist/roxify_native-macos-arm64 +0 -0
  27. package/dist/roxify_native-macos-x64 +0 -0
  28. package/dist/roxify_native.exe +0 -0
  29. package/native/archive.rs +0 -220
  30. package/native/audio.rs +0 -151
  31. package/native/bench_hybrid.rs +0 -145
  32. package/native/bwt.rs +0 -56
  33. package/native/context_mixing.rs +0 -117
  34. package/native/core.rs +0 -378
  35. package/native/crypto.rs +0 -209
  36. package/native/encoder.rs +0 -405
  37. package/native/hybrid.rs +0 -297
  38. package/native/image_utils.rs +0 -82
  39. package/native/io_advice.rs +0 -43
  40. package/native/io_ntfs_optimized.rs +0 -99
  41. package/native/lib.rs +0 -480
  42. package/native/main.rs +0 -842
  43. package/native/mtf.rs +0 -106
  44. package/native/packer.rs +0 -604
  45. package/native/png_chunk_writer.rs +0 -146
  46. package/native/png_utils.rs +0 -554
  47. package/native/pool.rs +0 -101
  48. package/native/progress.rs +0 -142
  49. package/native/rans.rs +0 -149
  50. package/native/rans_byte.rs +0 -286
  51. package/native/reconstitution.rs +0 -623
  52. package/native/streaming.rs +0 -189
  53. package/native/streaming_decode.rs +0 -625
  54. package/native/streaming_encode.rs +0 -684
  55. package/native/test_small_bwt.rs +0 -31
  56. package/native/test_stages.rs +0 -70
package/native/crypto.rs DELETED
@@ -1,209 +0,0 @@
1
- use anyhow::{anyhow, Result};
2
- use aes_gcm::{
3
- aead::{Aead, KeyInit},
4
- Aes256Gcm, Nonce,
5
- };
6
- use pbkdf2::pbkdf2_hmac;
7
- use rand::RngCore;
8
- use sha2::Sha256;
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
-
17
- const ENC_NONE: u8 = 0x00;
18
- const ENC_AES: u8 = 0x01;
19
- const ENC_XOR: u8 = 0x02;
20
- const ENC_AES_CTR: u8 = 0x03;
21
- const PBKDF2_ITERS: u32 = 600_000;
22
-
23
- pub fn encrypt_xor(data: &[u8], passphrase: &str) -> Vec<u8> {
24
- let key = passphrase.as_bytes();
25
- let mut result = Vec::with_capacity(1 + data.len());
26
- result.push(ENC_XOR);
27
-
28
- for (i, &byte) in data.iter().enumerate() {
29
- result.push(byte ^ key[i % key.len()]);
30
- }
31
-
32
- result
33
- }
34
-
35
- pub fn encrypt_aes(data: &[u8], passphrase: &str) -> Result<Vec<u8>> {
36
- let mut salt = [0u8; 16];
37
- rand::thread_rng().fill_bytes(&mut salt);
38
-
39
- let mut key = [0u8; 32];
40
- pbkdf2_hmac::<Sha256>(passphrase.as_bytes(), &salt, PBKDF2_ITERS, &mut key);
41
-
42
- let cipher = Aes256Gcm::new_from_slice(&key)
43
- .map_err(|e| anyhow::anyhow!("Failed to create cipher: {}", e))?;
44
-
45
- let mut iv = [0u8; 12];
46
- rand::thread_rng().fill_bytes(&mut iv);
47
- let nonce = Nonce::from_slice(&iv);
48
-
49
- let ciphertext = cipher
50
- .encrypt(nonce, data)
51
- .map_err(|e| anyhow::anyhow!("Encryption failed: {}", e))?;
52
-
53
- let cipher_len = ciphertext.len();
54
- if cipher_len < 16 {
55
- return Err(anyhow::anyhow!("Ciphertext too short"));
56
- }
57
-
58
- let tag = &ciphertext[cipher_len - 16..];
59
- let encrypted_data = &ciphertext[..cipher_len - 16];
60
-
61
- let mut result = Vec::with_capacity(1 + 16 + 12 + 16 + encrypted_data.len());
62
- result.push(ENC_AES);
63
- result.extend_from_slice(&salt);
64
- result.extend_from_slice(&iv);
65
- result.extend_from_slice(tag);
66
- result.extend_from_slice(encrypted_data);
67
-
68
- Ok(result)
69
- }
70
-
71
- pub fn no_encryption(data: &[u8]) -> Vec<u8> {
72
- let mut result = Vec::with_capacity(1 + data.len());
73
- result.push(ENC_NONE);
74
- result.extend_from_slice(data);
75
- result
76
- }
77
-
78
- pub fn no_encryption_in_place(mut data: Vec<u8>) -> Vec<u8> {
79
- data.insert(0, ENC_NONE);
80
- data
81
- }
82
-
83
- pub fn decrypt_xor(data: &[u8], passphrase: &str) -> Result<Vec<u8>> {
84
- if data.is_empty() { return Err(anyhow!("Empty xor payload")); }
85
- if passphrase.is_empty() { return Err(anyhow!("Passphrase required")); }
86
- let key = passphrase.as_bytes();
87
- let mut out = Vec::with_capacity(data.len());
88
- for (i, &b) in data.iter().enumerate() {
89
- out.push(b ^ key[i % key.len()]);
90
- }
91
- Ok(out)
92
- }
93
-
94
- pub fn decrypt_aes(data: &[u8], passphrase: &str) -> Result<Vec<u8>> {
95
- if data.len() < 1 + 16 + 12 + 16 { return Err(anyhow!("Invalid AES payload length")); }
96
- let salt = &data[1..17];
97
- let iv = &data[17..29];
98
- let tag = &data[29..45];
99
- let enc = &data[45..];
100
-
101
- let mut key = [0u8; 32];
102
- pbkdf2_hmac::<Sha256>(passphrase.as_bytes(), salt, PBKDF2_ITERS, &mut key);
103
-
104
- let cipher = Aes256Gcm::new_from_slice(&key)
105
- .map_err(|e| anyhow!("Failed to create cipher: {}", e))?;
106
-
107
- let mut combined = Vec::with_capacity(enc.len() + tag.len());
108
- combined.extend_from_slice(enc);
109
- combined.extend_from_slice(tag);
110
-
111
- let nonce = Nonce::from_slice(iv);
112
- let decrypted = cipher.decrypt(nonce, combined.as_ref())
113
- .map_err(|e| anyhow!("AES decryption failed: {}", e))?;
114
- Ok(decrypted)
115
- }
116
-
117
- pub fn try_decrypt(buf: &[u8], passphrase: Option<&str>) -> Result<Vec<u8>> {
118
- if buf.is_empty() { return Err(anyhow!("Empty buffer")); }
119
- let flag = buf[0];
120
- match flag {
121
- ENC_NONE => Ok(buf[1..].to_vec()),
122
- ENC_XOR => {
123
- let pass = passphrase.ok_or_else(|| anyhow!("Passphrase required for XOR decryption"))?;
124
- decrypt_xor(&buf[1..], pass)
125
- }
126
- ENC_AES => {
127
- let pass = passphrase.ok_or_else(|| anyhow!("Passphrase required for AES decryption"))?;
128
- decrypt_aes(buf, pass)
129
- }
130
- ENC_AES_CTR => {
131
- let pass = passphrase.ok_or_else(|| anyhow!("Passphrase required for AES-CTR decryption"))?;
132
- decrypt_aes_ctr(buf, pass)
133
- }
134
- _ => Err(anyhow!("Unknown encryption flag: {}", flag)),
135
- }
136
- }
137
-
138
- pub fn derive_aes_ctr_key(passphrase: &str, salt: &[u8]) -> [u8; 32] {
139
- let mut key = [0u8; 32];
140
- pbkdf2_hmac::<Sha256>(passphrase.as_bytes(), salt, PBKDF2_ITERS, &mut key);
141
- key
142
- }
143
-
144
- pub struct StreamingEncryptor {
145
- cipher: Aes256Ctr,
146
- hmac: HmacSha256,
147
- pub header: Vec<u8>,
148
- }
149
-
150
- impl StreamingEncryptor {
151
- pub fn new(passphrase: &str) -> Result<Self> {
152
- let mut salt = [0u8; 16];
153
- rand::thread_rng().fill_bytes(&mut salt);
154
- let mut iv = [0u8; 16];
155
- rand::thread_rng().fill_bytes(&mut iv);
156
-
157
- let key = derive_aes_ctr_key(passphrase, &salt);
158
- let cipher = Aes256Ctr::new_from_slices(&key, &iv)
159
- .map_err(|e| anyhow!("AES-CTR init: {}", e))?;
160
- let hmac = <HmacSha256 as Mac>::new_from_slice(&key)
161
- .map_err(|e| anyhow!("HMAC init: {}", e))?;
162
-
163
- let mut header = Vec::with_capacity(1 + 16 + 16);
164
- header.push(ENC_AES_CTR);
165
- header.extend_from_slice(&salt);
166
- header.extend_from_slice(&iv);
167
-
168
- Ok(Self { cipher, hmac, header })
169
- }
170
-
171
- pub fn header_len(&self) -> usize {
172
- self.header.len()
173
- }
174
-
175
- pub fn encrypt_chunk(&mut self, buf: &mut [u8]) {
176
- self.cipher.apply_keystream(buf);
177
- self.hmac.update(buf);
178
- }
179
-
180
- pub fn finalize_hmac(self) -> [u8; 32] {
181
- let result = self.hmac.finalize();
182
- result.into_bytes().into()
183
- }
184
- }
185
-
186
- pub fn decrypt_aes_ctr(data: &[u8], passphrase: &str) -> Result<Vec<u8>> {
187
- if data.len() < 1 + 16 + 16 + 32 {
188
- return Err(anyhow!("Invalid AES-CTR payload length"));
189
- }
190
- let salt = &data[1..17];
191
- let iv = &data[17..33];
192
- let hmac_tag = &data[data.len() - 32..];
193
- let ciphertext = &data[33..data.len() - 32];
194
-
195
- let key = derive_aes_ctr_key(passphrase, salt);
196
-
197
- let mut mac = <HmacSha256 as Mac>::new_from_slice(&key)
198
- .map_err(|e| anyhow!("HMAC init: {}", e))?;
199
- mac.update(ciphertext);
200
- mac.verify_slice(hmac_tag)
201
- .map_err(|_| anyhow!("HMAC verification failed - wrong passphrase or corrupted data"))?;
202
-
203
- let mut decrypted = ciphertext.to_vec();
204
- let mut cipher = Aes256Ctr::new_from_slices(&key, iv)
205
- .map_err(|e| anyhow!("AES-CTR init: {}", e))?;
206
- cipher.apply_keystream(&mut decrypted);
207
-
208
- Ok(decrypted)
209
- }
package/native/encoder.rs DELETED
@@ -1,405 +0,0 @@
1
- use anyhow::Result;
2
-
3
- use crate::png_chunk_writer::{write_chunked_idat_bytes, write_png_chunk};
4
-
5
- const MAGIC: &[u8] = b"ROX1";
6
- const PIXEL_MAGIC: &[u8] = b"PXL1";
7
- const PNG_HEADER: &[u8] = &[137, 80, 78, 71, 13, 10, 26, 10];
8
- const HEADER_VERSION_V2: u8 = 2;
9
-
10
- const MARKER_START: [(u8, u8, u8); 3] = [(255, 0, 0), (0, 255, 0), (0, 0, 255)];
11
- const MARKER_END: [(u8, u8, u8); 3] = [(0, 0, 255), (0, 255, 0), (255, 0, 0)];
12
- const MARKER_ZSTD: (u8, u8, u8) = (0, 255, 0);
13
-
14
- #[derive(Debug, Clone, Copy)]
15
- pub enum ImageFormat {
16
- Png,
17
- }
18
-
19
- pub fn encode_to_png(data: &[u8], compression_level: i32) -> Result<Vec<u8>> {
20
- encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, None, None, None, None, None)
21
- }
22
-
23
-
24
- pub fn encode_to_png_with_name(data: &[u8], compression_level: i32, name: Option<&str>) -> Result<Vec<u8>> {
25
- encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, None, None, name, None, None)
26
- }
27
-
28
- pub fn encode_to_png_with_name_and_filelist(data: &[u8], compression_level: i32, name: Option<&str>, file_list: Option<&str>) -> Result<Vec<u8>> {
29
- encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, None, None, name, file_list, None)
30
- }
31
-
32
- pub fn encode_to_png_raw(data: &[u8], compression_level: i32) -> Result<Vec<u8>> {
33
- encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, None, None, None, None, None)
34
- }
35
-
36
- pub fn encode_to_png_with_encryption_and_name(
37
- data: &[u8],
38
- compression_level: i32,
39
- passphrase: Option<&str>,
40
- encrypt_type: Option<&str>,
41
- name: Option<&str>,
42
- ) -> Result<Vec<u8>> {
43
- encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, passphrase, encrypt_type, name, None, None)
44
- }
45
-
46
- pub fn encode_to_png_with_encryption_name_and_filelist(
47
- data: &[u8],
48
- compression_level: i32,
49
- passphrase: Option<&str>,
50
- encrypt_type: Option<&str>,
51
- name: Option<&str>,
52
- file_list: Option<&str>,
53
- ) -> Result<Vec<u8>> {
54
- encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, passphrase, encrypt_type, name, file_list, None)
55
- }
56
-
57
- // ─── WAV container encoding ─────────────────────────────────────────────────
58
-
59
- /// Encode data into WAV container (8-bit PCM).
60
- /// Same compression/encryption pipeline as PNG, but wrapped in a WAV file
61
- /// instead of pixel grid. Overhead: 44 bytes (constant) vs PNG's variable overhead.
62
- pub fn encode_to_wav(data: &[u8], compression_level: i32) -> Result<Vec<u8>> {
63
- encode_to_wav_with_encryption_name_and_filelist(data, compression_level, None, None, None, None)
64
- }
65
-
66
- pub fn encode_to_wav_with_name_and_filelist(
67
- data: &[u8],
68
- compression_level: i32,
69
- name: Option<&str>,
70
- file_list: Option<&str>,
71
- ) -> Result<Vec<u8>> {
72
- encode_to_wav_with_encryption_name_and_filelist(data, compression_level, None, None, name, file_list)
73
- }
74
-
75
- pub fn encode_to_wav_with_encryption_name_and_filelist(
76
- data: &[u8],
77
- compression_level: i32,
78
- passphrase: Option<&str>,
79
- encrypt_type: Option<&str>,
80
- name: Option<&str>,
81
- file_list: Option<&str>,
82
- ) -> Result<Vec<u8>> {
83
- // Same compression + encryption pipeline as PNG
84
- let compressed = crate::core::zstd_compress_with_prefix(data, compression_level, None, MAGIC)
85
- .map_err(|e| anyhow::anyhow!("Compression failed: {}", e))?;
86
-
87
- let encrypted = if let Some(pass) = passphrase {
88
- match encrypt_type.unwrap_or("aes") {
89
- "xor" => crate::crypto::encrypt_xor(&compressed, pass),
90
- "aes" => crate::crypto::encrypt_aes(&compressed, pass)?,
91
- _ => crate::crypto::encrypt_aes(&compressed, pass)?,
92
- }
93
- } else {
94
- crate::crypto::no_encryption_in_place(compressed)
95
- };
96
-
97
- let meta_pixel = build_meta_pixel_with_name_and_filelist(&encrypted, name, file_list)?;
98
-
99
- let wav_payload = [PIXEL_MAGIC, &meta_pixel].concat();
100
-
101
- Ok(crate::audio::bytes_to_wav(&wav_payload))
102
- }
103
-
104
- /// Extract payload from a WAV file and return the raw meta_pixel bytes.
105
- pub fn decode_wav_payload(wav_data: &[u8]) -> Result<Vec<u8>> {
106
- crate::audio::wav_to_bytes(wav_data)
107
- .map_err(|e| anyhow::anyhow!("WAV decode failed: {}", e))
108
- }
109
-
110
- pub fn encode_to_png_with_encryption_name_and_format_and_filelist(
111
- data: &[u8],
112
- compression_level: i32,
113
- passphrase: Option<&str>,
114
- encrypt_type: Option<&str>,
115
- _format: ImageFormat,
116
- name: Option<&str>,
117
- file_list: Option<&str>,
118
- dict: Option<&[u8]>,
119
- ) -> Result<Vec<u8>> {
120
- encode_to_png_with_encryption_name_and_filelist_internal(data, compression_level, passphrase, encrypt_type, name, file_list, dict)
121
- }
122
-
123
- fn encode_to_png_with_encryption_name_and_filelist_internal(
124
- data: &[u8],
125
- compression_level: i32,
126
- passphrase: Option<&str>,
127
- encrypt_type: Option<&str>,
128
- name: Option<&str>,
129
- file_list: Option<&str>,
130
- dict: Option<&[u8]>,
131
- ) -> Result<Vec<u8>> {
132
- let compressed = crate::core::zstd_compress_with_prefix(data, compression_level, dict, MAGIC)
133
- .map_err(|e| anyhow::anyhow!("Compression failed: {}", e))?;
134
-
135
- let encrypted = if let Some(pass) = passphrase {
136
- match encrypt_type.unwrap_or("aes") {
137
- "xor" => crate::crypto::encrypt_xor(&compressed, pass),
138
- "aes" => crate::crypto::encrypt_aes(&compressed, pass)?,
139
- _ => crate::crypto::encrypt_aes(&compressed, pass)?,
140
- }
141
- } else {
142
- crate::crypto::no_encryption_in_place(compressed)
143
- };
144
-
145
- let meta_pixel = build_meta_pixel_with_name_and_filelist(&encrypted, name, file_list)?;
146
- drop(encrypted);
147
-
148
- let raw_payload_len = PIXEL_MAGIC.len() + meta_pixel.len();
149
- let padding_needed = (3 - (raw_payload_len % 3)) % 3;
150
- let padded_len = raw_payload_len + padding_needed;
151
-
152
- let marker_start_len = 12;
153
- let marker_end_bytes = 9;
154
-
155
- let data_with_markers_len = marker_start_len + padded_len;
156
- let data_pixels = (data_with_markers_len + 2) / 3;
157
- let end_marker_pixels = 3;
158
- let total_pixels = data_pixels + end_marker_pixels;
159
-
160
- let side = (total_pixels as f64).sqrt().ceil() as usize;
161
- let side = side.max(end_marker_pixels);
162
- let width = side;
163
- let height = side;
164
-
165
- let total_data_bytes = width * height * 3;
166
- let marker_end_pos = total_data_bytes - marker_end_bytes;
167
-
168
- let flat = build_flat_pixel_buffer(&meta_pixel, marker_end_pos, total_data_bytes);
169
- drop(meta_pixel);
170
-
171
- let row_bytes = width * 3;
172
- let idat_data = create_raw_deflate_from_rows(&flat, row_bytes, height);
173
- drop(flat);
174
-
175
- build_png(width, height, &idat_data, file_list)
176
- }
177
-
178
- fn build_flat_pixel_buffer(
179
- meta_pixel: &[u8],
180
- marker_end_pos: usize,
181
- total_data_bytes: usize,
182
- ) -> Vec<u8> {
183
- let mut flat = Vec::with_capacity(total_data_bytes);
184
-
185
- for m in &MARKER_START {
186
- flat.push(m.0); flat.push(m.1); flat.push(m.2);
187
- }
188
- flat.push(MARKER_ZSTD.0); flat.push(MARKER_ZSTD.1); flat.push(MARKER_ZSTD.2);
189
-
190
- flat.extend_from_slice(PIXEL_MAGIC);
191
- flat.extend_from_slice(meta_pixel);
192
-
193
- let payload_end = flat.len();
194
- if payload_end < marker_end_pos {
195
- flat.resize(marker_end_pos, 0);
196
- }
197
-
198
- for m in &MARKER_END {
199
- flat.push(m.0); flat.push(m.1); flat.push(m.2);
200
- }
201
-
202
- if flat.len() < total_data_bytes {
203
- flat.resize(total_data_bytes, 0);
204
- }
205
-
206
- flat
207
- }
208
-
209
- fn build_meta_pixel_with_name_and_filelist(payload: &[u8], name: Option<&str>, file_list: Option<&str>) -> Result<Vec<u8>> {
210
- let version = HEADER_VERSION_V2;
211
- let name_bytes = name.map(|n| n.as_bytes()).unwrap_or(&[]);
212
- let name_len = name_bytes.len().min(255) as u8;
213
- let payload_len_bytes = (payload.len() as u64).to_be_bytes();
214
-
215
- let mut result = Vec::with_capacity(1 + 1 + name_len as usize + 8 + payload.len() + 256);
216
- result.push(version);
217
- result.push(name_len);
218
-
219
- if name_len > 0 {
220
- result.extend_from_slice(&name_bytes[..name_len as usize]);
221
- }
222
-
223
- result.extend_from_slice(&payload_len_bytes);
224
- result.extend_from_slice(payload);
225
-
226
- if let Some(file_list_json) = file_list {
227
- result.extend_from_slice(b"rXFL");
228
- let json_bytes = file_list_json.as_bytes();
229
- let json_len = json_bytes.len() as u32;
230
- result.extend_from_slice(&json_len.to_be_bytes());
231
- result.extend_from_slice(json_bytes);
232
- }
233
-
234
- Ok(result)
235
- }
236
-
237
- fn build_png(width: usize, height: usize, idat_data: &[u8], file_list: Option<&str>) -> Result<Vec<u8>> {
238
- let mut png = Vec::with_capacity(8 + 25 + 12 + idat_data.len() + 12 + 256);
239
-
240
- png.extend_from_slice(PNG_HEADER);
241
-
242
- let mut ihdr_data = [0u8; 13];
243
- ihdr_data[0..4].copy_from_slice(&(width as u32).to_be_bytes());
244
- ihdr_data[4..8].copy_from_slice(&(height as u32).to_be_bytes());
245
- ihdr_data[8] = 8;
246
- ihdr_data[9] = 2;
247
- ihdr_data[10] = 0;
248
- ihdr_data[11] = 0;
249
- ihdr_data[12] = 0;
250
-
251
- write_png_chunk(&mut png, b"IHDR", &ihdr_data)?;
252
- write_chunked_idat_bytes(&mut png, idat_data)?;
253
-
254
- if let Some(file_list_json) = file_list {
255
- write_png_chunk(&mut png, b"rXFL", file_list_json.as_bytes())?;
256
- }
257
-
258
- write_png_chunk(&mut png, b"IEND", &[])?;
259
-
260
- Ok(png)
261
- }
262
-
263
- fn create_raw_deflate_from_rows(flat: &[u8], row_bytes: usize, height: usize) -> Vec<u8> {
264
- let stride = row_bytes + 1;
265
- let scanlines_total = height * stride;
266
-
267
- let mut scanlines = vec![0u8; scanlines_total];
268
- for row in 0..height {
269
- let flat_start = row * row_bytes;
270
- let flat_end = (flat_start + row_bytes).min(flat.len());
271
- let copy_len = flat_end.saturating_sub(flat_start);
272
- if copy_len > 0 {
273
- let dst_start = row * stride + 1;
274
- scanlines[dst_start..dst_start + copy_len].copy_from_slice(&flat[flat_start..flat_end]);
275
- }
276
- }
277
-
278
- const MAX_BLOCK: usize = 65535;
279
- let num_blocks = (scanlines_total + MAX_BLOCK - 1) / MAX_BLOCK;
280
- let total_size = 2 + num_blocks * 5 + scanlines_total + 4;
281
- let mut result = Vec::with_capacity(total_size);
282
-
283
- result.push(0x78);
284
- result.push(0x01);
285
-
286
- let mut offset = 0;
287
- while offset < scanlines.len() {
288
- let chunk_size = (scanlines.len() - offset).min(MAX_BLOCK);
289
- let is_last = offset + chunk_size >= scanlines.len();
290
- let header = [
291
- if is_last { 0x01 } else { 0x00 },
292
- chunk_size as u8,
293
- (chunk_size >> 8) as u8,
294
- !chunk_size as u8,
295
- (!(chunk_size >> 8)) as u8,
296
- ];
297
- result.extend_from_slice(&header);
298
- result.extend_from_slice(&scanlines[offset..offset + chunk_size]);
299
- offset += chunk_size;
300
- }
301
-
302
- let adler = crate::core::adler32_bytes(&scanlines);
303
- result.extend_from_slice(&adler.to_be_bytes());
304
-
305
- result
306
- }
307
-
308
- #[cfg(test)]
309
- mod tests {
310
- use super::*;
311
- use crate::png_utils;
312
-
313
- #[test]
314
- fn test_rxfl_chunk_present_when_file_list_provided() {
315
- let sample_data = b"hello world".to_vec();
316
- let file_list_json = Some("[{\"name\": \"a.txt\", \"size\": 11}]" as &str);
317
- let png = encode_to_png_with_encryption_name_and_filelist_internal(&sample_data, 3, None, None, None, file_list_json, None)
318
- .expect("encode should succeed");
319
-
320
- let chunks = png_utils::extract_png_chunks(&png).expect("extract chunks");
321
- let found = chunks.iter().any(|c| c.name == "rXFL");
322
- assert!(found, "rXFL chunk must be present when file_list is provided");
323
-
324
- let rxfl_chunk = chunks.into_iter().find(|c| c.name == "rXFL").expect("rXFL present");
325
- let s = String::from_utf8_lossy(&rxfl_chunk.data);
326
- assert!(s.contains("a.txt"), "rXFL chunk should contain the file name");
327
- }
328
-
329
- #[test]
330
- fn test_extract_payload_and_partial_unpack() {
331
- use std::fs;
332
- let base = std::env::temp_dir().join(format!("rox_test_{}", rand::random::<u32>()));
333
- let dir = base.join("data");
334
- fs::create_dir_all(dir.join("sub")).unwrap();
335
- fs::write(dir.join("a.txt"), b"hello").unwrap();
336
- fs::write(dir.join("sub").join("b.txt"), b"world").unwrap();
337
-
338
- let pack_result = crate::packer::pack_path_with_metadata(&dir).expect("pack path");
339
- let png = encode_to_png_with_encryption_name_and_filelist_internal(&pack_result.data, 3, None, None, None, pack_result.file_list_json.as_deref(), None)
340
- .expect("encode should succeed");
341
-
342
- let payload = crate::png_utils::extract_payload_from_png(&png).expect("extract payload");
343
- assert!(payload.len() > 1);
344
- assert_eq!(payload[0], 0x00u8);
345
-
346
- let compressed = payload[1..].to_vec();
347
- let mut decompressed = crate::core::zstd_decompress_bytes(&compressed, None).expect("decompress");
348
- if decompressed.starts_with(b"ROX1") {
349
- decompressed = decompressed[4..].to_vec();
350
- }
351
-
352
- let out_dir = base.join("out");
353
- fs::create_dir_all(&out_dir).unwrap();
354
-
355
- let written = crate::packer::unpack_buffer_to_dir(&decompressed, &out_dir, Some(&["sub/b.txt".to_string()])).expect("unpack");
356
- assert_eq!(written.len(), 1);
357
- let got = fs::read_to_string(out_dir.join("sub").join("b.txt")).unwrap();
358
- assert_eq!(got, "world");
359
- }
360
-
361
- #[test]
362
- fn test_encrypt_decrypt_roundtrip() {
363
- let data = b"hello world".to_vec();
364
- let png = encode_to_png_with_encryption_name_and_filelist(&data, 3, Some("password"), Some("aes"), None, None)
365
- .expect("encode should succeed");
366
- let payload = crate::png_utils::extract_payload_from_png(&png).expect("extract");
367
- let decrypted = crate::crypto::try_decrypt(&payload, Some("password")).expect("decrypt");
368
- let mut decompressed = crate::core::zstd_decompress_bytes(&decrypted, None).expect("decompress");
369
- if decompressed.starts_with(b"ROX1") { decompressed = decompressed[4..].to_vec(); }
370
- assert_eq!(decompressed, data);
371
- }
372
-
373
- #[test]
374
- fn test_marker_end_in_last_3_pixels() {
375
- use image::ImageReader;
376
- use std::io::Cursor;
377
-
378
- for size in &[11, 100, 1000, 5000, 50000] {
379
- let data: Vec<u8> = (0..*size).map(|i| (i % 256) as u8).collect();
380
-
381
- let png_raw = encode_to_png_raw(&data, 3).expect("encode raw");
382
- let png_auto = encode_to_png(&data, 3).expect("encode auto");
383
-
384
- for (label, png) in &[("raw", &png_raw), ("auto", &png_auto)] {
385
- let reader = ImageReader::new(Cursor::new(*png))
386
- .with_guessed_format().unwrap();
387
- let img = reader.decode().unwrap();
388
- let rgb = img.to_rgb8();
389
- let w = rgb.width();
390
- let h = rgb.height();
391
-
392
- let p1 = rgb.get_pixel(w - 3, h - 1);
393
- let p2 = rgb.get_pixel(w - 2, h - 1);
394
- let p3 = rgb.get_pixel(w - 1, h - 1);
395
-
396
- assert_eq!([p1[0], p1[1], p1[2]], [0, 0, 255],
397
- "MARKER_END pixel 0 (blue) wrong for {}@size={}, got {:?}", label, size, p1);
398
- assert_eq!([p2[0], p2[1], p2[2]], [0, 255, 0],
399
- "MARKER_END pixel 1 (green) wrong for {}@size={}, got {:?}", label, size, p2);
400
- assert_eq!([p3[0], p3[1], p3[2]], [255, 0, 0],
401
- "MARKER_END pixel 2 (red) wrong for {}@size={}, got {:?}", label, size, p3);
402
- }
403
- }
404
- }
405
- }