roxify 1.9.5 → 1.9.7

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.9.5"
3
+ version = "1.9.7"
4
4
  edition = "2021"
5
5
  publish = false
6
6
 
Binary file
@@ -1170,6 +1170,34 @@ export async function decodePngToBinary(input, opts = {}) {
1170
1170
  e instanceof DataFormatError) {
1171
1171
  throw e;
1172
1172
  }
1173
+ try {
1174
+ const rawPayload = Buffer.from(native.extractPayloadFromPng(processedBuf));
1175
+ let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
1176
+ payload = await tryDecompress(payload, (info) => {
1177
+ if (opts.onProgress)
1178
+ opts.onProgress(info);
1179
+ });
1180
+ if (!payload.slice(0, MAGIC.length).equals(MAGIC)) {
1181
+ throw new DataFormatError('Missing ROX1 magic after native extraction');
1182
+ }
1183
+ payload = payload.slice(MAGIC.length);
1184
+ const nameFromPng = native.extractNameFromPng
1185
+ ? (() => { try {
1186
+ return native.extractNameFromPng(processedBuf);
1187
+ }
1188
+ catch {
1189
+ return undefined;
1190
+ } })()
1191
+ : undefined;
1192
+ return { buf: payload, meta: { name: nameFromPng } };
1193
+ }
1194
+ catch (nativeErr) {
1195
+ if (nativeErr instanceof PassphraseRequiredError ||
1196
+ nativeErr instanceof IncorrectPassphraseError ||
1197
+ nativeErr instanceof DataFormatError) {
1198
+ throw nativeErr;
1199
+ }
1200
+ }
1173
1201
  const errMsg = e instanceof Error ? e.message : String(e);
1174
1202
  throw new Error('Failed to decode PNG: ' + errMsg);
1175
1203
  }
@@ -1,14 +1,21 @@
1
- use image::{ImageFormat, DynamicImage};
1
+ use image::{ImageFormat, DynamicImage, ImageReader};
2
2
  use std::io::Cursor;
3
3
 
