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 +1 -1
- package/dist/roxify_native.exe +0 -0
- package/dist/utils/decoder.js +28 -0
- package/native/lib.rs +14 -0
- package/native/png_utils.rs +273 -8
- package/native/reconstitution.rs +1 -1
- package/package.json +1 -1
- package/roxify_native-x86_64-pc-windows-msvc.node +0 -0
package/Cargo.toml
CHANGED
package/dist/roxify_native.exe
CHANGED
|
Binary file
|
package/dist/utils/decoder.js
CHANGED
|
@@ -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> {
|
package/native/png_utils.rs
CHANGED
|
@@ -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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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)
|
|
198
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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> {
|
package/native/reconstitution.rs
CHANGED
|
@@ -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 /
|
|
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
|
Binary file
|