roxify 1.2.4 → 1.2.6
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 +86 -63
- package/dist/index.d.ts +11 -132
- package/dist/index.js +11 -2639
- package/dist/minpng.js +31 -24
- package/dist/pack.d.ts +11 -10
- package/dist/pack.js +104 -62
- package/dist/utils/constants.d.ts +38 -0
- package/dist/utils/constants.js +22 -0
- package/dist/utils/crc.d.ts +4 -0
- package/dist/utils/crc.js +29 -0
- package/dist/utils/decoder.d.ts +4 -0
- package/dist/utils/decoder.js +626 -0
- package/dist/utils/encoder.d.ts +4 -0
- package/dist/utils/encoder.js +348 -0
- package/dist/utils/errors.d.ts +9 -0
- package/dist/utils/errors.js +18 -0
- package/dist/utils/helpers.d.ts +11 -0
- package/dist/utils/helpers.js +76 -0
- package/dist/utils/inspection.d.ts +19 -0
- package/dist/utils/inspection.js +518 -0
- package/dist/utils/optimization.d.ts +3 -0
- package/dist/utils/optimization.js +636 -0
- package/dist/utils/reconstitution.d.ts +3 -0
- package/dist/utils/reconstitution.js +266 -0
- package/dist/utils/types.d.ts +44 -0
- package/dist/utils/types.js +1 -0
- package/dist/utils/zstd.d.ts +17 -0
- package/dist/utils/zstd.js +118 -0
- package/package.json +1 -1
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import cliProgress from 'cli-progress';
|
|
2
|
+
import { createCipheriv, pbkdf2Sync, randomBytes } from 'crypto';
|
|
3
|
+
import encode from 'png-chunks-encode';
|
|
4
|
+
import sharp from 'sharp';
|
|
5
|
+
import * as zlib from 'zlib';
|
|
6
|
+
import { unpackBuffer } from '../pack.js';
|
|
7
|
+
import { COMPRESSION_MARKERS, ENC_AES, ENC_NONE, ENC_XOR, MAGIC, MARKER_END, MARKER_START, PIXEL_MAGIC, PNG_HEADER, PNG_HEADER_HEX, } from './constants.js';
|
|
8
|
+
import { applyXor, colorsToBytes } from './helpers.js';
|
|
9
|
+
import { optimizePngBuffer } from './optimization.js';
|
|
10
|
+
import { parallelZstdCompress } from './zstd.js';
|
|
11
|
+
export async function encodeBinaryToPng(input, opts = {}) {
|
|
12
|
+
let progressBar = null;
|
|
13
|
+
if (opts.showProgress) {
|
|
14
|
+
progressBar = new cliProgress.SingleBar({
|
|
15
|
+
format: ' {bar} {percentage}% | {step} | {elapsed}s',
|
|
16
|
+
}, cliProgress.Presets.shades_classic);
|
|
17
|
+
progressBar.start(100, 0, { step: 'Starting', elapsed: '0' });
|
|
18
|
+
const startTime = Date.now();
|
|
19
|
+
if (!opts.onProgress) {
|
|
20
|
+
opts.onProgress = (info) => {
|
|
21
|
+
let pct = 0;
|
|
22
|
+
if (info.phase === 'compress_progress' && info.loaded && info.total) {
|
|
23
|
+
pct = (info.loaded / info.total) * 50;
|
|
24
|
+
}
|
|
25
|
+
else if (info.phase === 'compress_done') {
|
|
26
|
+
pct = 50;
|
|
27
|
+
}
|
|
28
|
+
else if (info.phase === 'encrypt_done') {
|
|
29
|
+
pct = 80;
|
|
30
|
+
}
|
|
31
|
+
else if (info.phase === 'png_gen') {
|
|
32
|
+
pct = 90;
|
|
33
|
+
}
|
|
34
|
+
else if (info.phase === 'done') {
|
|
35
|
+
pct = 100;
|
|
36
|
+
}
|
|
37
|
+
progressBar.update(Math.floor(pct), {
|
|
38
|
+
step: info.phase.replace('_', ' '),
|
|
39
|
+
elapsed: String(Math.floor((Date.now() - startTime) / 1000)),
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
let payload = Buffer.concat([MAGIC, input]);
|
|
45
|
+
if (opts.onProgress)
|
|
46
|
+
opts.onProgress({ phase: 'compress_start', total: payload.length });
|
|
47
|
+
const deltaEncoded = payload;
|
|
48
|
+
payload = await parallelZstdCompress(deltaEncoded, 19, (loaded, total) => {
|
|
49
|
+
if (opts.onProgress) {
|
|
50
|
+
opts.onProgress({
|
|
51
|
+
phase: 'compress_progress',
|
|
52
|
+
loaded,
|
|
53
|
+
total,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (opts.onProgress)
|
|
58
|
+
opts.onProgress({ phase: 'compress_done', loaded: payload.length });
|
|
59
|
+
if (opts.passphrase && !opts.encrypt) {
|
|
60
|
+
opts.encrypt = 'aes';
|
|
61
|
+
}
|
|
62
|
+
if (opts.encrypt === 'auto' && !opts._skipAuto) {
|
|
63
|
+
const candidates = ['none', 'xor', 'aes'];
|
|
64
|
+
const candidateBufs = [];
|
|
65
|
+
for (const c of candidates) {
|
|
66
|
+
const testBuf = await encodeBinaryToPng(input, {
|
|
67
|
+
...opts,
|
|
68
|
+
encrypt: c,
|
|
69
|
+
_skipAuto: true,
|
|
70
|
+
});
|
|
71
|
+
candidateBufs.push({ enc: c, buf: testBuf });
|
|
72
|
+
}
|
|
73
|
+
candidateBufs.sort((a, b) => a.buf.length - b.buf.length);
|
|
74
|
+
return candidateBufs[0].buf;
|
|
75
|
+
}
|
|
76
|
+
if (opts.passphrase && opts.encrypt && opts.encrypt !== 'auto') {
|
|
77
|
+
const encChoice = opts.encrypt;
|
|
78
|
+
if (opts.onProgress)
|
|
79
|
+
opts.onProgress({ phase: 'encrypt_start' });
|
|
80
|
+
if (encChoice === 'aes') {
|
|
81
|
+
const salt = randomBytes(16);
|
|
82
|
+
const iv = randomBytes(12);
|
|
83
|
+
const PBKDF2_ITERS = 1000000;
|
|
84
|
+
const key = pbkdf2Sync(opts.passphrase, salt, PBKDF2_ITERS, 32, 'sha256');
|
|
85
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
86
|
+
const enc = Buffer.concat([cipher.update(payload), cipher.final()]);
|
|
87
|
+
const tag = cipher.getAuthTag();
|
|
88
|
+
payload = Buffer.concat([Buffer.from([ENC_AES]), salt, iv, tag, enc]);
|
|
89
|
+
if (opts.onProgress)
|
|
90
|
+
opts.onProgress({ phase: 'encrypt_done' });
|
|
91
|
+
}
|
|
92
|
+
else if (encChoice === 'xor') {
|
|
93
|
+
const xored = applyXor(payload, opts.passphrase);
|
|
94
|
+
payload = Buffer.concat([Buffer.from([ENC_XOR]), xored]);
|
|
95
|
+
if (opts.onProgress)
|
|
96
|
+
opts.onProgress({ phase: 'encrypt_done' });
|
|
97
|
+
}
|
|
98
|
+
else if (encChoice === 'none') {
|
|
99
|
+
payload = Buffer.concat([Buffer.from([ENC_NONE]), payload]);
|
|
100
|
+
if (opts.onProgress)
|
|
101
|
+
opts.onProgress({ phase: 'encrypt_done' });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
payload = Buffer.concat([Buffer.from([ENC_NONE]), payload]);
|
|
106
|
+
}
|
|
107
|
+
if (opts.onProgress)
|
|
108
|
+
opts.onProgress({ phase: 'meta_prep_done', loaded: payload.length });
|
|
109
|
+
const metaParts = [];
|
|
110
|
+
const includeName = opts.includeName === undefined ? true : !!opts.includeName;
|
|
111
|
+
if (includeName && opts.name) {
|
|
112
|
+
const nameBuf = Buffer.from(opts.name, 'utf8');
|
|
113
|
+
metaParts.push(Buffer.from([nameBuf.length]));
|
|
114
|
+
metaParts.push(nameBuf);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
metaParts.push(Buffer.from([0]));
|
|
118
|
+
}
|
|
119
|
+
metaParts.push(payload);
|
|
120
|
+
let meta = Buffer.concat(metaParts);
|
|
121
|
+
if (opts.includeFileList && opts.fileList) {
|
|
122
|
+
let sizeMap = null;
|
|
123
|
+
try {
|
|
124
|
+
const unpack = unpackBuffer(input);
|
|
125
|
+
if (unpack) {
|
|
126
|
+
sizeMap = {};
|
|
127
|
+
for (const ef of unpack.files)
|
|
128
|
+
sizeMap[ef.path] = ef.buf.length;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (e) { }
|
|
132
|
+
const normalized = opts.fileList.map((f) => {
|
|
133
|
+
if (typeof f === 'string')
|
|
134
|
+
return { name: f, size: sizeMap && sizeMap[f] ? sizeMap[f] : 0 };
|
|
135
|
+
if (f && typeof f === 'object') {
|
|
136
|
+
if (f.name)
|
|
137
|
+
return { name: f.name, size: f.size ?? 0 };
|
|
138
|
+
if (f.path)
|
|
139
|
+
return { name: f.path, size: f.size ?? 0 };
|
|
140
|
+
}
|
|
141
|
+
return { name: String(f), size: 0 };
|
|
142
|
+
});
|
|
143
|
+
const jsonBuf = Buffer.from(JSON.stringify(normalized), 'utf8');
|
|
144
|
+
const lenBuf = Buffer.alloc(4);
|
|
145
|
+
lenBuf.writeUInt32BE(jsonBuf.length, 0);
|
|
146
|
+
meta = Buffer.concat([meta, Buffer.from('rXFL', 'utf8'), lenBuf, jsonBuf]);
|
|
147
|
+
}
|
|
148
|
+
if (opts.output === 'rox') {
|
|
149
|
+
return Buffer.concat([MAGIC, meta]);
|
|
150
|
+
}
|
|
151
|
+
{
|
|
152
|
+
const nameBuf = opts.name
|
|
153
|
+
? Buffer.from(opts.name, 'utf8')
|
|
154
|
+
: Buffer.alloc(0);
|
|
155
|
+
const nameLen = nameBuf.length;
|
|
156
|
+
const payloadLenBuf = Buffer.alloc(4);
|
|
157
|
+
payloadLenBuf.writeUInt32BE(payload.length, 0);
|
|
158
|
+
const version = 1;
|
|
159
|
+
let metaPixel = Buffer.concat([
|
|
160
|
+
Buffer.from([version]),
|
|
161
|
+
Buffer.from([nameLen]),
|
|
162
|
+
nameBuf,
|
|
163
|
+
payloadLenBuf,
|
|
164
|
+
payload,
|
|
165
|
+
]);
|
|
166
|
+
if (opts.includeFileList && opts.fileList) {
|
|
167
|
+
let sizeMap2 = null;
|
|
168
|
+
try {
|
|
169
|
+
const unpack = unpackBuffer(input);
|
|
170
|
+
if (unpack) {
|
|
171
|
+
sizeMap2 = {};
|
|
172
|
+
for (const ef of unpack.files)
|
|
173
|
+
sizeMap2[ef.path] = ef.buf.length;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (e) { }
|
|
177
|
+
const normalized = opts.fileList.map((f) => {
|
|
178
|
+
if (typeof f === 'string')
|
|
179
|
+
return { name: f, size: sizeMap2 && sizeMap2[f] ? sizeMap2[f] : 0 };
|
|
180
|
+
if (f && typeof f === 'object') {
|
|
181
|
+
if (f.name)
|
|
182
|
+
return { name: f.name, size: f.size ?? 0 };
|
|
183
|
+
if (f.path)
|
|
184
|
+
return { name: f.path, size: f.size ?? 0 };
|
|
185
|
+
}
|
|
186
|
+
return { name: String(f), size: 0 };
|
|
187
|
+
});
|
|
188
|
+
const jsonBuf = Buffer.from(JSON.stringify(normalized), 'utf8');
|
|
189
|
+
const lenBuf = Buffer.alloc(4);
|
|
190
|
+
lenBuf.writeUInt32BE(jsonBuf.length, 0);
|
|
191
|
+
metaPixel = Buffer.concat([
|
|
192
|
+
metaPixel,
|
|
193
|
+
Buffer.from('rXFL', 'utf8'),
|
|
194
|
+
lenBuf,
|
|
195
|
+
jsonBuf,
|
|
196
|
+
]);
|
|
197
|
+
}
|
|
198
|
+
const dataWithoutMarkers = Buffer.concat([PIXEL_MAGIC, metaPixel]);
|
|
199
|
+
const padding = (3 - (dataWithoutMarkers.length % 3)) % 3;
|
|
200
|
+
const paddedData = padding > 0
|
|
201
|
+
? Buffer.concat([dataWithoutMarkers, Buffer.alloc(padding)])
|
|
202
|
+
: dataWithoutMarkers;
|
|
203
|
+
const markerStartBytes = colorsToBytes(MARKER_START);
|
|
204
|
+
const compressionMarkerBytes = colorsToBytes(COMPRESSION_MARKERS.zstd);
|
|
205
|
+
const dataWithMarkers = Buffer.concat([
|
|
206
|
+
markerStartBytes,
|
|
207
|
+
compressionMarkerBytes,
|
|
208
|
+
paddedData,
|
|
209
|
+
]);
|
|
210
|
+
const bytesPerPixel = 3;
|
|
211
|
+
const dataPixels = Math.ceil(dataWithMarkers.length / 3);
|
|
212
|
+
const totalPixels = dataPixels + MARKER_END.length;
|
|
213
|
+
const maxWidth = 16384;
|
|
214
|
+
let side = Math.ceil(Math.sqrt(totalPixels));
|
|
215
|
+
if (side < MARKER_END.length)
|
|
216
|
+
side = MARKER_END.length;
|
|
217
|
+
let logicalWidth;
|
|
218
|
+
let logicalHeight;
|
|
219
|
+
if (side <= maxWidth) {
|
|
220
|
+
logicalWidth = side;
|
|
221
|
+
logicalHeight = side;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
logicalWidth = Math.min(maxWidth, totalPixels);
|
|
225
|
+
logicalHeight = Math.ceil(totalPixels / logicalWidth);
|
|
226
|
+
}
|
|
227
|
+
const scale = 1;
|
|
228
|
+
const width = logicalWidth * scale;
|
|
229
|
+
const height = logicalHeight * scale;
|
|
230
|
+
const raw = Buffer.alloc(width * height * bytesPerPixel);
|
|
231
|
+
for (let ly = 0; ly < logicalHeight; ly++) {
|
|
232
|
+
for (let lx = 0; lx < logicalWidth; lx++) {
|
|
233
|
+
const linearIdx = ly * logicalWidth + lx;
|
|
234
|
+
let r = 0, g = 0, b = 0;
|
|
235
|
+
if (ly === logicalHeight - 1 &&
|
|
236
|
+
lx >= logicalWidth - MARKER_END.length) {
|
|
237
|
+
const markerIdx = lx - (logicalWidth - MARKER_END.length);
|
|
238
|
+
r = MARKER_END[markerIdx].r;
|
|
239
|
+
g = MARKER_END[markerIdx].g;
|
|
240
|
+
b = MARKER_END[markerIdx].b;
|
|
241
|
+
}
|
|
242
|
+
else if (linearIdx < dataPixels) {
|
|
243
|
+
const srcIdx = linearIdx * 3;
|
|
244
|
+
r = srcIdx < dataWithMarkers.length ? dataWithMarkers[srcIdx] : 0;
|
|
245
|
+
g =
|
|
246
|
+
srcIdx + 1 < dataWithMarkers.length
|
|
247
|
+
? dataWithMarkers[srcIdx + 1]
|
|
248
|
+
: 0;
|
|
249
|
+
b =
|
|
250
|
+
srcIdx + 2 < dataWithMarkers.length
|
|
251
|
+
? dataWithMarkers[srcIdx + 2]
|
|
252
|
+
: 0;
|
|
253
|
+
}
|
|
254
|
+
for (let sy = 0; sy < scale; sy++) {
|
|
255
|
+
for (let sx = 0; sx < scale; sx++) {
|
|
256
|
+
const px = lx * scale + sx;
|
|
257
|
+
const py = ly * scale + sy;
|
|
258
|
+
const dstIdx = (py * width + px) * 3;
|
|
259
|
+
raw[dstIdx] = r;
|
|
260
|
+
raw[dstIdx + 1] = g;
|
|
261
|
+
raw[dstIdx + 2] = b;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (opts.onProgress)
|
|
267
|
+
opts.onProgress({ phase: 'png_gen', loaded: 0, total: 100 });
|
|
268
|
+
let loaded = 0;
|
|
269
|
+
const progressInterval = setInterval(() => {
|
|
270
|
+
loaded = Math.min(loaded + 2, 98);
|
|
271
|
+
if (opts.onProgress)
|
|
272
|
+
opts.onProgress({ phase: 'png_gen', loaded, total: 100 });
|
|
273
|
+
}, 50);
|
|
274
|
+
let bufScr;
|
|
275
|
+
const LARGE_IMAGE_PIXELS = 50000000;
|
|
276
|
+
if (width * height > LARGE_IMAGE_PIXELS || process.env.ROX_FAST_PNG) {
|
|
277
|
+
const idatData = zlib.deflateSync(raw, {
|
|
278
|
+
level: 6,
|
|
279
|
+
memLevel: 8,
|
|
280
|
+
strategy: zlib.constants.Z_DEFAULT_STRATEGY,
|
|
281
|
+
});
|
|
282
|
+
const ihdrData2 = Buffer.alloc(13);
|
|
283
|
+
ihdrData2.writeUInt32BE(width, 0);
|
|
284
|
+
ihdrData2.writeUInt32BE(height, 4);
|
|
285
|
+
ihdrData2[8] = 8;
|
|
286
|
+
ihdrData2[9] = 2;
|
|
287
|
+
ihdrData2[10] = 0;
|
|
288
|
+
ihdrData2[11] = 0;
|
|
289
|
+
ihdrData2[12] = 0;
|
|
290
|
+
const chunks2 = [];
|
|
291
|
+
chunks2.push({ name: 'IHDR', data: ihdrData2 });
|
|
292
|
+
chunks2.push({ name: 'IDAT', data: idatData });
|
|
293
|
+
chunks2.push({ name: 'IEND', data: Buffer.alloc(0) });
|
|
294
|
+
const tmp = Buffer.from(encode(chunks2));
|
|
295
|
+
bufScr =
|
|
296
|
+
tmp.slice(0, 8).toString('hex') === PNG_HEADER_HEX
|
|
297
|
+
? tmp
|
|
298
|
+
: Buffer.concat([PNG_HEADER, tmp]);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
bufScr = await sharp(raw, {
|
|
302
|
+
raw: { width, height, channels: 3 },
|
|
303
|
+
})
|
|
304
|
+
.png({
|
|
305
|
+
compressionLevel: 6,
|
|
306
|
+
palette: false,
|
|
307
|
+
effort: 1,
|
|
308
|
+
adaptiveFiltering: false,
|
|
309
|
+
})
|
|
310
|
+
.toBuffer();
|
|
311
|
+
}
|
|
312
|
+
if (opts.onProgress)
|
|
313
|
+
opts.onProgress({ phase: 'png_gen', loaded: 100, total: 100 });
|
|
314
|
+
if (opts.onProgress)
|
|
315
|
+
opts.onProgress({ phase: 'optimizing', loaded: 0, total: 100 });
|
|
316
|
+
let optInterval = null;
|
|
317
|
+
if (opts.onProgress) {
|
|
318
|
+
let optLoaded = 0;
|
|
319
|
+
optInterval = setInterval(() => {
|
|
320
|
+
optLoaded = Math.min(optLoaded + 5, 95);
|
|
321
|
+
opts.onProgress?.({
|
|
322
|
+
phase: 'optimizing',
|
|
323
|
+
loaded: optLoaded,
|
|
324
|
+
total: 100,
|
|
325
|
+
});
|
|
326
|
+
}, 100);
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
const optimized = await optimizePngBuffer(bufScr, true);
|
|
330
|
+
clearInterval(progressInterval);
|
|
331
|
+
if (optInterval) {
|
|
332
|
+
clearInterval(optInterval);
|
|
333
|
+
optInterval = null;
|
|
334
|
+
}
|
|
335
|
+
if (opts.onProgress)
|
|
336
|
+
opts.onProgress({ phase: 'optimizing', loaded: 100, total: 100 });
|
|
337
|
+
progressBar?.stop();
|
|
338
|
+
return optimized;
|
|
339
|
+
}
|
|
340
|
+
catch (e) {
|
|
341
|
+
clearInterval(progressInterval);
|
|
342
|
+
if (optInterval)
|
|
343
|
+
clearInterval(optInterval);
|
|
344
|
+
progressBar?.stop();
|
|
345
|
+
return bufScr;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class PassphraseRequiredError extends Error {
|
|
2
|
+
constructor(message?: string);
|
|
3
|
+
}
|
|
4
|
+
export declare class IncorrectPassphraseError extends Error {
|
|
5
|
+
constructor(message?: string);
|
|
6
|
+
}
|
|
7
|
+
export declare class DataFormatError extends Error {
|
|
8
|
+
constructor(message?: string);
|
|
9
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class PassphraseRequiredError extends Error {
|
|
2
|
+
constructor(message = 'Passphrase required') {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'PassphraseRequiredError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class IncorrectPassphraseError extends Error {
|
|
8
|
+
constructor(message = 'Incorrect passphrase') {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'IncorrectPassphraseError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class DataFormatError extends Error {
|
|
14
|
+
constructor(message = 'Data format error') {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'DataFormatError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
export declare function colorsToBytes(colors: Array<{
|
|
4
|
+
r: number;
|
|
5
|
+
g: number;
|
|
6
|
+
b: number;
|
|
7
|
+
}>): Buffer;
|
|
8
|
+
export declare function deltaEncode(data: Buffer): Buffer;
|
|
9
|
+
export declare function deltaDecode(data: Buffer): Buffer;
|
|
10
|
+
export declare function applyXor(buf: Buffer, passphrase: string): Buffer;
|
|
11
|
+
export declare function tryDecryptIfNeeded(buf: Buffer, passphrase?: string): Buffer;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createDecipheriv, pbkdf2Sync } from 'crypto';
|
|
2
|
+
import { ENC_AES, ENC_NONE, ENC_XOR } from './constants.js';
|
|
3
|
+
import { IncorrectPassphraseError, PassphraseRequiredError } from './errors.js';
|
|
4
|
+
export function colorsToBytes(colors) {
|
|
5
|
+
const buf = Buffer.alloc(colors.length * 3);
|
|
6
|
+
for (let i = 0; i < colors.length; i++) {
|
|
7
|
+
buf[i * 3] = colors[i].r;
|
|
8
|
+
buf[i * 3 + 1] = colors[i].g;
|
|
9
|
+
buf[i * 3 + 2] = colors[i].b;
|
|
10
|
+
}
|
|
11
|
+
return buf;
|
|
12
|
+
}
|
|
13
|
+
export function deltaEncode(data) {
|
|
14
|
+
if (data.length === 0)
|
|
15
|
+
return data;
|
|
16
|
+
const out = Buffer.alloc(data.length);
|
|
17
|
+
out[0] = data[0];
|
|
18
|
+
for (let i = 1; i < data.length; i++) {
|
|
19
|
+
out[i] = (data[i] - data[i - 1] + 256) & 0xff;
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
export function deltaDecode(data) {
|
|
24
|
+
if (data.length === 0)
|
|
25
|
+
return data;
|
|
26
|
+
const out = Buffer.alloc(data.length);
|
|
27
|
+
out[0] = data[0];
|
|
28
|
+
for (let i = 1; i < data.length; i++) {
|
|
29
|
+
out[i] = (out[i - 1] + data[i]) & 0xff;
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
export function applyXor(buf, passphrase) {
|
|
34
|
+
const key = Buffer.from(passphrase, 'utf8');
|
|
35
|
+
const out = Buffer.alloc(buf.length);
|
|
36
|
+
for (let i = 0; i < buf.length; i++) {
|
|
37
|
+
out[i] = buf[i] ^ key[i % key.length];
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
export function tryDecryptIfNeeded(buf, passphrase) {
|
|
42
|
+
if (!buf || buf.length === 0)
|
|
43
|
+
return buf;
|
|
44
|
+
const flag = buf[0];
|
|
45
|
+
if (flag === ENC_AES) {
|
|
46
|
+
const MIN_AES_LEN = 1 + 16 + 12 + 16 + 1;
|
|
47
|
+
if (buf.length < MIN_AES_LEN)
|
|
48
|
+
throw new IncorrectPassphraseError();
|
|
49
|
+
if (!passphrase)
|
|
50
|
+
throw new PassphraseRequiredError();
|
|
51
|
+
const salt = buf.slice(1, 17);
|
|
52
|
+
const iv = buf.slice(17, 29);
|
|
53
|
+
const tag = buf.slice(29, 45);
|
|
54
|
+
const enc = buf.slice(45);
|
|
55
|
+
const PBKDF2_ITERS = 1000000;
|
|
56
|
+
const key = pbkdf2Sync(passphrase, salt, PBKDF2_ITERS, 32, 'sha256');
|
|
57
|
+
const dec = createDecipheriv('aes-256-gcm', key, iv);
|
|
58
|
+
dec.setAuthTag(tag);
|
|
59
|
+
try {
|
|
60
|
+
const decrypted = Buffer.concat([dec.update(enc), dec.final()]);
|
|
61
|
+
return decrypted;
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
throw new IncorrectPassphraseError();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (flag === ENC_XOR) {
|
|
68
|
+
if (!passphrase)
|
|
69
|
+
throw new PassphraseRequiredError();
|
|
70
|
+
return applyXor(buf.slice(1), passphrase);
|
|
71
|
+
}
|
|
72
|
+
if (flag === ENC_NONE) {
|
|
73
|
+
return buf.slice(1);
|
|
74
|
+
}
|
|
75
|
+
return buf;
|
|
76
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
/**
|
|
4
|
+
* List files in a Rox PNG archive without decoding the full payload.
|
|
5
|
+
* Returns the file list if available, otherwise null.
|
|
6
|
+
* @param pngBuf - PNG data
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
export declare function listFilesInPng(pngBuf: Buffer, opts?: {
|
|
10
|
+
includeSizes?: boolean;
|
|
11
|
+
}): Promise<string[] | {
|
|
12
|
+
name: string;
|
|
13
|
+
size: number;
|
|
14
|
+
}[] | null>;
|
|
15
|
+
/**
|
|
16
|
+
* Detect if a PNG/ROX buffer contains an encrypted payload (requires passphrase)
|
|
17
|
+
* Returns true if encryption flag indicates AES or XOR.
|
|
18
|
+
*/
|
|
19
|
+
export declare function hasPassphraseInPng(pngBuf: Buffer): Promise<boolean>;
|