roxify 1.7.1 → 1.7.3
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/constants.d.ts +6 -6
- package/dist/utils/inspection.d.ts +0 -28
- package/dist/utils/inspection.js +121 -410
- package/dist/utils/native.js +10 -4
- package/package.json +4 -3
- package/roxify_native.node +0 -0
- package/roxify_native-x86_64-unknown-linux-gnu.node +0 -0
package/dist/cli.js
CHANGED
|
Binary file
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export declare const CHUNK_TYPE = "rXDT";
|
|
2
|
-
export declare const MAGIC:
|
|
3
|
-
export declare const PIXEL_MAGIC:
|
|
4
|
-
export declare const PIXEL_MAGIC_BLOCK:
|
|
2
|
+
export declare const MAGIC: any;
|
|
3
|
+
export declare const PIXEL_MAGIC: any;
|
|
4
|
+
export declare const PIXEL_MAGIC_BLOCK: any;
|
|
5
5
|
export declare const ENC_NONE = 0;
|
|
6
6
|
export declare const ENC_AES = 1;
|
|
7
7
|
export declare const ENC_XOR = 2;
|
|
8
|
-
export declare const FILTER_ZERO:
|
|
9
|
-
export declare const PNG_HEADER:
|
|
10
|
-
export declare const PNG_HEADER_HEX:
|
|
8
|
+
export declare const FILTER_ZERO: any;
|
|
9
|
+
export declare const PNG_HEADER: any;
|
|
10
|
+
export declare const PNG_HEADER_HEX: any;
|
|
11
11
|
export declare const MARKER_COLORS: {
|
|
12
12
|
r: number;
|
|
13
13
|
g: number;
|
|
@@ -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/dist/utils/native.js
CHANGED
|
@@ -99,10 +99,16 @@ function getNativeModule() {
|
|
|
99
99
|
candidates.push(resolve(root, 'target', profile, 'roxify_native.node'));
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
// --- 3. Generic fallback names
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
102
|
+
// --- 3. Generic fallback names ---
|
|
103
|
+
// ONLY used when a platform-specific triple file also exists next to it,
|
|
104
|
+
// or when we are on the SAME platform that built the generic file (dev mode).
|
|
105
|
+
// In production (npm install), the platform-specific files MUST exist.
|
|
106
|
+
// We do NOT blindly load roxify_native.node because it could be a Linux
|
|
107
|
+
// binary loaded on Windows (or vice-versa), causing ERR_DLOPEN_FAILED.
|
|
108
|
+
//
|
|
109
|
+
// Generic names are ONLY safe in local dev (where you just built for your
|
|
110
|
+
// own platform). We keep them but ONLY for target/release/ build outputs.
|
|
111
|
+
// The root-level roxify_native.node is intentionally excluded.
|
|
106
112
|
// Deduplicate
|
|
107
113
|
const seen = new Set();
|
|
108
114
|
const unique = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roxify",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.3",
|
|
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"
|
package/roxify_native.node
CHANGED
|
Binary file
|
|
Binary file
|