roxify 1.9.8 → 1.10.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.
@@ -1,7 +1,35 @@
1
+ /**
2
+ * List files stored inside a ROX PNG without fully extracting it.
3
+ * Returns `null` if no file list could be found.
4
+ *
5
+ * @param pngBuf - Buffer containing a PNG file.
6
+ * @param opts - Options to include sizes.
7
+ * @returns Promise resolving to an array of file names or objects with sizes.
8
+ *
9
+ * @example
10
+ * ```js
11
+ * import { listFilesInPng } from 'roxify';
12
+ * const files = await listFilesInPng(fs.readFileSync('out.png'), { includeSizes: true });
13
+ * console.log(files);
14
+ * ```
15
+ */
1
16
  export declare function listFilesInPng(pngBuf: Buffer, opts?: {
2
17
  includeSizes?: boolean;
3
18
  }): Promise<string[] | {
4
19
  name: string;
5
20
  size: number;
6
21
  }[] | null>;
22
+ /**
23
+ * Check if a PNG contains an encrypted payload requiring a passphrase.
24
+ *
25
+ * @param pngBuf - Buffer containing a PNG file.
26
+ * @returns Promise resolving to `true` if the PNG requires a passphrase.
27
+ *
28
+ * @example
29
+ * ```js
30
+ * import { hasPassphraseInPng } from 'roxify';
31
+ * const needPass = await hasPassphraseInPng(fs.readFileSync('out.png'));
32
+ * console.log('needs passphrase?', needPass);
33
+ * ```
34
+ */
7
35
  export declare function hasPassphraseInPng(pngBuf: Buffer): Promise<boolean>;
@@ -1,47 +1,75 @@
1
1
  import * as zlib from 'zlib';
2
- import { CHUNK_TYPE, ENC_AES, ENC_XOR, MAGIC, PIXEL_MAGIC, } from './constants.js';
2
+ import { unpackBuffer } from '../pack.js';
3
+ import { CHUNK_TYPE, ENC_AES, ENC_XOR, MAGIC, MARKER_COLORS, PIXEL_MAGIC, } from './constants.js';
4
+ import { decodePngToBinary } from './decoder.js';
5
+ import { PassphraseRequiredError } from './errors.js';
3
6
  import { native } from './native.js';