4
+ fn load_no_limits(input: &[u8]) -> Result<DynamicImage, String> {
5
+ let mut reader = ImageReader::new(Cursor::new(input))
6
+ .with_guessed_format()
7
+ .map_err(|e| format!("Failed to guess format: {}", e))?;
8
+ reader.no_limits();
9
+ reader.decode().map_err(|e| format!("Failed to load image: {}", e))
10
+ }
11
+
4
12
  pub fn sharp_resize(
5
13
  input: &[u8],
6
14
  width: u32,
7
15
  height: u32,
8
16
  kernel: &str,
9
17
  ) -> Result<Vec<u8>, String> {
10
- let img = image::load_from_memory(input)
11
- .map_err(|e| format!("Failed to load image: {}", e))?;
18
+ let img = load_no_limits(input)?;
12
19
 
13
20
  let filter = match kernel {
14
21
  "nearest" => image::imageops::FilterType::Nearest,
@@ -28,8 +35,7 @@ pub fn sharp_resize(
28
35
  }
29
36
 
30
37
  pub fn sharp_raw_pixels(input: &[u8]) -> Result<(Vec<u8>, u32, u32), String> {
31
- let img = image::load_from_memory(input)
32
- .map_err(|e| format!("Failed to load image: {}", e))?;
38
+ let img = load_no_limits(input)?;
33
39
 
34
40
  let rgb = img.to_rgb8();
35
41
  let width = rgb.width();
@@ -40,8 +46,7 @@ pub fn sharp_raw_pixels(input: &[u8]) -> Result<(Vec<u8>, u32, u32), String> {
40
46
  }
41
47
 
42
48
  pub fn sharp_metadata(input: &[u8]) -> Result<(u32, u32, String), String> {
43
- let img = image::load_from_memory(input)
44
- .map_err(|e| format!("Failed to load image: {}", e))?;
49
+ let img = load_no_limits(input)?;
45
50
 
46
51
  let width = img.width();
47
52
  let height = img.height();
package/native/lib.rs CHANGED
@@ -389,6 +389,20 @@ pub fn crop_and_reconstitute(png_buffer: Buffer) -> Result<Vec<u8>> {
389
389
  .map_err(|e| Error::from_reason(e))
390
390
  }
391
391
 
392
+ #[cfg(not(test))]
393
+ #[napi]
394
+ pub fn unstretch_nn(png_buffer: Buffer) -> Result<Vec<u8>> {
395
+ reconstitution::unstretch_nn(&png_buffer)
396
+ .map_err(|e| Error::from_reason(e))
397
+ }
398
+
399
+ #[cfg(not(test))]
400
+ #[napi]
401
+ pub fn extract_payload_from_png(png_buffer: Buffer) -> Result<Vec<u8>> {
402
+ png_utils::extract_payload_from_png(&png_buffer)
403
+ .map_err(|e| Error::from_reason(e))
404
+ }
405
+
392
406
  #[cfg(not(test))]
393
407
  #[napi]
394
408
  pub fn extract_file_list_from_pixels(png_buffer: Buffer) -> Result<String> {
@@ -1,5 +1,6 @@
1
1
  use bytemuck::{Pod, Zeroable};
2
- use std::io::{Read, Seek, SeekFrom};
2
+ use image::ImageReader;
3
+ use std::io::{Cursor, Read, Seek, SeekFrom};
3
4
 
4
5
  #[repr(C)]
5
6
  #[derive(Clone, Copy, Pod, Zeroable)]
@@ -148,11 +149,25 @@ pub fn extract_payload_from_png(png_data: &[u8]) -> Result<Vec<u8>, String> {
148
149
  return Ok(payload);
149
150
  }
150
151
  }
152
+ if let Ok(unstretched) = crate::reconstitution::unstretch_nn(&reconst) {
153
+ if let Ok(payload) = extract_payload_direct(&unstretched) {
154
+ if validate_payload_deep(&payload) {
155
+ return Ok(payload);
156
+ }
157
+ }
158
+ }
151
159
  }
152
- let unstretched = crate::reconstitution::unstretch_nn(png_data)?;
153
- let payload = extract_payload_direct(&unstretched)?;
154
- if validate_payload_deep(&payload) {
155
- return Ok(payload);
160
+ if let Ok(unstretched) = crate::reconstitution::unstretch_nn(png_data) {
161
+ if let Ok(payload) = extract_payload_direct(&unstretched) {
162
+ if validate_payload_deep(&payload) {
163
+ return Ok(payload);
164
+ }
165
+ }
166
+ }
167
+ if let Ok(payload) = extract_payload_from_embedded_nn(png_data) {
168
+ if validate_payload_deep(&payload) {
169
+ return Ok(payload);
170
+ }
156
171
  }
157
172
  Err("No valid payload found after all extraction attempts".to_string())
158
173
  }
@@ -176,10 +191,199 @@ fn find_pixel_header(raw: &[u8]) -> Result<usize, String> {
176
191
  }
177
192
 
178
193
  fn decode_to_rgb(png_data: &[u8]) -> Result<Vec<u8>, String> {
179
- let img = image::load_from_memory(png_data).map_err(|e| format!("image load error: {}", e))?;
194
+ let mut reader = ImageReader::new(Cursor::new(png_data))
195
+ .with_guessed_format()
196
+ .map_err(|e| format!("format guess error: {}", e))?;
197
+ reader.no_limits();
198
+ let img = reader.decode().map_err(|e| format!("image decode error: {}", e))?;
180
199
  Ok(img.to_rgb8().into_raw())
181
200
  }
182
201
 
202
+ fn decode_to_rgba_grid(png_data: &[u8]) -> Result<(Vec<[u8; 4]>, u32, u32), String> {
203
+ let mut reader = ImageReader::new(Cursor::new(png_data))
204
+ .with_guessed_format()
205
+ .map_err(|e| format!("format guess error: {}", e))?;
206
+ reader.no_limits();
207
+ let img = reader.decode().map_err(|e| format!("image decode error: {}", e))?;
208
+ let rgba = img.to_rgba8();
209
+ let w = rgba.width();
210
+ let h = rgba.height();
211
+ let pixels: Vec<[u8; 4]> = rgba.pixels().map(|p| [p[0], p[1], p[2], p[3]]).collect();
212
+ Ok((pixels, w, h))
213
+ }
214
+
215
+ fn reconstruct_logical_pixels_from_nn(
216
+ pixels: &[[u8; 4]], width: u32, height: u32
217
+ ) -> Result<Vec<u8>, String> {
218
+ let w = width as usize;
219
+ let h = height as usize;
220
+ let get = |x: usize, y: usize| -> [u8; 4] { pixels[y * w + x] };
221
+
222
+ let magic = [b'P', b'X', b'L', b'1'];
223
+
224
+ let mut header_row = None;
225
+ let mut header_col = None;
226
+ 'outer: for y in 0..h {
227
+ for x in 0..w.saturating_sub(1) {
228
+ let p0 = get(x, y);
229
+ let p1 = get(x + 1, y);
230
+ let seq = [p0[0], p0[1], p0[2], p1[0], p1[1], p1[2]];
231
+ for start in 0..3 {
232
+ if start + 4 <= 6 && seq[start] == magic[0] && seq[start+1] == magic[1]
233
+ && seq[start+2] == magic[2] && seq[start+3] == magic[3]
234
+ {
235
+ header_row = Some(y);
236
+ header_col = Some(x);
237
+ break 'outer;
238
+ }
239
+ }
240
+ }
241
+ }
242
+ let header_row = header_row.ok_or("PXL1 not found in 2D pixel scan")?;
243
+ let header_col = header_col.ok_or("PXL1 column not found")?;
244
+
245
+ let mut scale_y = 1usize;
246
+ for dy in 1..h - header_row {
247
+ let y2 = header_row + dy;
248
+ let mut same = true;
249
+ for x in header_col..(header_col + 4).min(w) {
250
+ if get(x, y2) != get(x, header_row) { same = false; break; }
251
+ }
252
+ if same { scale_y += 1; } else { break; }
253
+ }
254
+
255
+ let cur = get(header_col, header_row);
256
+ let mut block_start = header_col;
257
+ while block_start > 0 && get(block_start - 1, header_row) == cur {
258
+ block_start -= 1;
259
+ }
260
+ let mut block_end = header_col + 1;
261
+ while block_end < w && get(block_end, header_row) == cur {
262
+ block_end += 1;
263
+ }
264
+ let scale_x = block_end - block_start;
265
+ if scale_x < 2 {
266
+ return Err("Could not determine NN scale_x".to_string());
267
+ }
268
+
269
+ let ref_y = header_row;
270
+ let mut embed_left = block_start;
271
+ loop {
272
+ if embed_left < scale_x { break; }
273
+ let candidate = embed_left - scale_x;
274
+ let c0 = get(candidate, ref_y);
275
+ let mut is_block = true;
276
+ for dx in 1..scale_x {
277
+ if candidate + dx >= w || get(candidate + dx, ref_y) != c0 {
278
+ is_block = false;
279
+ break;
280
+ }
281
+ }
282
+ if !is_block { break; }
283
+ if candidate + scale_x < w && get(candidate + scale_x, ref_y) == c0 {
284
+ break;
285
+ }
286
+ embed_left = candidate;
287
+ }
288
+
289
+ let mut embed_top = header_row;
290
+ loop {
291
+ if embed_top < scale_y { break; }
292
+ let candidate = embed_top - scale_y;
293
+ let mut is_block = true;
294
+ for dy in 0..scale_y {
295
+ if candidate + dy >= h { is_block = false; break; }
296
+ if dy > 0 && get(embed_left, candidate + dy) != get(embed_left, candidate) {
297
+ is_block = false;
298
+ break;
299
+ }
300
+ }
301
+ if !is_block { break; }
302
+ embed_top = candidate;
303
+ }
304
+
305
+ let mut logical_cols: Vec<usize> = Vec::new();
306
+ let mut x = embed_left;
307
+ while x < w {
308
+ logical_cols.push(x);
309
+ let c = get(x, ref_y);
310
+ let mut nx = x + 1;
311
+ while nx < w && get(nx, ref_y) == c {
312
+ nx += 1;
313
+ }
314
+ if nx >= w { break; }
315
+ let blk = nx - x;
316
+ if blk < scale_x.saturating_sub(2) || blk > scale_x + 2 {
317
+ break;
318
+ }
319
+ x = nx;
320
+ }
321
+
322
+ let mut logical_rows: Vec<usize> = Vec::new();
323
+ let mut y = embed_top;
324
+ while y < h {
325
+ logical_rows.push(y);
326
+ let c = get(embed_left, y);
327
+ let mut ny = y + 1;
328
+ while ny < h && get(embed_left, ny) == c {
329
+ ny += 1;
330
+ }
331
+ if ny >= h { break; }
332
+ let blk = ny - y;
333
+ if blk < scale_y.saturating_sub(2) || blk > scale_y + 2 {
334
+ break;
335
+ }
336
+ y = ny;
337
+ }
338
+
339
+ if logical_cols.len() < 3 || logical_rows.len() < 3 {
340
+ return Err("Embedded region too small".to_string());
341
+ }
342
+
343
+ let img_w = logical_cols.len();
344
+ let mut logical_rgb = Vec::with_capacity(img_w * logical_rows.len() * 3);
345
+ for &ry in &logical_rows {
346
+ for &cx in &logical_cols {
347
+ let p = get(cx, ry);
348
+ logical_rgb.push(p[0]);
349
+ logical_rgb.push(p[1]);
350
+ logical_rgb.push(p[2]);
351
+ }
352
+ }
353
+ Ok(logical_rgb)
354
+ }
355
+
356
+ fn extract_payload_from_embedded_nn(png_data: &[u8]) -> Result<Vec<u8>, String> {
357
+ let (pixels, width, height) = decode_to_rgba_grid(png_data)?;
358
+ let logical_rgb = reconstruct_logical_pixels_from_nn(&pixels, width, height)?;
359
+ let pos = {
360
+ let magic = b"PXL1";
361
+ let mut found = None;
362
+ for i in 0..logical_rgb.len().saturating_sub(4) {
363
+ if &logical_rgb[i..i+4] == magic {
364
+ found = Some(i);
365
+ break;
366
+ }
367
+ }
368
+ found.ok_or("PXL1 not found in reconstructed pixels")?
369
+ };
370
+ let mut idx = pos + 4;
371
+ if idx + 2 > logical_rgb.len() { return Err("Truncated header in embedded NN".to_string()); }
372
+ let _version = logical_rgb[idx]; idx += 1;
373
+ let name_len = logical_rgb[idx] as usize; idx += 1;
374
+ if idx + name_len > logical_rgb.len() { return Err("Truncated name in embedded NN".to_string()); }
375
+ idx += name_len;
376
+ if idx + 4 > logical_rgb.len() { return Err("Truncated payload length in embedded NN".to_string()); }
377
+ let payload_len = ((logical_rgb[idx] as u32) << 24)
378
+ | ((logical_rgb[idx+1] as u32) << 16)
379
+ | ((logical_rgb[idx+2] as u32) << 8)
380
+ | (logical_rgb[idx+3] as u32);
381
+ idx += 4;
382
+ let end = idx + (payload_len as usize);
383
+ if end > logical_rgb.len() { return Err("Truncated payload in embedded NN".to_string()); }
384
+ Ok(logical_rgb[idx..end].to_vec())
385
+ }
386
+
183
387
  pub fn extract_name_from_png(png_data: &[u8]) -> Option<String> {
184
388
  if let Some(name) = extract_name_direct(png_data) {
185
389
  return Some(name);
@@ -188,9 +392,21 @@ pub fn extract_name_from_png(png_data: &[u8]) -> Option<String> {
188
392
  if let Some(name) = extract_name_direct(&reconst) {
189
393
  return Some(name);
190
394
  }
395
+ if let Ok(unstretched) = crate::reconstitution::unstretch_nn(&reconst) {
396
+ if let Some(name) = extract_name_direct(&unstretched) {
397
+ return Some(name);
398
+ }
399
+ }
400
+ }
401
+ if let Ok(unstretched) = crate::reconstitution::unstretch_nn(png_data) {
402
+ if let Some(name) = extract_name_direct(&unstretched) {
403
+ return Some(name);
404
+ }
405
+ }
406
+ if let Ok(name) = extract_name_from_embedded_nn(png_data) {
407
+ return Some(name);
191
408
  }
192
- let unstretched = crate::reconstitution::unstretch_nn(png_data).ok()?;
193
- extract_name_direct(&unstretched)
409
+ None
194
410
  }
195
411
 
196
412
  fn extract_name_direct(png_data: &[u8]) -> Option<String> {
@@ -204,6 +420,18 @@ fn extract_name_direct(png_data: &[u8]) -> Option<String> {
204
420
  String::from_utf8(raw[idx..idx + name_len].to_vec()).ok()
205
421
  }
206
422
 
423
+ fn extract_name_from_embedded_nn(png_data: &[u8]) -> Result<String, String> {
424
+ let (pixels, width, height) = decode_to_rgba_grid(png_data)?;
425
+ let logical_rgb = reconstruct_logical_pixels_from_nn(&pixels, width, height)?;
426
+ let pos = find_pixel_header(&logical_rgb)?;
427
+ let mut idx = pos + 4;
428
+ if idx + 2 > logical_rgb.len() { return Err("Truncated".to_string()); }
429
+ idx += 1;
430
+ let name_len = logical_rgb[idx] as usize; idx += 1;
431
+ if name_len == 0 || idx + name_len > logical_rgb.len() { return Err("Truncated name".to_string()); }
432
+ String::from_utf8(logical_rgb[idx..idx + name_len].to_vec()).map_err(|e| e.to_string())
433
+ }
434
+
207
435
  fn extract_payload_direct(png_data: &[u8]) -> Result<Vec<u8>, String> {
208
436
  let raw = decode_to_rgb(png_data)?;
209
437
  let pos = find_pixel_header(&raw)?;
@@ -233,9 +461,51 @@ pub fn extract_file_list_from_pixels(png_data: &[u8]) -> Result<String, String>
233
461
  if let Ok(result) = extract_file_list_direct(&reconst) {
234
462
  return Ok(result);
235
463
  }
464
+ if let Ok(unstretched) = crate::reconstitution::unstretch_nn(&reconst) {
465
+ if let Ok(result) = extract_file_list_direct(&unstretched) {
466
+ return Ok(result);
467
+ }
468
+ }
469
+ }
470
+ if let Ok(unstretched) = crate::reconstitution::unstretch_nn(png_data) {
471
+ if let Ok(result) = extract_file_list_direct(&unstretched) {
472
+ return Ok(result);
473
+ }
474
+ }
475
+ if let Ok(result) = extract_file_list_from_embedded_nn(png_data) {
476
+ return Ok(result);
236
477
  }
237
- let unstretched = crate::reconstitution::unstretch_nn(png_data)?;
238
- extract_file_list_direct(&unstretched)
478
+ Err("No file list found after all extraction attempts".to_string())
479
+ }
480
+
481
+ fn extract_file_list_from_embedded_nn(png_data: &[u8]) -> Result<String, String> {
482
+ let (pixels, width, height) = decode_to_rgba_grid(png_data)?;
483
+ let logical_rgb = reconstruct_logical_pixels_from_nn(&pixels, width, height)?;
484
+ let pos = find_pixel_header(&logical_rgb)?;
485
+ let mut idx = pos + 4;
486
+ if idx + 2 > logical_rgb.len() { return Err("Truncated".to_string()); }
487
+ idx += 1;
488
+ let name_len = logical_rgb[idx] as usize; idx += 1;
489
+ if idx + name_len > logical_rgb.len() { return Err("Truncated".to_string()); }
490
+ idx += name_len;
491
+ if idx + 4 > logical_rgb.len() { return Err("Truncated".to_string()); }
492
+ let payload_len = ((logical_rgb[idx] as u32) << 24)
493
+ | ((logical_rgb[idx+1] as u32) << 16)
494
+ | ((logical_rgb[idx+2] as u32) << 8)
495
+ | (logical_rgb[idx+3] as u32);
496
+ idx += 4;
497
+ idx += payload_len as usize;
498
+ if idx + 8 > logical_rgb.len() { return Err("No file list in embedded NN".to_string()); }
499
+ if &logical_rgb[idx..idx + 4] != b"rXFL" { return Err("No rXFL marker in embedded NN".to_string()); }
500
+ idx += 4;
501
+ let json_len = ((logical_rgb[idx] as u32) << 24)
502
+ | ((logical_rgb[idx+1] as u32) << 16)
503
+ | ((logical_rgb[idx+2] as u32) << 8)
504
+ | (logical_rgb[idx+3] as u32);
505
+ idx += 4;
506
+ let json_end = idx + json_len as usize;
507
+ if json_end > logical_rgb.len() { return Err("Truncated file list in embedded NN".to_string()); }
508
+ String::from_utf8(logical_rgb[idx..json_end].to_vec()).map_err(|e| format!("Invalid UTF-8: {}", e))
239
509
  }
240
510
 
241
511
  fn extract_file_list_direct(png_data: &[u8]) -> Result<String, String> {
@@ -1,5 +1,14 @@
1
- use image::{Rgba, RgbaImage};
1
+ use image::{Rgba, RgbaImage, DynamicImage, ImageReader};
2
2
  use std::cmp::min;
3
+ use std::io::Cursor;
4
+
5
+ fn load_image_no_limits(data: &[u8]) -> Result<DynamicImage, String> {
6
+ let mut reader = ImageReader::new(Cursor::new(data))
7
+ .with_guessed_format()
8
+ .map_err(|e| format!("format guess error: {}", e))?;
9
+ reader.no_limits();
10
+ reader.decode().map_err(|e| format!("image decode error: {}", e))
11
+ }
3
12
 
4
13
  fn color_dist(a: [u8; 4], b: [u8; 4]) -> i32 {
5
14
  (a[0] as i32 - b[0] as i32).abs() +
@@ -77,7 +86,7 @@ fn intra_block_transitions_v(
77
86
  }
78
87
 
79
88
  pub fn crop_and_reconstitute(png_data: &[u8]) -> Result<Vec<u8>, String> {
80
- let img = image::load_from_memory(png_data).unwrap();
89
+ let img = load_image_no_limits(png_data)?;
81
90
  let rgba = img.to_rgba8();
82
91
  let width = rgba.width();
83
92
  let height = rgba.height();
@@ -411,13 +420,13 @@ pub fn crop_and_reconstitute(png_data: &[u8]) -> Result<Vec<u8>, String> {
411
420
  }
412
421
 
413
422
  let mut output = Vec::new();
414
- out.write_to(&mut std::io::Cursor::new(&mut output), image::ImageFormat::Png).unwrap();
423
+ out.write_to(&mut std::io::Cursor::new(&mut output), image::ImageFormat::Png)
424
+ .map_err(|e| format!("PNG write error: {}", e))?;
415
425
  Ok(output)
416
426
  }
417
427
 
418
428
  pub fn unstretch_nn(png_data: &[u8]) -> Result<Vec<u8>, String> {
419
- let img = image::load_from_memory(png_data)
420
- .map_err(|e| format!("image load error: {}", e))?;
429
+ let img = load_image_no_limits(png_data)?;
421
430
  let rgba = img.to_rgba8();
422
431
  let width = rgba.width() as usize;
423
432
  let height = rgba.height() as usize;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roxify",
3
- "version": "1.9.5",
3
+ "version": "1.9.7",
4
4
  "type": "module",
5
5
  "description": "Ultra-lightweight PNG steganography with native Rust acceleration. Encode binary data into PNG images with zstd compression.",
6
6
  "main": "dist/index.js",