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