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/native/main.rs CHANGED
@@ -14,6 +14,9 @@ mod png_utils;
14
14
  mod audio;
15
15
  mod reconstitution;
16
16
  mod archive;
17
+ mod streaming;
18
+ mod streaming_decode;
19
+ mod streaming_encode;
17
20
 
18
21
  use crate::encoder::ImageFormat;
19
22
  use std::path::PathBuf;
@@ -104,15 +107,28 @@ enum Commands {
104
107
  }
105
108
 
106
109
  fn read_all(path: &PathBuf) -> anyhow::Result<Vec<u8>> {
107
- let mut f = File::open(path)?;
108
- let mut buf = Vec::new();
109
- f.read_to_end(&mut buf)?;
110
- Ok(buf)
110
+ let metadata = std::fs::metadata(path)?;
111
+ let size = metadata.len() as usize;
112
+
113
+ if size > 256 * 1024 * 1024 {
114
+ let file = File::open(path)?;
115
+ let mmap = unsafe { memmap2::Mmap::map(&file)? };
116
+ Ok(mmap.to_vec())
117
+ } else {
118
+ let mut f = File::open(path)?;
119
+ let mut buf = Vec::with_capacity(size);
120
+ f.read_to_end(&mut buf)?;
121
+ Ok(buf)
122
+ }
111
123
  }
112
124
 
113
125
  fn write_all(path: &PathBuf, data: &[u8]) -> anyhow::Result<()> {
114
- let mut f = File::create(path)?;
115
- f.write_all(data)?;
126
+ let f = File::create(path)?;
127
+ let buf_size = if data.len() > 64 * 1024 * 1024 { 16 * 1024 * 1024 }
128
+ else { (8 * 1024 * 1024).min(data.len().max(8192)) };
129
+ let mut writer = std::io::BufWriter::with_capacity(buf_size, f);
130
+ writer.write_all(data)?;
131
+ writer.flush()?;
116
132
  Ok(())
117
133
  }
118
134
 
@@ -143,53 +159,79 @@ fn main() -> anyhow::Result<()> {
143
159
  }