4
- function parseFileList(parsedFiles) {
5
- if (parsedFiles.length > 0 &&
6
- typeof parsedFiles[0] === 'object' &&
7
- (parsedFiles[0].name || parsedFiles[0].path)) {
8
- return parsedFiles
9
- .map((p) => ({ name: p.name ?? p.path, size: typeof p.size === 'number' ? p.size : 0 }))
10
- .sort((a, b) => a.name.localeCompare(b.name));
11
- }
12
- return parsedFiles.sort();
13
- }
14
- function tryExtractFileListFromChunks(chunks) {
15
- const rxfl = chunks.find((c) => c.name === 'rXFL');
16
- if (rxfl) {
17
- return parseFileList(JSON.parse(Buffer.from(rxfl.data).toString('utf8')));
18
- }
19
- const meta = chunks.find((c) => c.name === CHUNK_TYPE);
20
- if (meta) {
21
- const dataBuf = Buffer.from(meta.data);
22
- const markerIdx = dataBuf.indexOf(Buffer.from('rXFL'));
23
- if (markerIdx !== -1 && markerIdx + 8 <= dataBuf.length) {
24
- const jsonLen = dataBuf.readUInt32BE(markerIdx + 4);
25
- const jsonEnd = markerIdx + 8 + jsonLen;
26
- if (jsonEnd <= dataBuf.length) {
27
- return parseFileList(JSON.parse(dataBuf.slice(markerIdx + 8, jsonEnd).toString('utf8')));
28
- }
29
- }
30
- }
31
- return null;
32
- }
7
+ import { cropAndReconstitute } from './reconstitution.js';
8
+ /**
9
+ * List files stored inside a ROX PNG without fully extracting it.
10
+ * Returns `null` if no file list could be found.
11
+ *
12
+ * @param pngBuf - Buffer containing a PNG file.
13
+ * @param opts - Options to include sizes.
14
+ * @returns Promise resolving to an array of file names or objects with sizes.
15
+ *
16
+ * @example
17
+ * ```js
18
+ * import { listFilesInPng } from 'roxify';
19
+ * const files = await listFilesInPng(fs.readFileSync('out.png'), { includeSizes: true });
20
+ * console.log(files);
21
+ * ```
22
+ */
33
23
  export async function listFilesInPng(pngBuf, opts = {}) {
34
24
  try {
35
25
  const chunks = native.extractPngChunks(pngBuf);
36
- const result = tryExtractFileListFromChunks(chunks);
37
- if (result)
38
- return result;
26
+ const fileListChunk = chunks.find((c) => c.name === 'rXFL');
27
+ if (fileListChunk) {
28
+ const data = Buffer.from(fileListChunk.data);
29
+ const parsedFiles = JSON.parse(data.toString('utf8'));
30
+ if (parsedFiles.length > 0 &&
31
+ typeof parsedFiles[0] === 'object' &&
32
+ (parsedFiles[0].name || parsedFiles[0].path)) {
33
+ const objs = parsedFiles.map((p) => ({
34
+ name: p.name ?? p.path,
35
+ size: typeof p.size === 'number' ? p.size : 0,
36
+ }));
37
+ return objs.sort((a, b) => a.name.localeCompare(b.name));
38
+ }
39
+ const files = parsedFiles;
40
+ return files.sort();
41
+ }
42
+ const metaChunk = chunks.find((c) => c.name === CHUNK_TYPE);
43
+ if (metaChunk) {
44
+ const dataBuf = Buffer.from(metaChunk.data);
45
+ const markerIdx = dataBuf.indexOf(Buffer.from('rXFL'));
46
+ if (markerIdx !== -1 && markerIdx + 8 <= dataBuf.length) {
47
+ const jsonLen = dataBuf.readUInt32BE(markerIdx + 4);
48
+ const jsonStart = markerIdx + 8;
49
+ const jsonEnd = jsonStart + jsonLen;
50
+ if (jsonEnd <= dataBuf.length) {
51
+ const parsedFiles = JSON.parse(dataBuf.slice(jsonStart, jsonEnd).toString('utf8'));
52
+ if (parsedFiles.length > 0 &&
53
+ typeof parsedFiles[0] === 'object' &&
54
+ (parsedFiles[0].name || parsedFiles[0].path)) {
55
+ const objs = parsedFiles.map((p) => ({
56
+ name: p.name ?? p.path,
57
+ size: typeof p.size === 'number' ? p.size : 0,
58
+ }));
59
+ return objs.sort((a, b) => a.name.localeCompare(b.name));
60
+ }
61
+ const files = parsedFiles;
62
+ return files.sort();
63
+ }
64
+ }
65
+ }
39
66
  const ihdr = chunks.find((c) => c.name === 'IHDR');
40
67
  const idatChunks = chunks.filter((c) => c.name === 'IDAT');
41
68
  if (ihdr && idatChunks.length > 0) {
42
69
  const ihdrData = Buffer.from(ihdr.data);
43
70
  const width = ihdrData.readUInt32BE(0);
44
- const rowLen = 1 + width * 3;
71
+ const bpp = 3;
72
+ const rowLen = 1 + width * bpp;
45
73
  const files = await new Promise((resolve) => {
46
74
  const inflate = zlib.createInflate();
47
75
  let buffer = Buffer.alloc(0);
@@ -70,7 +98,8 @@ export async function listFilesInPng(pngBuf, opts = {}) {
70
98
  const validClean = cleanBuffer.slice(0, cleanPtr);
71
99
  if (validClean.length < 12)
72
100
  return;
73
- if (!validClean.slice(8, 12).equals(PIXEL_MAGIC)) {
101
+ const magic = validClean.slice(8, 12);
102
+ if (!magic.equals(PIXEL_MAGIC)) {
74
103
  resolved = true;
75
104
  inflate.destroy();
76
105
  resolve(null);
@@ -87,7 +116,8 @@ export async function listFilesInPng(pngBuf, opts = {}) {
87
116
  idx += 4;
88
117
  if (validClean.length < idx + 4)
89
118
  return;
90
- if (validClean.slice(idx, idx + 4).toString('utf8') === 'rXFL') {
119
+ const marker = validClean.slice(idx, idx + 4).toString('utf8');
120
+ if (marker === 'rXFL') {
91
121
  idx += 4;
92
122
  if (validClean.length < idx + 4)
93
123
  return;
@@ -95,12 +125,27 @@ export async function listFilesInPng(pngBuf, opts = {}) {
95
125
  idx += 4;
96
126
  if (validClean.length < idx + jsonLen)
97
127
  return;
128
+ const jsonBuf = validClean.slice(idx, idx + jsonLen);
98
129
  try {
130
+ const parsedFiles = JSON.parse(jsonBuf.toString('utf8'));
99
131
  resolved = true;
100
132
  inflate.destroy();
101
- resolve(parseFileList(JSON.parse(validClean.slice(idx, idx + jsonLen).toString('utf8'))));
133
+ if (parsedFiles.length > 0 &&
134
+ typeof parsedFiles[0] === 'object' &&
135
+ (parsedFiles[0].name || parsedFiles[0].path)) {
136
+ const objs = parsedFiles.map((p) => ({
137
+ name: p.name ?? p.path,
138
+ size: typeof p.size === 'number' ? p.size : 0,
139
+ }));
140
+ resolve(objs.sort((a, b) => a.name.localeCompare(b.name)));
141
+ return;
142
+ }
143
+ const names = parsedFiles;
144
+ resolve(names.sort());
102
145
  }
103
- catch {
146
+ catch (e) {
147
+ resolved = true;
148
+ inflate.destroy();
104
149
  resolve(null);
105
150
  }
106
151
  }
@@ -110,10 +155,14 @@ export async function listFilesInPng(pngBuf, opts = {}) {
110
155
  resolve(null);
111
156
  }
112
157
  });
113
- inflate.on('error', () => { if (!resolved)
114
- resolve(null); });
115
- inflate.on('end', () => { if (!resolved)
116
- resolve(null); });
158
+ inflate.on('error', () => {
159
+ if (!resolved)
160
+ resolve(null);
161
+ });
162
+ inflate.on('end', () => {
163
+ if (!resolved)
164
+ resolve(null);
165
+ });
117
166
  for (const chunk of idatChunks) {
118
167
  if (resolved)
119
168
  break;
@@ -125,16 +174,265 @@ export async function listFilesInPng(pngBuf, opts = {}) {
125
174
  return files;
126
175
  }
127
176
  }
177
+ catch (e) {
178
+ console.log(' error:', e);
179
+ }
180
+ try {
181
+ try {
182
+ const rawData = native.sharpToRaw(pngBuf);
183
+ const data = rawData.pixels;
184
+ const currentWidth = rawData.width;
185
+ const currentHeight = rawData.height;
186
+ const rawRGB = Buffer.alloc(currentWidth * currentHeight * 3);
187
+ for (let i = 0; i < currentWidth * currentHeight; i++) {
188
+ rawRGB[i * 3] = data[i * 3];
189
+ rawRGB[i * 3 + 1] = data[i * 4 + 1];
190
+ rawRGB[i * 3 + 2] = data[i * 4 + 2];
191
+ }
192
+ const found = rawRGB.indexOf(PIXEL_MAGIC);
193
+ if (found !== -1) {
194
+ let idx = found + PIXEL_MAGIC.length;
195
+ if (idx + 2 <= rawRGB.length) {
196
+ const version = rawRGB[idx++];
197
+ const nameLen = rawRGB[idx++];
198
+ if (process.env.ROX_DEBUG)
199
+ console.log('listFilesInPng: pixel version', version, 'nameLen', nameLen);
200
+ if (nameLen > 0 && idx + nameLen <= rawRGB.length) {
201
+ idx += nameLen;
202
+ }
203
+ if (idx + 4 <= rawRGB.length) {
204
+ const payloadLen = rawRGB.readUInt32BE(idx);
205
+ idx += 4;
206
+ const afterPayload = idx + payloadLen;
207
+ if (afterPayload <= rawRGB.length) {
208
+ if (afterPayload + 8 <= rawRGB.length) {
209
+ const marker = rawRGB
210
+ .slice(afterPayload, afterPayload + 4)
211
+ .toString('utf8');
212
+ if (marker === 'rXFL') {
213
+ const jsonLen = rawRGB.readUInt32BE(afterPayload + 4);
214
+ const jsonStart = afterPayload + 8;
215
+ const jsonEnd = jsonStart + jsonLen;
216
+ if (jsonEnd <= rawRGB.length) {
217
+ const jsonBuf = rawRGB.slice(jsonStart, jsonEnd);
218
+ const parsedFiles = JSON.parse(jsonBuf.toString('utf8'));
219
+ if (parsedFiles.length > 0 &&
220
+ typeof parsedFiles[0] === 'object' &&
221
+ (parsedFiles[0].name || parsedFiles[0].path)) {
222
+ const objs = parsedFiles.map((p) => ({
223
+ name: p.name ?? p.path,
224
+ size: typeof p.size === 'number' ? p.size : 0,
225
+ }));
226
+ return objs.sort((a, b) => a.name.localeCompare(b.name));
227
+ }
228
+ const files = parsedFiles;
229
+ return files.sort();
230
+ }
231
+ }
232
+ }
233
+ }
234
+ }
235
+ }
236
+ }
237
+ }
238
+ catch (e) { }
239
+ }
240
+ catch (e) { }
241
+ try {
242
+ const reconstructed = await cropAndReconstitute(pngBuf);
243
+ try {
244
+ const rawData = native.sharpToRaw(reconstructed);
245
+ const data = rawData.pixels;
246
+ const currentWidth = rawData.width;
247
+ const currentHeight = rawData.height;
248
+ const rawRGB = Buffer.alloc(currentWidth * currentHeight * 3);
249
+ for (let i = 0; i < currentWidth * currentHeight; i++) {
250
+ rawRGB[i * 3] = data[i * 3];
251
+ rawRGB[i * 3 + 1] = data[i * 3 + 1];
252
+ rawRGB[i * 3 + 2] = data[i * 3 + 2];
253
+ }
254
+ const found = rawRGB.indexOf(PIXEL_MAGIC);
255
+ if (found !== -1) {
256
+ let idx = found + PIXEL_MAGIC.length;
257
+ if (idx + 2 <= rawRGB.length) {
258
+ const version = rawRGB[idx++];
259
+ const nameLen = rawRGB[idx++];
260
+ if (process.env.ROX_DEBUG)
261
+ console.log('listFilesInPng (reconstructed): pixel version', version, 'nameLen', nameLen);
262
+ if (nameLen > 0 && idx + nameLen <= rawRGB.length) {
263
+ idx += nameLen;
264
+ }
265
+ if (idx + 4 <= rawRGB.length) {
266
+ const payloadLen = rawRGB.readUInt32BE(idx);
267
+ idx += 4;
268
+ const afterPayload = idx + payloadLen;
269
+ if (afterPayload <= rawRGB.length) {
270
+ if (afterPayload + 8 <= rawRGB.length) {
271
+ const marker = rawRGB
272
+ .slice(afterPayload, afterPayload + 4)
273
+ .toString('utf8');
274
+ if (marker === 'rXFL') {
275
+ const jsonLen = rawRGB.readUInt32BE(afterPayload + 4);
276
+ const jsonStart = afterPayload + 8;
277
+ const jsonEnd = jsonStart + jsonLen;
278
+ if (jsonEnd <= rawRGB.length) {
279
+ const jsonBuf = rawRGB.slice(jsonStart, jsonEnd);
280
+ const parsedFiles = JSON.parse(jsonBuf.toString('utf8'));
281
+ if (parsedFiles.length > 0 &&
282
+ typeof parsedFiles[0] === 'object' &&
283
+ (parsedFiles[0].name || parsedFiles[0].path)) {
284
+ const objs = parsedFiles.map((p) => ({
285
+ name: p.name ?? p.path,
286
+ size: typeof p.size === 'number' ? p.size : 0,
287
+ }));
288
+ return objs.sort((a, b) => a.name.localeCompare(b.name));
289
+ }
290
+ const files = parsedFiles;
291
+ return files.sort();
292
+ }
293
+ }
294
+ }
295
+ }
296
+ }
297
+ }
298
+ }
299
+ }
300
+ catch (e) { }
301
+ try {
302
+ const chunks = native.extractPngChunks(reconstructed);
303
+ const fileListChunk = chunks.find((c) => c.name === 'rXFL');
304
+ if (fileListChunk) {
305
+ const data = Buffer.from(fileListChunk.data);
306
+ const parsedFiles = JSON.parse(data.toString('utf8'));
307
+ if (parsedFiles.length > 0 &&
308
+ typeof parsedFiles[0] === 'object' &&
309
+ (parsedFiles[0].name || parsedFiles[0].path)) {
310
+ const objs = parsedFiles.map((p) => ({
311
+ name: p.name ?? p.path,
312
+ size: typeof p.size === 'number' ? p.size : 0,
313
+ }));
314
+ return objs.sort((a, b) => a.name.localeCompare(b.name));
315
+ }
316
+ const files = parsedFiles;
317
+ if (opts.includeSizes) {
318
+ const sizes = await getFileSizesFromPng(pngBuf);
319
+ if (sizes) {
320
+ return files
321
+ .map((f) => ({ name: f, size: sizes[f] ?? 0 }))
322
+ .sort((a, b) => a.name.localeCompare(b.name));
323
+ }
324
+ }
325
+ return files.sort();
326
+ }
327
+ const metaChunk = chunks.find((c) => c.name === CHUNK_TYPE);
328
+ if (metaChunk) {
329
+ const dataBuf = Buffer.from(metaChunk.data);
330
+ const markerIdx = dataBuf.indexOf(Buffer.from('rXFL'));
331
+ if (markerIdx !== -1 && markerIdx + 8 <= dataBuf.length) {
332
+ const jsonLen = dataBuf.readUInt32BE(markerIdx + 4);
333
+ const jsonStart = markerIdx + 8;
334
+ const jsonEnd = jsonStart + jsonLen;
335
+ if (jsonEnd <= dataBuf.length) {
336
+ const parsedFiles = JSON.parse(dataBuf.slice(jsonStart, jsonEnd).toString('utf8'));
337
+ if (parsedFiles.length > 0 &&
338
+ typeof parsedFiles[0] === 'object' &&
339
+ (parsedFiles[0].name || parsedFiles[0].path)) {
340
+ const objs = parsedFiles.map((p) => ({
341
+ name: p.name ?? p.path,
342
+ size: typeof p.size === 'number' ? p.size : 0,
343
+ }));
344
+ return objs.sort((a, b) => a.name.localeCompare(b.name));
345
+ }
346
+ const files = parsedFiles;
347
+ return files.sort();
348
+ }
349
+ }
350
+ }
351
+ }
352
+ catch (e) { }
353
+ }
354
+ catch (e) { }
355
+ try {
356
+ const chunks = native.extractPngChunks(pngBuf);
357
+ const fileListChunk = chunks.find((c) => c.name === 'rXFL');
358
+ if (fileListChunk) {
359
+ const data = Buffer.from(fileListChunk.data);
360
+ const parsedFiles = JSON.parse(data.toString('utf8'));
361
+ if (parsedFiles.length > 0 &&
362
+ typeof parsedFiles[0] === 'object' &&
363
+ (parsedFiles[0].name || parsedFiles[0].path)) {
364
+ const objs = parsedFiles.map((p) => ({
365
+ name: p.name ?? p.path,
366
+ size: typeof p.size === 'number' ? p.size : 0,
367
+ }));
368
+ return objs.sort((a, b) => a.name.localeCompare(b.name));
369
+ }
370
+ const files = parsedFiles;
371
+ return files.sort();
372
+ }
373
+ const metaChunk = chunks.find((c) => c.name === CHUNK_TYPE);
374
+ if (metaChunk) {
375
+ const dataBuf = Buffer.from(metaChunk.data);
376
+ const markerIdx = dataBuf.indexOf(Buffer.from('rXFL'));
377
+ if (markerIdx !== -1 && markerIdx + 8 <= dataBuf.length) {
378
+ const jsonLen = dataBuf.readUInt32BE(markerIdx + 4);
379
+ const jsonStart = markerIdx + 8;
380
+ const jsonEnd = jsonStart + jsonLen;
381
+ if (jsonEnd <= dataBuf.length) {
382
+ const parsedFiles = JSON.parse(dataBuf.slice(jsonStart, jsonEnd).toString('utf8'));
383
+ if (parsedFiles.length > 0 &&
384
+ typeof parsedFiles[0] === 'object' &&
385
+ (parsedFiles[0].name || parsedFiles[0].path)) {
386
+ const objs = parsedFiles.map((p) => ({
387
+ name: p.name ?? p.path,
388
+ size: typeof p.size === 'number' ? p.size : 0,
389
+ }));
390
+ return objs.sort((a, b) => a.name.localeCompare(b.name));
391
+ }
392
+ const files = parsedFiles;
393
+ return files.sort();
394
+ }
395
+ }
396
+ }
397
+ }
128
398
  catch (e) { }
399
+ return null;
400
+ }
401
+ async function getFileSizesFromPng(pngBuf) {
129
402
  try {
130
- const json = native.extractFileListFromPixels(pngBuf);
131
- if (json) {
132
- return parseFileList(JSON.parse(json));
403
+ const res = await decodePngToBinary(pngBuf, { showProgress: false });
404
+ if (res && res.files) {
405
+ const map = {};
406
+ for (const f of res.files)
407
+ map[f.path] = f.buf.length;
408
+ return map;
409
+ }
410
+ if (res && res.buf) {
411
+ const unpack = unpackBuffer(res.buf);
412
+ if (unpack) {
413
+ const map = {};
414
+ for (const f of unpack.files)
415
+ map[f.path] = f.buf.length;
416
+ return map;
417
+ }
133
418
  }
134
419
  }
135
420
  catch (e) { }
136
421
  return null;
137
422
  }
423
+ /**
424
+ * Check if a PNG contains an encrypted payload requiring a passphrase.
425
+ *
426
+ * @param pngBuf - Buffer containing a PNG file.
427
+ * @returns Promise resolving to `true` if the PNG requires a passphrase.
428
+ *
429
+ * @example
430
+ * ```js
431
+ * import { hasPassphraseInPng } from 'roxify';
432
+ * const needPass = await hasPassphraseInPng(fs.readFileSync('out.png'));
433
+ * console.log('needs passphrase?', needPass);
434
+ * ```
435
+ */
138
436
  export async function hasPassphraseInPng(pngBuf) {
139
437
  try {
140
438
  if (pngBuf.slice(0, MAGIC.length).equals(MAGIC)) {
@@ -148,90 +446,74 @@ export async function hasPassphraseInPng(pngBuf) {
148
446
  const flag = pngBuf[offset];
149
447
  return flag === ENC_AES || flag === ENC_XOR;
150
448
  }
151
- const chunks = native.extractPngChunks(pngBuf);
152
- const target = chunks.find((c) => c.name === CHUNK_TYPE);
153
- if (target) {
154
- const data = Buffer.isBuffer(target.data) ? target.data : Buffer.from(target.data);
155
- if (data.length >= 1) {
156
- const nameLen = data.readUInt8(0);
157
- const payloadStart = 1 + nameLen;
158
- if (payloadStart < data.length) {
159
- return data[payloadStart] === ENC_AES || data[payloadStart] === ENC_XOR;
449
+ try {
450
+ const chunksRaw = native.extractPngChunks(pngBuf);
451
+ const target = chunksRaw.find((c) => c.name === CHUNK_TYPE);
452
+ if (target) {
453
+ const data = Buffer.isBuffer(target.data)
454
+ ? target.data
455
+ : Buffer.from(target.data);
456
+ if (data.length >= 1) {
457
+ const nameLen = data.readUInt8(0);
458
+ const payloadStart = 1 + nameLen;
459
+ if (payloadStart < data.length) {
460
+ const flag = data[payloadStart];
461
+ return flag === ENC_AES || flag === ENC_XOR;
462
+ }
160
463
  }
161
464
  }
162
465
  }
163
- const ihdr = chunks.find((c) => c.name === 'IHDR');
164
- const idatChunks = chunks.filter((c) => c.name === 'IDAT');
165
- if (ihdr && idatChunks.length > 0) {
166
- const ihdrData = Buffer.from(ihdr.data);
167
- const width = ihdrData.readUInt32BE(0);
168
- const rowLen = 1 + width * 3;
169
- return await new Promise((resolve) => {
170
- const inflate = zlib.createInflate();
171
- let buffer = Buffer.alloc(0);
172
- let resolved = false;
173
- inflate.on('data', (chunk) => {
174
- if (resolved)
175
- return;
176
- buffer = Buffer.concat([buffer, chunk]);
177
- const cleanBuffer = Buffer.alloc(buffer.length);
178
- let cleanPtr = 0;
179
- let ptr = 0;
180
- while (ptr < buffer.length) {
181
- const rowPos = ptr % rowLen;
182
- if (rowPos === 0) {
183
- ptr++;
184
- }
185
- else {
186
- const rem = rowLen - rowPos;
187
- const avail = buffer.length - ptr;
188
- const toCopy = Math.min(rem, avail);
189
- buffer.copy(cleanBuffer, cleanPtr, ptr, ptr + toCopy);
190
- cleanPtr += toCopy;
191
- ptr += toCopy;
192
- }
193
- }
194
- const valid = cleanBuffer.slice(0, cleanPtr);
195
- if (valid.length < 12)
196
- return;
197
- if (!valid.slice(8, 12).equals(PIXEL_MAGIC)) {
198
- resolved = true;
199
- inflate.destroy();
200
- resolve(false);
201
- return;
202
- }
203
- let idx = 12;
204
- if (valid.length < idx + 2)
205
- return;
206
- idx++;
207
- const nameLen = valid[idx++];
208
- if (valid.length < idx + nameLen + 4)
209
- return;
210
- idx += nameLen;
211
- if (valid.length < idx + 4 + 1)
212
- return;
213
- const payloadLen = valid.readUInt32BE(idx);
214
- idx += 4;
215
- if (valid.length < idx + 1)
216
- return;
217
- const flag = valid[idx];
218
- resolved = true;
219
- inflate.destroy();
220
- resolve(flag === ENC_AES || flag === ENC_XOR);
221
- });
222
- inflate.on('error', () => { if (!resolved)
223
- resolve(false); });
224
- inflate.on('end', () => { if (!resolved)
225
- resolve(false); });
226
- for (const chunk of idatChunks) {
227
- if (resolved)
466
+ catch (e) { }
467
+ try {
468
+ const rawData = native.sharpToRaw(pngBuf);
469
+ const rawRGB = Buffer.from(rawData.pixels);
470
+ const markerLen = MARKER_COLORS.length * 3;
471
+ for (let i = 0; i <= rawRGB.length - markerLen; i += 3) {
472
+ let ok = true;
473
+ for (let m = 0; m < MARKER_COLORS.length; m++) {
474
+ const j = i + m * 3;
475
+ if (rawRGB[j] !== MARKER_COLORS[m].r ||
476
+ rawRGB[j + 1] !== MARKER_COLORS[m].g ||
477
+ rawRGB[j + 2] !== MARKER_COLORS[m].b) {
478
+ ok = false;
228
479
  break;
229
- inflate.write(Buffer.from(chunk.data));
480
+ }
230
481
  }
231
- inflate.end();
232
- });
482
+ if (!ok)
483
+ continue;
484
+ const headerStart = i + markerLen;
485
+ if (headerStart + PIXEL_MAGIC.length >= rawRGB.length)
486
+ continue;
487
+ if (!rawRGB
488
+ .slice(headerStart, headerStart + PIXEL_MAGIC.length)
489
+ .equals(PIXEL_MAGIC))
490
+ continue;
491
+ const metaStart = headerStart + PIXEL_MAGIC.length;
492
+ if (metaStart + 2 >= rawRGB.length)
493
+ continue;
494
+ const nameLen = rawRGB[metaStart + 1];
495
+ const payloadLenOff = metaStart + 2 + nameLen;
496
+ const payloadStart = payloadLenOff + 4;
497
+ if (payloadStart >= rawRGB.length)
498
+ continue;
499
+ const flag = rawRGB[payloadStart];
500
+ return flag === ENC_AES || flag === ENC_XOR;
501
+ }
502
+ }
503
+ catch (e) { }
504
+ try {
505
+ await decodePngToBinary(pngBuf, { showProgress: false });
506
+ return false;
507
+ }
508
+ catch (e) {
509
+ if (e instanceof PassphraseRequiredError)
510
+ return true;
511
+ if (e.message && e.message.toLowerCase().includes('passphrase'))
512
+ return true;
513
+ return false;
233
514
  }
234
515
  }
235
- catch (e) { }
236
- return false;
516
+ catch (e) {
517
+ return false;
518
+ }
237
519
  }
@@ -72,7 +72,15 @@ function getNativeModule() {
72
72
  for (const triple of triples) {
73
73
  const name = `roxify_native-${triple}.node`;
74
74
  const libName = `libroxify_native-${triple}.node`;
75
- candidates.push(resolve(moduleDir, '..', name), resolve(moduleDir, '..', libName), resolve(root, name), resolve(root, libName), resolve(root, 'node_modules', 'roxify', name), resolve(root, 'node_modules', 'roxify', libName), resolve(moduleDir, '..', '..', name), resolve(moduleDir, '..', '..', libName));
75
+ candidates.push(
76
+ // dist/ sibling (npm-installed package)
77
+ resolve(moduleDir, '..', name), resolve(moduleDir, '..', libName),
78
+ // package root
79
+ resolve(root, name), resolve(root, libName),
80
+ // node_modules/roxify/
81
+ resolve(root, 'node_modules', 'roxify', name), resolve(root, 'node_modules', 'roxify', libName),
82
+ // two levels up (global npm install)
83
+ resolve(moduleDir, '..', '..', name), resolve(moduleDir, '..', '..', libName));
76
84
  }
77
85
  // --- 2. Build output candidates (local dev) ---
78
86
  for (const triple of triples) {