roxify 1.9.6 → 1.9.8

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.6"
3
+ version = "1.9.8"
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
  }
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> {
@@ -149,11 +149,25 @@ pub fn extract_payload_from_png(png_data: &[u8]) -> Result<Vec<u8>, String> {
149
149
  return Ok(payload);
150
150
  }
151
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
+ }
159
+ }
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
+ }
152
166
  }
153
- let unstretched = crate::reconstitution::unstretch_nn(png_data)?;
154
- let payload = extract_payload_direct(&unstretched)?;
155
- if validate_payload_deep(&payload) {
156
- return Ok(payload);
167
+ if let Ok(payload) = extract_payload_from_embedded_nn(png_data) {
168
+ if validate_payload_deep(&payload) {
169
+ return Ok(payload);
170
+ }
157
171
  }
158
172
  Err("No valid payload found after all extraction attempts".to_string())
159
173
  }
@@ -185,6 +199,191 @@ fn decode_to_rgb(png_data: &[u8]) -> Result<Vec<u8>, String> {
185
199
  Ok(img.to_rgb8().into_raw())
186
200
  }
187
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
+
188
387
  pub fn extract_name_from_png(png_data: &[u8]) -> Option<String> {
189
388
  if let Some(name) = extract_name_direct(png_data) {
190
389
  return Some(name);
@@ -193,9 +392,21 @@ pub fn extract_name_from_png(png_data: &[u8]) -> Option<String> {
193
392
  if let Some(name) = extract_name_direct(&reconst) {
194
393
  return Some(name);
195
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
+ }
196
400
  }
197
- let unstretched = crate::reconstitution::unstretch_nn(png_data).ok()?;
198
- extract_name_direct(&unstretched)
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);
408
+ }
409
+ None
199
410
  }
200
411
 
201
412
  fn extract_name_direct(png_data: &[u8]) -> Option<String> {
@@ -209,6 +420,18 @@ fn extract_name_direct(png_data: &[u8]) -> Option<String> {
209
420
  String::from_utf8(raw[idx..idx + name_len].to_vec()).ok()
210
421
  }
211
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
+
212
435
  fn extract_payload_direct(png_data: &[u8]) -> Result<Vec<u8>, String> {
213
436
  let raw = decode_to_rgb(png_data)?;
214
437
  let pos = find_pixel_header(&raw)?;
@@ -238,9 +461,51 @@ pub fn extract_file_list_from_pixels(png_data: &[u8]) -> Result<String, String>
238
461
  if let Ok(result) = extract_file_list_direct(&reconst) {
239
462
  return Ok(result);
240
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);
241
477
  }
242
- let unstretched = crate::reconstitution::unstretch_nn(png_data)?;
243
- 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))
244
509
  }
245
510
 
246
511
  fn extract_file_list_direct(png_data: &[u8]) -> Result<String, String> {
@@ -349,7 +349,7 @@ pub fn crop_and_reconstitute(png_data: &[u8]) -> Result<Vec<u8>, String> {
349
349
  // Filtre : la zone encodée NN a des blocs monochromes → intra très bas
350
350
  // Zone de fond aléatoire → intra ≈ lw × n_scan × 0.99 >> 0
351
351
  // Seuil : lw/8 × n_scan pour permettre ≈ lw/8 pixels parasites
352
- let intra_threshold = ((lw as u32 / 4 + 2) * n_scan).max(n_scan * 3);
352
+ let intra_threshold = ((lw as u32 / 2 + 3) * n_scan).max(n_scan * 3);
353
353
 
354
354
  if intra_final > intra_threshold {
355
355
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roxify",
3
- "version": "1.9.6",
3
+ "version": "1.9.8",
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",