144
160
  Commands::Encode { input, output, level, passphrase, encrypt, name, dict } => {
145
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
+
146
179
  let (payload, file_list_json) = if is_dir {
147
- let tar_data = archive::tar_pack_directory(&input)
148
- .map_err(|e| anyhow::anyhow!(e))?;
149
- let list = archive::tar_file_list(&tar_data)
180
+ let result = archive::tar_pack_directory_with_list(&input)
150
181
  .map_err(|e| anyhow::anyhow!(e))?;
151
- let json_list: Vec<serde_json::Value> = list.iter()
182
+ let json_list: Vec<serde_json::Value> = result.file_list.iter()
152
183
  .map(|(name, size)| serde_json::json!({"name": name, "size": size}))
153
184
  .collect();
154
- (tar_data, Some(serde_json::to_string(&json_list)?))
185
+ (result.data, Some(serde_json::to_string(&json_list)?))
155
186
  } else {
156
187
  let pack_result = packer::pack_path_with_metadata(&input)?;
157
188
  (pack_result.data, pack_result.file_list_json)
158
189
  };
159
190
 
160
- let file_name = name.as_deref()
161
- .or_else(|| input.file_name().and_then(|n| n.to_str()));
162
-
163
191
  let dict_bytes: Option<Vec<u8>> = match dict {
164
192
  Some(path) => Some(read_all(&path)?),
165
193
  None => None,
166
194
  };
167
195
 
168
- let png = if let Some(ref pass) = passphrase {
169
- encoder::encode_to_png_with_encryption_name_and_format_and_filelist(
196
+ let use_streaming = payload.len() > 64 * 1024 * 1024;
197
+
198
+ if use_streaming {
199
+ streaming::encode_to_png_file(
170
200
  &payload,
201
+ &output,
171
202
  level,
172
- Some(pass),
203
+ passphrase.as_deref(),
173
204
  Some(&encrypt),
174
- ImageFormat::Png,
175
205
  file_name,
176
206
  file_list_json.as_deref(),
177
207
  dict_bytes.as_deref(),
178
- )?
208
+ )?;
179
209
  } else {
180
- encoder::encode_to_png_with_encryption_name_and_format_and_filelist(
181
- &payload,
182
- level,
183
- None,
184
- None,
185
- ImageFormat::Png,
186
- file_name,
187
- file_list_json.as_deref(),
188
- dict_bytes.as_deref(),
189
- )?
190
- };
191
-
192
- write_all(&output, &png)?;
210
+ let png = if let Some(ref pass) = passphrase {
211
+ encoder::encode_to_png_with_encryption_name_and_format_and_filelist(
212
+ &payload,
213
+ level,
214
+ Some(pass),
215
+ Some(&encrypt),
216
+ ImageFormat::Png,
217
+ file_name,
218
+ file_list_json.as_deref(),
219
+ dict_bytes.as_deref(),
220
+ )?
221
+ } else {
222
+ encoder::encode_to_png_with_encryption_name_and_format_and_filelist(
223
+ &payload,
224
+ level,
225
+ None,
226
+ None,
227
+ ImageFormat::Png,
228
+ file_name,
229
+ file_list_json.as_deref(),
230
+ dict_bytes.as_deref(),
231
+ )?
232
+ };
233
+ write_all(&output, &png)?;
234
+ }
193
235
 
194
236
  if file_list_json.is_some() {
195
237
  if is_dir {
@@ -246,13 +288,13 @@ fn main() -> anyhow::Result<()> {
246
288
  let is_png = buf.len() >= 8 && &buf[0..8] == &[137, 80, 78, 71, 13, 10, 26, 10];
247
289
  if is_png {
248
290
  let payload = png_utils::extract_payload_from_png(&buf).map_err(|e| anyhow::anyhow!(e))?;
249
- if !payload.is_empty() && (payload[0] == 0x01 || payload[0] == 0x02) {
291
+ if !payload.is_empty() && (payload[0] == 0x01 || payload[0] == 0x02 || payload[0] == 0x03) {
250
292
  println!("Passphrase detected.");
251
293
  } else {
252
294
  println!("No passphrase detected.");
253
295
  }
254
296
  } else {
255
- if !buf.is_empty() && (buf[0] == 0x01 || buf[0] == 0x02) {
297
+ if !buf.is_empty() && (buf[0] == 0x01 || buf[0] == 0x02 || buf[0] == 0x03) {
256
298
  println!("Passphrase detected.");
257
299
  } else {
258
300
  println!("No passphrase detected.");
@@ -294,6 +336,27 @@ fn main() -> anyhow::Result<()> {
294
336
  write_all(&dest, &out)?;
295
337
  }
296
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
+
297
360
  let buf = read_all(&input)?;
298
361
  let dict_bytes: Option<Vec<u8>> = match dict {
299
362
  Some(path) => Some(read_all(&path)?),
@@ -333,7 +396,7 @@ fn main() -> anyhow::Result<()> {
333
396
  buf[1..].to_vec()
334
397
  } else if buf.starts_with(b"ROX1") {
335
398
  buf[4..].to_vec()
336
- } else if buf[0] == 0x01u8 || buf[0] == 0x02u8 {
399
+ } else if buf[0] == 0x01u8 || buf[0] == 0x02u8 || buf[0] == 0x03u8 {
337
400
  let pass = passphrase.as_ref().map(|s: &String| s.as_str());
338
401
  match crate::crypto::try_decrypt(&buf, pass) {
339
402
  Ok(v) => v,
@@ -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,214 @@
1
+ use std::io::{Write, BufWriter};
2
+ use std::fs::File;
3
+ use std::path::Path;
4
+
5
+ const PNG_HEADER: &[u8] = &[137, 80, 78, 71, 13, 10, 26, 10];
6
+ const PIXEL_MAGIC: &[u8] = b"PXL1";
7
+ const MARKER_START: [(u8, u8, u8); 3] = [(255, 0, 0), (0, 255, 0), (0, 0, 255)];
8
+ const MARKER_END: [(u8, u8, u8); 3] = [(0, 0, 255), (0, 255, 0), (255, 0, 0)];
9
+ const MARKER_ZSTD: (u8, u8, u8) = (0, 255, 0);
10
+ const MAGIC: &[u8] = b"ROX1";
11
+
12
+ pub fn encode_to_png_file(
13
+ data: &[u8],
14
+ output_path: &Path,
15
+ compression_level: i32,
16
+ passphrase: Option<&str>,
17
+ encrypt_type: Option<&str>,
18
+ name: Option<&str>,
19
+ file_list: Option<&str>,
20
+ dict: Option<&[u8]>,
21
+ ) -> anyhow::Result<()> {
22
+ let compressed = crate::core::zstd_compress_with_prefix(data, compression_level, dict, MAGIC)
23
+ .map_err(|e| anyhow::anyhow!("Compression failed: {}", e))?;
24
+
25
+ let encrypted = if let Some(pass) = passphrase {
26
+ match encrypt_type.unwrap_or("aes") {
27
+ "xor" => crate::crypto::encrypt_xor(&compressed, pass),
28
+ "aes" => crate::crypto::encrypt_aes(&compressed, pass)?,
29
+ _ => crate::crypto::encrypt_aes(&compressed, pass)?,
30
+ }
31
+ } else {
32
+ crate::crypto::no_encryption(&compressed)
33
+ };
34
+ drop(compressed);
35
+
36
+ let meta_pixel = build_meta_pixel(&encrypted, name, file_list)?;
37
+ drop(encrypted);
38
+
39
+ let raw_payload_len = PIXEL_MAGIC.len() + meta_pixel.len();
40
+ let padding_needed = (3 - (raw_payload_len % 3)) % 3;
41
+ let padded_len = raw_payload_len + padding_needed;
42
+
43
+ let marker_start_len = 12;
44
+ let data_with_markers_len = marker_start_len + padded_len;
45
+ let data_pixels = (data_with_markers_len + 2) / 3;
46
+ let end_marker_pixels = 3;
47
+ let total_pixels = data_pixels + end_marker_pixels;
48
+
49
+ let side = (total_pixels as f64).sqrt().ceil() as usize;
50
+ let side = side.max(end_marker_pixels);
51
+ let width = side;
52
+ let height = side;
53
+ let row_bytes = width * 3;
54
+ let total_data_bytes = width * height * 3;
55
+ let marker_end_pos = (height - 1) * width * 3 + (width - end_marker_pixels) * 3;
56
+
57
+ let flat = build_flat_buffer(&meta_pixel, padding_needed, marker_end_pos, total_data_bytes);
58
+ drop(meta_pixel);
59
+
60
+ let stride = row_bytes + 1;
61
+ let scanlines_total = height * stride;
62
+
63
+ let mut scanlines = vec![0u8; scanlines_total];
64
+ for row in 0..height {
65
+ let flat_start = row * row_bytes;
66
+ let flat_end = (flat_start + row_bytes).min(flat.len());
67
+ let copy_len = flat_end.saturating_sub(flat_start);
68
+ if copy_len > 0 {
69
+ let dst = row * stride + 1;
70
+ scanlines[dst..dst + copy_len].copy_from_slice(&flat[flat_start..flat_end]);
71
+ }
72
+ }
73
+ drop(flat);
74
+
75
+ let adler = crate::core::adler32_bytes(&scanlines);
76
+
77
+ const MAX_BLOCK: usize = 65535;
78
+ let num_blocks = (scanlines_total + MAX_BLOCK - 1) / MAX_BLOCK;
79
+ let idat_len = 2 + num_blocks * 5 + scanlines_total + 4;
80
+
81
+ let f = File::create(output_path)?;
82
+ let mut w = BufWriter::with_capacity(16 * 1024 * 1024, f);
83
+
84
+ w.write_all(PNG_HEADER)?;
85
+
86
+ let mut ihdr = [0u8; 13];
87
+ ihdr[0..4].copy_from_slice(&(width as u32).to_be_bytes());
88
+ ihdr[4..8].copy_from_slice(&(height as u32).to_be_bytes());
89
+ ihdr[8] = 8;
90
+ ihdr[9] = 2;
91
+ write_chunk_small(&mut w, b"IHDR", &ihdr)?;
92
+
93
+ write_idat_direct(&mut w, &scanlines, idat_len, adler)?;
94
+ drop(scanlines);
95
+
96
+ if let Some(fl) = file_list {
97
+ write_chunk_small(&mut w, b"rXFL", fl.as_bytes())?;
98
+ }
99
+ write_chunk_small(&mut w, b"IEND", &[])?;
100
+ w.flush()?;
101
+
102
+ Ok(())
103
+ }
104
+
105
+ fn write_idat_direct<W: Write>(
106
+ w: &mut W,
107
+ scanlines: &[u8],
108
+ idat_len: usize,
109
+ adler: u32,
110
+ ) -> anyhow::Result<()> {
111
+ const MAX_BLOCK: usize = 65535;
112
+
113
+ w.write_all(&(idat_len as u32).to_be_bytes())?;
114
+ w.write_all(b"IDAT")?;
115
+
116
+ let mut crc = crc32fast::Hasher::new();
117
+ crc.update(b"IDAT");
118
+
119
+ let zlib = [0x78u8, 0x01];
120
+ w.write_all(&zlib)?;
121
+ crc.update(&zlib);
122
+
123
+ let mut offset = 0;
124
+ while offset < scanlines.len() {
125
+ let chunk_size = (scanlines.len() - offset).min(MAX_BLOCK);
126
+ let is_last = offset + chunk_size >= scanlines.len();
127
+ let header = [
128
+ if is_last { 0x01 } else { 0x00 },
129
+ chunk_size as u8,
130
+ (chunk_size >> 8) as u8,
131
+ !chunk_size as u8,
132
+ (!(chunk_size >> 8)) as u8,
133
+ ];
134
+ w.write_all(&header)?;
135
+ crc.update(&header);
136
+ let slice = &scanlines[offset..offset + chunk_size];
137
+ w.write_all(slice)?;
138
+ crc.update(slice);
139
+ offset += chunk_size;
140
+ }
141
+
142
+ let adler_bytes = adler.to_be_bytes();
143
+ w.write_all(&adler_bytes)?;
144
+ crc.update(&adler_bytes);
145
+
146
+ w.write_all(&crc.finalize().to_be_bytes())?;
147
+ Ok(())
148
+ }
149
+
150
+ fn build_flat_buffer(
151
+ meta_pixel: &[u8],
152
+ _padding_needed: usize,
153
+ marker_end_pos: usize,
154
+ total_data_bytes: usize,
155
+ ) -> Vec<u8> {
156
+ let mut flat = vec![0u8; total_data_bytes];
157
+
158
+ let mut pos = 0;
159
+ for m in &MARKER_START {
160
+ flat[pos] = m.0; flat[pos + 1] = m.1; flat[pos + 2] = m.2;
161
+ pos += 3;
162
+ }
163
+ flat[pos] = MARKER_ZSTD.0; flat[pos + 1] = MARKER_ZSTD.1; flat[pos + 2] = MARKER_ZSTD.2;
164
+ pos += 3;
165
+ flat[pos..pos + PIXEL_MAGIC.len()].copy_from_slice(PIXEL_MAGIC);
166
+ pos += PIXEL_MAGIC.len();
167
+ flat[pos..pos + meta_pixel.len()].copy_from_slice(meta_pixel);
168
+
169
+ if marker_end_pos + 9 <= total_data_bytes {
170
+ for (i, m) in MARKER_END.iter().enumerate() {
171
+ let off = marker_end_pos + i * 3;
172
+ flat[off] = m.0; flat[off + 1] = m.1; flat[off + 2] = m.2;
173
+ }
174
+ }
175
+
176
+ flat
177
+ }
178
+
179
+ fn build_meta_pixel(payload: &[u8], name: Option<&str>, file_list: Option<&str>) -> anyhow::Result<Vec<u8>> {
180
+ let version = 1u8;
181
+ let name_bytes = name.map(|n| n.as_bytes()).unwrap_or(&[]);
182
+ let name_len = name_bytes.len().min(255) as u8;
183
+ let payload_len_bytes = (payload.len() as u32).to_be_bytes();
184
+
185
+ let mut result = Vec::with_capacity(1 + 1 + name_len as usize + 4 + payload.len() + 256);
186
+ result.push(version);
187
+ result.push(name_len);
188
+ if name_len > 0 {
189
+ result.extend_from_slice(&name_bytes[..name_len as usize]);
190
+ }
191
+ result.extend_from_slice(&payload_len_bytes);
192
+ result.extend_from_slice(payload);
193
+
194
+ if let Some(fl) = file_list {
195
+ result.extend_from_slice(b"rXFL");
196
+ let json_bytes = fl.as_bytes();
197
+ result.extend_from_slice(&(json_bytes.len() as u32).to_be_bytes());
198
+ result.extend_from_slice(json_bytes);
199
+ }
200
+
201
+ Ok(result)
202
+ }
203
+
204
+ fn write_chunk_small<W: Write>(w: &mut W, chunk_type: &[u8; 4], data: &[u8]) -> anyhow::Result<()> {
205
+ w.write_all(&(data.len() as u32).to_be_bytes())?;
206
+ w.write_all(chunk_type)?;
207
+ w.write_all(data)?;
208
+
209
+ let mut h = crc32fast::Hasher::new();
210
+ h.update(chunk_type);
211
+ h.update(data);
212
+ w.write_all(&h.finalize().to_be_bytes())?;
213
+ Ok(())
214
+ }