roxify 1.7.2 → 1.7.4
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/dist/cli.js +1 -1
- package/dist/roxify_native.exe +0 -0
- package/dist/utils/inspection.d.ts +0 -28
- package/dist/utils/inspection.js +121 -410
- package/package.json +5 -4
- package/roxify_native.node +0 -0
- package/roxify_native-x86_64-pc-windows-gnu.node +0 -0
- package/roxify_native-x86_64-pc-windows-msvc.node +0 -0
- package/roxify_native-x86_64-unknown-linux-gnu.node +0 -0
package/dist/cli.js
CHANGED
|
Binary file
|
|
@@ -1,35 +1,7 @@
|
|
|
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
|
-
*/
|
|
16
1
|
export declare function listFilesInPng(pngBuf: Buffer, opts?: {
|
|
17
2
|
includeSizes?: boolean;
|
|
18
3
|
}): Promise<string[] | {
|
|
19
4
|
name: string;
|
|
20
5
|
size: number;
|
|
21
6
|
}[] | 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
|
-
*/
|
|
35
7
|
export declare function hasPassphraseInPng(pngBuf: Buffer): Promise<boolean>;
|
package/dist/utils/inspection.js
CHANGED
|
@@ -1,75 +1,47 @@
|
|
|
1
1
|
import * as zlib from 'zlib';
|
|
2
|
-
import {
|
|
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';
|
|
2
|
+
import { CHUNK_TYPE, ENC_AES, ENC_XOR, MAGIC, PIXEL_MAGIC, } from './constants.js';
|
|
6
3
|
import { native } from './native.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
}
|
|
23
33
|
export async function listFilesInPng(pngBuf, opts = {}) {
|
|
24
34
|
try {
|
|
25
35
|
const chunks = native.extractPngChunks(pngBuf);
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
|
|
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
|
-
}
|
|
36
|
+
const result = tryExtractFileListFromChunks(chunks);
|
|
37
|
+
if (result)
|
|
38
|
+
return result;
|
|
66
39
|
const ihdr = chunks.find((c) => c.name === 'IHDR');
|
|
67
40
|
const idatChunks = chunks.filter((c) => c.name === 'IDAT');
|
|
68
41
|
if (ihdr && idatChunks.length > 0) {
|
|
69
42
|
const ihdrData = Buffer.from(ihdr.data);
|
|
70
43
|
const width = ihdrData.readUInt32BE(0);
|
|
71
|
-
const
|
|
72
|
-
const rowLen = 1 + width * bpp;
|
|
44
|
+
const rowLen = 1 + width * 3;
|
|
73
45
|
const files = await new Promise((resolve) => {
|
|
74
46
|
const inflate = zlib.createInflate();
|
|
75
47
|
let buffer = Buffer.alloc(0);
|
|
@@ -98,8 +70,7 @@ export async function listFilesInPng(pngBuf, opts = {}) {
|
|
|
98
70
|
const validClean = cleanBuffer.slice(0, cleanPtr);
|
|
99
71
|
if (validClean.length < 12)
|
|
100
72
|
return;
|
|
101
|
-
|
|
102
|
-
if (!magic.equals(PIXEL_MAGIC)) {
|
|
73
|
+
if (!validClean.slice(8, 12).equals(PIXEL_MAGIC)) {
|
|
103
74
|
resolved = true;
|
|
104
75
|
inflate.destroy();
|
|
105
76
|
resolve(null);
|
|
@@ -116,8 +87,7 @@ export async function listFilesInPng(pngBuf, opts = {}) {
|
|
|
116
87
|
idx += 4;
|
|
117
88
|
if (validClean.length < idx + 4)
|
|
118
89
|
return;
|
|
119
|
-
|
|
120
|
-
if (marker === 'rXFL') {
|
|
90
|
+
if (validClean.slice(idx, idx + 4).toString('utf8') === 'rXFL') {
|
|
121
91
|
idx += 4;
|
|
122
92
|
if (validClean.length < idx + 4)
|
|
123
93
|
return;
|
|
@@ -125,27 +95,12 @@ export async function listFilesInPng(pngBuf, opts = {}) {
|
|
|
125
95
|
idx += 4;
|
|
126
96
|
if (validClean.length < idx + jsonLen)
|
|
127
97
|
return;
|
|
128
|
-
const jsonBuf = validClean.slice(idx, idx + jsonLen);
|
|
129
98
|
try {
|
|
130
|
-
const parsedFiles = JSON.parse(jsonBuf.toString('utf8'));
|
|
131
99
|
resolved = true;
|
|
132
100
|
inflate.destroy();
|
|
133
|
-
|
|
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());
|
|
101
|
+
resolve(parseFileList(JSON.parse(validClean.slice(idx, idx + jsonLen).toString('utf8'))));
|
|
145
102
|
}
|
|
146
|
-
catch
|
|
147
|
-
resolved = true;
|
|
148
|
-
inflate.destroy();
|
|
103
|
+
catch {
|
|
149
104
|
resolve(null);
|
|
150
105
|
}
|
|
151
106
|
}
|
|
@@ -155,14 +110,10 @@ export async function listFilesInPng(pngBuf, opts = {}) {
|
|
|
155
110
|
resolve(null);
|
|
156
111
|
}
|
|
157
112
|
});
|
|
158
|
-
inflate.on('error', () => {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
inflate.on('end', () => {
|
|
163
|
-
if (!resolved)
|
|
164
|
-
resolve(null);
|
|
165
|
-
});
|
|
113
|
+
inflate.on('error', () => { if (!resolved)
|
|
114
|
+
resolve(null); });
|
|
115
|
+
inflate.on('end', () => { if (!resolved)
|
|
116
|
+
resolve(null); });
|
|
166
117
|
for (const chunk of idatChunks) {
|
|
167
118
|
if (resolved)
|
|
168
119
|
break;
|
|
@@ -174,265 +125,9 @@ export async function listFilesInPng(pngBuf, opts = {}) {
|
|
|
174
125
|
return files;
|
|
175
126
|
}
|
|
176
127
|
}
|
|
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
|
-
}
|
|
398
128
|
catch (e) { }
|
|
399
129
|
return null;
|
|
400
130
|
}
|
|
401
|
-
async function getFileSizesFromPng(pngBuf) {
|
|
402
|
-
try {
|
|
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
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
catch (e) { }
|
|
421
|
-
return null;
|
|
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
|
-
*/
|
|
436
131
|
export async function hasPassphraseInPng(pngBuf) {
|
|
437
132
|
try {
|
|
438
133
|
if (pngBuf.slice(0, MAGIC.length).equals(MAGIC)) {
|
|
@@ -446,74 +141,90 @@ export async function hasPassphraseInPng(pngBuf) {
|
|
|
446
141
|
const flag = pngBuf[offset];
|
|
447
142
|
return flag === ENC_AES || flag === ENC_XOR;
|
|
448
143
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (data.length
|
|
457
|
-
|
|
458
|
-
const payloadStart = 1 + nameLen;
|
|
459
|
-
if (payloadStart < data.length) {
|
|
460
|
-
const flag = data[payloadStart];
|
|
461
|
-
return flag === ENC_AES || flag === ENC_XOR;
|
|
462
|
-
}
|
|
144
|
+
const chunks = native.extractPngChunks(pngBuf);
|
|
145
|
+
const target = chunks.find((c) => c.name === CHUNK_TYPE);
|
|
146
|
+
if (target) {
|
|
147
|
+
const data = Buffer.isBuffer(target.data) ? target.data : Buffer.from(target.data);
|
|
148
|
+
if (data.length >= 1) {
|
|
149
|
+
const nameLen = data.readUInt8(0);
|
|
150
|
+
const payloadStart = 1 + nameLen;
|
|
151
|
+
if (payloadStart < data.length) {
|
|
152
|
+
return data[payloadStart] === ENC_AES || data[payloadStart] === ENC_XOR;
|
|
463
153
|
}
|
|
464
154
|
}
|
|
465
155
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
156
|
+
const ihdr = chunks.find((c) => c.name === 'IHDR');
|
|
157
|
+
const idatChunks = chunks.filter((c) => c.name === 'IDAT');
|
|
158
|
+
if (ihdr && idatChunks.length > 0) {
|
|
159
|
+
const ihdrData = Buffer.from(ihdr.data);
|
|
160
|
+
const width = ihdrData.readUInt32BE(0);
|
|
161
|
+
const rowLen = 1 + width * 3;
|
|
162
|
+
return await new Promise((resolve) => {
|
|
163
|
+
const inflate = zlib.createInflate();
|
|
164
|
+
let buffer = Buffer.alloc(0);
|
|
165
|
+
let resolved = false;
|
|
166
|
+
inflate.on('data', (chunk) => {
|
|
167
|
+
if (resolved)
|
|
168
|
+
return;
|
|
169
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
170
|
+
const cleanBuffer = Buffer.alloc(buffer.length);
|
|
171
|
+
let cleanPtr = 0;
|
|
172
|
+
let ptr = 0;
|
|
173
|
+
while (ptr < buffer.length) {
|
|
174
|
+
const rowPos = ptr % rowLen;
|
|
175
|
+
if (rowPos === 0) {
|
|
176
|
+
ptr++;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
const rem = rowLen - rowPos;
|
|
180
|
+
const avail = buffer.length - ptr;
|
|
181
|
+
const toCopy = Math.min(rem, avail);
|
|
182
|
+
buffer.copy(cleanBuffer, cleanPtr, ptr, ptr + toCopy);
|
|
183
|
+
cleanPtr += toCopy;
|
|
184
|
+
ptr += toCopy;
|
|
185
|
+
}
|
|
480
186
|
}
|
|
187
|
+
const valid = cleanBuffer.slice(0, cleanPtr);
|
|
188
|
+
if (valid.length < 12)
|
|
189
|
+
return;
|
|
190
|
+
if (!valid.slice(8, 12).equals(PIXEL_MAGIC)) {
|
|
191
|
+
resolved = true;
|
|
192
|
+
inflate.destroy();
|
|
193
|
+
resolve(false);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
let idx = 12;
|
|
197
|
+
if (valid.length < idx + 2)
|
|
198
|
+
return;
|
|
199
|
+
idx++;
|
|
200
|
+
const nameLen = valid[idx++];
|
|
201
|
+
if (valid.length < idx + nameLen + 4)
|
|
202
|
+
return;
|
|
203
|
+
idx += nameLen;
|
|
204
|
+
if (valid.length < idx + 4 + 1)
|
|
205
|
+
return;
|
|
206
|
+
const payloadLen = valid.readUInt32BE(idx);
|
|
207
|
+
idx += 4;
|
|
208
|
+
if (valid.length < idx + 1)
|
|
209
|
+
return;
|
|
210
|
+
const flag = valid[idx];
|
|
211
|
+
resolved = true;
|
|
212
|
+
inflate.destroy();
|
|
213
|
+
resolve(flag === ENC_AES || flag === ENC_XOR);
|
|
214
|
+
});
|
|
215
|
+
inflate.on('error', () => { if (!resolved)
|
|
216
|
+
resolve(false); });
|
|
217
|
+
inflate.on('end', () => { if (!resolved)
|
|
218
|
+
resolve(false); });
|
|
219
|
+
for (const chunk of idatChunks) {
|
|
220
|
+
if (resolved)
|
|
221
|
+
break;
|
|
222
|
+
inflate.write(Buffer.from(chunk.data));
|
|
481
223
|
}
|
|
482
|
-
|
|
483
|
-
|
|
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;
|
|
224
|
+
inflate.end();
|
|
225
|
+
});
|
|
514
226
|
}
|
|
515
227
|
}
|
|
516
|
-
catch (e) {
|
|
517
|
-
|
|
518
|
-
}
|
|
228
|
+
catch (e) { }
|
|
229
|
+
return false;
|
|
519
230
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roxify",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ultra-lightweight PNG steganography with native Rust acceleration. Encode binary data into PNG images with zstd compression.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"libroxify_native-aarch64-unknown-linux-gnu.node",
|
|
28
28
|
"libroxify_native-x86_64-apple-darwin.node",
|
|
29
29
|
"libroxify_native-aarch64-apple-darwin.node",
|
|
30
|
+
"dist/roxify_native.exe",
|
|
30
31
|
"README.md",
|
|
31
32
|
"LICENSE"
|
|
32
33
|
],
|
|
@@ -57,8 +58,8 @@
|
|
|
57
58
|
"publish:npm": "npm run package:prepare && echo 'Run npm publish --access public'",
|
|
58
59
|
"release:flow": "node scripts/release-flow.cjs",
|
|
59
60
|
"release:flow:auto": "AUTO_PUBLISH=1 node scripts/release-flow.cjs",
|
|
60
|
-
"
|
|
61
|
-
"prepublishOnly": "
|
|
61
|
+
"release:full": "node scripts/publish.cjs",
|
|
62
|
+
"prepublishOnly": "npx -p typescript tsc || echo 'TS build skipped'",
|
|
62
63
|
"test": "npm run build && node ./test/run-all-tests.cjs",
|
|
63
64
|
"test:integration": "node scripts/run-integration-tests.cjs",
|
|
64
65
|
"cli": "node dist/cli.js"
|
|
@@ -101,4 +102,4 @@
|
|
|
101
102
|
"engines": {
|
|
102
103
|
"node": ">=18.0.0"
|
|
103
104
|
}
|
|
104
|
-
}
|
|
105
|
+
}
|
package/roxify_native.node
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|