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.
- package/CHANGELOG.md +36 -0
- package/LICENSE +21 -21
- package/README.md +370 -117
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +606 -606
- package/dist/index.d.ts +13 -13
- package/dist/index.js +13 -13
- package/dist/minpng.d.ts +8 -8
- package/dist/minpng.js +238 -238
- package/dist/pack.d.ts +31 -31
- package/dist/pack.js +188 -188
- package/dist/utils/constants.d.ts +38 -38
- package/dist/utils/constants.js +22 -22
- package/dist/utils/crc.d.ts +4 -4
- package/dist/utils/crc.js +29 -29
- package/dist/utils/decoder.d.ts +4 -4
- package/dist/utils/decoder.js +665 -665
- package/dist/utils/encoder.d.ts +4 -4
- package/dist/utils/encoder.js +422 -422
- package/dist/utils/errors.d.ts +9 -9
- package/dist/utils/errors.js +18 -18
- package/dist/utils/helpers.d.ts +11 -11
- package/dist/utils/helpers.js +112 -76
- package/dist/utils/inspection.d.ts +19 -19
- package/dist/utils/inspection.js +518 -518
- package/dist/utils/optimization.d.ts +3 -3
- package/dist/utils/optimization.js +640 -640
- package/dist/utils/reconstitution.d.ts +3 -3
- package/dist/utils/reconstitution.js +266 -266
- package/dist/utils/types.d.ts +44 -44
- package/dist/utils/types.js +1 -1
- package/dist/utils/zstd.d.ts +17 -17
- package/dist/utils/zstd.js +153 -131
- package/libroxify_native.node +0 -0
- package/package.json +47 -65
package/dist/utils/inspection.js
CHANGED
|
@@ -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
|
+
}
|