roxify 1.1.8 → 1.1.9
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/README.md +0 -8
- package/dist/cli.js +2 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +105 -55
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -95,14 +95,6 @@ const pngBuffer = await encodeBinaryToPng(inputBuffer, {
|
|
|
95
95
|
});
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
```js
|
|
99
|
-
const { buf, meta } = await decodePngToBinary(pngFromDisk, {
|
|
100
|
-
onProgress: (info) => {
|
|
101
|
-
console.log(`Phase: ${info.phase}, Loaded: ${info.loaded}/${info.total}`);
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
```
|
|
105
|
-
|
|
106
98
|
## Requirements
|
|
107
99
|
|
|
108
100
|
- Node.js 18+ (ESM)
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
|
4
4
|
import { basename, dirname, join, resolve } from 'path';
|
|
5
5
|
import { DataFormatError, decodePngToBinary, encodeBinaryToPng, IncorrectPassphraseError, listFilesInPng, PassphraseRequiredError, } from './index.js';
|
|
6
6
|
import { packPaths, unpackBuffer } from './pack.js';
|
|
7
|
-
const VERSION = '1.1.
|
|
7
|
+
const VERSION = '1.1.9';
|
|
8
8
|
function showHelp() {
|
|
9
9
|
console.log(`
|
|
10
10
|
ROX CLI — Encode/decode binary in PNG
|
|
@@ -190,9 +190,7 @@ async function encodeCommand(args) {
|
|
|
190
190
|
options.fileList = packResult.list;
|
|
191
191
|
}
|
|
192
192
|
else {
|
|
193
|
-
const startRead = Date.now();
|
|
194
193
|
inputBuffer = readFileSync(resolvedInput);
|
|
195
|
-
const readTime = Date.now() - startRead;
|
|
196
194
|
console.log('');
|
|
197
195
|
displayName = basename(resolvedInput);
|
|
198
196
|
}
|
|
@@ -213,7 +211,6 @@ async function encodeCommand(args) {
|
|
|
213
211
|
const encodeBar = new cliProgress.SingleBar({
|
|
214
212
|
format: ' {bar} {percentage}% | {step} | {elapsed}s',
|
|
215
213
|
}, cliProgress.Presets.shades_classic);
|
|
216
|
-
let totalMB = Math.max(1, Math.round(inputBuffer.length / 1024 / 1024));
|
|
217
214
|
encodeBar.start(100, 0, {
|
|
218
215
|
step: 'Starting',
|
|
219
216
|
elapsed: '0',
|
|
@@ -461,7 +458,7 @@ async function listCommand(args) {
|
|
|
461
458
|
const resolvedInput = resolve(inputPath);
|
|
462
459
|
try {
|
|
463
460
|
const inputBuffer = readFileSync(resolvedInput);
|
|
464
|
-
const fileList = listFilesInPng(inputBuffer);
|
|
461
|
+
const fileList = await listFilesInPng(inputBuffer);
|
|
465
462
|
if (fileList) {
|
|
466
463
|
console.log(`Files in ${resolvedInput}:`);
|
|
467
464
|
for (const file of fileList) {
|
package/dist/index.d.ts
CHANGED
|
@@ -195,4 +195,4 @@ export { packPaths, unpackBuffer } from './pack.js';
|
|
|
195
195
|
* @param pngBuf - PNG data
|
|
196
196
|
* @public
|
|
197
197
|
*/
|
|
198
|
-
export declare function listFilesInPng(pngBuf: Buffer): string[] | null
|
|
198
|
+
export declare function listFilesInPng(pngBuf: Buffer): Promise<string[] | null>;
|
package/dist/index.js
CHANGED
|
@@ -90,7 +90,6 @@ async function parallelZstdCompress(payload, level = 22, onProgress) {
|
|
|
90
90
|
if (payload.length <= chunkSize) {
|
|
91
91
|
return Buffer.from(await zstdCompress(payload, level));
|
|
92
92
|
}
|
|
93
|
-
const chunks = [];
|
|
94
93
|
const promises = [];
|
|
95
94
|
const totalChunks = Math.ceil(payload.length / chunkSize);
|
|
96
95
|
let completedChunks = 0;
|
|
@@ -131,6 +130,8 @@ async function parallelZstdDecompress(payload, onProgress, onChunk, outPath) {
|
|
|
131
130
|
}
|
|
132
131
|
const magic = payload.readUInt32BE(0);
|
|
133
132
|
if (magic !== 0x5a535444) {
|
|
133
|
+
if (process.env.ROX_DEBUG)
|
|
134
|
+
console.log('tryZstdDecompress: invalid magic');
|
|
134
135
|
onProgress?.({ phase: 'decompress_start', total: 1 });
|
|
135
136
|
const d = Buffer.from(await zstdDecompress(payload));
|
|
136
137
|
onProgress?.({ phase: 'decompress_progress', loaded: 1, total: 1 });
|
|
@@ -211,9 +212,6 @@ function applyXor(buf, passphrase) {
|
|
|
211
212
|
}
|
|
212
213
|
return out;
|
|
213
214
|
}
|
|
214
|
-
function tryBrotliDecompress(payload) {
|
|
215
|
-
return Buffer.from(zlib.brotliDecompressSync(payload));
|
|
216
|
-
}
|
|
217
215
|
async function tryZstdDecompress(payload, onProgress, onChunk, outPath) {
|
|
218
216
|
return await parallelZstdDecompress(payload, onProgress, onChunk, outPath);
|
|
219
217
|
}
|
|
@@ -253,19 +251,6 @@ function tryDecryptIfNeeded(buf, passphrase) {
|
|
|
253
251
|
}
|
|
254
252
|
return buf;
|
|
255
253
|
}
|
|
256
|
-
function idxFor(x, y, width) {
|
|
257
|
-
return (y * width + x) * 4;
|
|
258
|
-
}
|
|
259
|
-
function eqRGB(a, b) {
|
|
260
|
-
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
261
|
-
}
|
|
262
|
-
async function loadRaw(imgInput) {
|
|
263
|
-
const { data, info } = await sharp(imgInput)
|
|
264
|
-
.ensureAlpha()
|
|
265
|
-
.raw()
|
|
266
|
-
.toBuffer({ resolveWithObject: true });
|
|
267
|
-
return { data, info };
|
|
268
|
-
}
|
|
269
254
|
export async function cropAndReconstitute(input, debugDir) {
|
|
270
255
|
async function loadRaw(imgInput) {
|
|
271
256
|
const { data, info } = await sharp(imgInput)
|
|
@@ -280,7 +265,7 @@ export async function cropAndReconstitute(input, debugDir) {
|
|
|
280
265
|
function eqRGB(a, b) {
|
|
281
266
|
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
282
267
|
}
|
|
283
|
-
const {
|
|
268
|
+
const { info } = await loadRaw(input);
|
|
284
269
|
const doubledBuffer = await sharp(input)
|
|
285
270
|
.resize({
|
|
286
271
|
width: info.width * 2,
|
|
@@ -585,9 +570,7 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
585
570
|
}
|
|
586
571
|
}
|
|
587
572
|
let payload = Buffer.concat([MAGIC, input]);
|
|
588
|
-
const brQuality = typeof opts.brQuality === 'number' ? opts.brQuality : 11;
|
|
589
573
|
const mode = opts.mode === undefined ? 'screenshot' : opts.mode;
|
|
590
|
-
const compression = opts.compression || 'zstd';
|
|
591
574
|
if (opts.onProgress)
|
|
592
575
|
opts.onProgress({ phase: 'compress_start', total: payload.length });
|
|
593
576
|
const useDelta = mode !== 'screenshot';
|
|
@@ -635,8 +618,6 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
635
618
|
payload = Buffer.concat([Buffer.from([ENC_AES]), salt, iv, tag, enc]);
|
|
636
619
|
if (opts.onProgress)
|
|
637
620
|
opts.onProgress({ phase: 'encrypt_done' });
|
|
638
|
-
}
|
|
639
|
-
else if (encChoice === 'xor') {
|
|
640
621
|
const xored = applyXor(payload, opts.passphrase);
|
|
641
622
|
payload = Buffer.concat([Buffer.from([ENC_XOR]), xored]);
|
|
642
623
|
if (opts.onProgress)
|
|
@@ -664,7 +645,13 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
664
645
|
metaParts.push(Buffer.from([0]));
|
|
665
646
|
}
|
|
666
647
|
metaParts.push(payload);
|
|
667
|
-
|
|
648
|
+
let meta = Buffer.concat(metaParts);
|
|
649
|
+
if (opts.includeFileList && opts.fileList) {
|
|
650
|
+
const jsonBuf = Buffer.from(JSON.stringify(opts.fileList), 'utf8');
|
|
651
|
+
const lenBuf = Buffer.alloc(4);
|
|
652
|
+
lenBuf.writeUInt32BE(jsonBuf.length, 0);
|
|
653
|
+
meta = Buffer.concat([meta, Buffer.from('rXFL', 'utf8'), lenBuf, jsonBuf]);
|
|
654
|
+
}
|
|
668
655
|
if (opts.output === 'rox') {
|
|
669
656
|
return Buffer.concat([MAGIC, meta]);
|
|
670
657
|
}
|
|
@@ -676,13 +663,24 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
676
663
|
const payloadLenBuf = Buffer.alloc(4);
|
|
677
664
|
payloadLenBuf.writeUInt32BE(payload.length, 0);
|
|
678
665
|
const version = 1;
|
|
679
|
-
|
|
666
|
+
let metaPixel = Buffer.concat([
|
|
680
667
|
Buffer.from([version]),
|
|
681
668
|
Buffer.from([nameLen]),
|
|
682
669
|
nameBuf,
|
|
683
670
|
payloadLenBuf,
|
|
684
671
|
payload,
|
|
685
672
|
]);
|
|
673
|
+
if (opts.includeFileList && opts.fileList) {
|
|
674
|
+
const jsonBuf = Buffer.from(JSON.stringify(opts.fileList), 'utf8');
|
|
675
|
+
const lenBuf = Buffer.alloc(4);
|
|
676
|
+
lenBuf.writeUInt32BE(jsonBuf.length, 0);
|
|
677
|
+
metaPixel = Buffer.concat([
|
|
678
|
+
metaPixel,
|
|
679
|
+
Buffer.from('rXFL', 'utf8'),
|
|
680
|
+
lenBuf,
|
|
681
|
+
jsonBuf,
|
|
682
|
+
]);
|
|
683
|
+
}
|
|
686
684
|
const dataWithoutMarkers = Buffer.concat([PIXEL_MAGIC, metaPixel]);
|
|
687
685
|
const padding = (3 - (dataWithoutMarkers.length % 3)) % 3;
|
|
688
686
|
const paddedData = padding > 0
|
|
@@ -763,15 +761,6 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
763
761
|
adaptiveFiltering: true,
|
|
764
762
|
})
|
|
765
763
|
.toBuffer();
|
|
766
|
-
if (opts.includeFileList && opts.fileList) {
|
|
767
|
-
const chunks = extract(bufScr);
|
|
768
|
-
const fileListChunk = {
|
|
769
|
-
name: 'rXFL',
|
|
770
|
-
data: Buffer.from(JSON.stringify(opts.fileList), 'utf8'),
|
|
771
|
-
};
|
|
772
|
-
chunks.splice(-1, 0, fileListChunk);
|
|
773
|
-
bufScr = Buffer.from(encode(chunks));
|
|
774
|
-
}
|
|
775
764
|
if (opts.onProgress)
|
|
776
765
|
opts.onProgress({ phase: 'done', loaded: bufScr.length });
|
|
777
766
|
progressBar?.stop();
|
|
@@ -785,13 +774,24 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
785
774
|
const payloadLenBuf = Buffer.alloc(4);
|
|
786
775
|
payloadLenBuf.writeUInt32BE(payload.length, 0);
|
|
787
776
|
const version = 1;
|
|
788
|
-
|
|
777
|
+
let metaPixel = Buffer.concat([
|
|
789
778
|
Buffer.from([version]),
|
|
790
779
|
Buffer.from([nameLen]),
|
|
791
780
|
nameBuf,
|
|
792
781
|
payloadLenBuf,
|
|
793
782
|
payload,
|
|
794
783
|
]);
|
|
784
|
+
if (opts.includeFileList && opts.fileList) {
|
|
785
|
+
const jsonBuf = Buffer.from(JSON.stringify(opts.fileList), 'utf8');
|
|
786
|
+
const lenBuf = Buffer.alloc(4);
|
|
787
|
+
lenBuf.writeUInt32BE(jsonBuf.length, 0);
|
|
788
|
+
metaPixel = Buffer.concat([
|
|
789
|
+
metaPixel,
|
|
790
|
+
Buffer.from('rXFL', 'utf8'),
|
|
791
|
+
lenBuf,
|
|
792
|
+
jsonBuf,
|
|
793
|
+
]);
|
|
794
|
+
}
|
|
795
795
|
const full = Buffer.concat([PIXEL_MAGIC, metaPixel]);
|
|
796
796
|
const bytesPerPixel = 3;
|
|
797
797
|
const nPixels = Math.ceil((full.length + 8) / 3);
|
|
@@ -873,12 +873,6 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
873
873
|
chunks2.push({ name: 'IHDR', data: ihdrData });
|
|
874
874
|
chunks2.push({ name: 'IDAT', data: idatData });
|
|
875
875
|
chunks2.push({ name: CHUNK_TYPE, data: meta });
|
|
876
|
-
if (opts.includeFileList && opts.fileList) {
|
|
877
|
-
chunks2.push({
|
|
878
|
-
name: 'rXFL',
|
|
879
|
-
data: Buffer.from(JSON.stringify(opts.fileList), 'utf8'),
|
|
880
|
-
});
|
|
881
|
-
}
|
|
882
876
|
chunks2.push({ name: 'IEND', data: Buffer.alloc(0) });
|
|
883
877
|
if (opts.onProgress)
|
|
884
878
|
opts.onProgress({ phase: 'png_gen' });
|
|
@@ -946,9 +940,6 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
946
940
|
if (rawBytesEstimate > MAX_RAW_BYTES) {
|
|
947
941
|
throw new DataFormatError(`Image too large to decode in-process (${Math.round(rawBytesEstimate / 1024 / 1024)} MB). Increase Node heap or use a smaller image/compact mode.`);
|
|
948
942
|
}
|
|
949
|
-
const MAX_DOUBLE_BYTES = 200 * 1024 * 1024;
|
|
950
|
-
const doubledPixels = info.width * 2 * (info.height * 2);
|
|
951
|
-
const doubledBytesEstimate = doubledPixels * 4;
|
|
952
943
|
if (false) {
|
|
953
944
|
const doubledBuffer = await sharp(pngBuf)
|
|
954
945
|
.resize({
|
|
@@ -1499,7 +1490,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
1499
1490
|
await tryZstdDecompress(payload, (info) => {
|
|
1500
1491
|
if (opts.onProgress)
|
|
1501
1492
|
opts.onProgress(info);
|
|
1502
|
-
}, async (decChunk
|
|
1493
|
+
}, async (decChunk) => {
|
|
1503
1494
|
let outChunk = decChunk;
|
|
1504
1495
|
if (version === 3) {
|
|
1505
1496
|
const out = Buffer.alloc(decChunk.length);
|
|
@@ -1538,7 +1529,7 @@ export async function decodePngToBinary(pngBuf, opts = {}) {
|
|
|
1538
1529
|
await writeInChunks(ws, outChunk, 64 * 1024);
|
|
1539
1530
|
}
|
|
1540
1531
|
});
|
|
1541
|
-
await new Promise((res
|
|
1532
|
+
await new Promise((res) => ws.end(() => res()));
|
|
1542
1533
|
if (opts.onProgress)
|
|
1543
1534
|
opts.onProgress({ phase: 'done' });
|
|
1544
1535
|
progressBar?.stop();
|
|
@@ -1609,7 +1600,60 @@ export { packPaths, unpackBuffer } from './pack.js';
|
|
|
1609
1600
|
* @param pngBuf - PNG data
|
|
1610
1601
|
* @public
|
|
1611
1602
|
*/
|
|
1612
|
-
export function listFilesInPng(pngBuf) {
|
|
1603
|
+
export async function listFilesInPng(pngBuf) {
|
|
1604
|
+
try {
|
|
1605
|
+
try {
|
|
1606
|
+
const { data, info } = await sharp(pngBuf)
|
|
1607
|
+
.ensureAlpha()
|
|
1608
|
+
.raw()
|
|
1609
|
+
.toBuffer({ resolveWithObject: true });
|
|
1610
|
+
const currentWidth = info.width;
|
|
1611
|
+
const currentHeight = info.height;
|
|
1612
|
+
const rawRGB = Buffer.alloc(currentWidth * currentHeight * 3);
|
|
1613
|
+
for (let i = 0; i < currentWidth * currentHeight; i++) {
|
|
1614
|
+
rawRGB[i * 3] = data[i * 4];
|
|
1615
|
+
rawRGB[i * 3 + 1] = data[i * 4 + 1];
|
|
1616
|
+
rawRGB[i * 3 + 2] = data[i * 4 + 2];
|
|
1617
|
+
}
|
|
1618
|
+
const found = rawRGB.indexOf(PIXEL_MAGIC);
|
|
1619
|
+
if (found !== -1) {
|
|
1620
|
+
let idx = found + PIXEL_MAGIC.length;
|
|
1621
|
+
if (idx + 2 <= rawRGB.length) {
|
|
1622
|
+
const version = rawRGB[idx++];
|
|
1623
|
+
const nameLen = rawRGB[idx++];
|
|
1624
|
+
if (process.env.ROX_DEBUG)
|
|
1625
|
+
console.log('listFilesInPng: pixel version', version, 'nameLen', nameLen);
|
|
1626
|
+
if (nameLen > 0 && idx + nameLen <= rawRGB.length) {
|
|
1627
|
+
idx += nameLen;
|
|
1628
|
+
}
|
|
1629
|
+
if (idx + 4 <= rawRGB.length) {
|
|
1630
|
+
const payloadLen = rawRGB.readUInt32BE(idx);
|
|
1631
|
+
idx += 4;
|
|
1632
|
+
const afterPayload = idx + payloadLen;
|
|
1633
|
+
if (afterPayload <= rawRGB.length) {
|
|
1634
|
+
if (afterPayload + 8 <= rawRGB.length) {
|
|
1635
|
+
const marker = rawRGB
|
|
1636
|
+
.slice(afterPayload, afterPayload + 4)
|
|
1637
|
+
.toString('utf8');
|
|
1638
|
+
if (marker === 'rXFL') {
|
|
1639
|
+
const jsonLen = rawRGB.readUInt32BE(afterPayload + 4);
|
|
1640
|
+
const jsonStart = afterPayload + 8;
|
|
1641
|
+
const jsonEnd = jsonStart + jsonLen;
|
|
1642
|
+
if (jsonEnd <= rawRGB.length) {
|
|
1643
|
+
const jsonBuf = rawRGB.slice(jsonStart, jsonEnd);
|
|
1644
|
+
const files = JSON.parse(jsonBuf.toString('utf8'));
|
|
1645
|
+
return files.sort();
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
catch (e) { }
|
|
1655
|
+
}
|
|
1656
|
+
catch (e) { }
|
|
1613
1657
|
try {
|
|
1614
1658
|
const chunks = extract(pngBuf);
|
|
1615
1659
|
const fileListChunk = chunks.find((c) => c.name === 'rXFL');
|
|
@@ -1618,17 +1662,23 @@ export function listFilesInPng(pngBuf) {
|
|
|
1618
1662
|
? fileListChunk.data
|
|
1619
1663
|
: Buffer.from(fileListChunk.data);
|
|
1620
1664
|
const files = JSON.parse(data.toString('utf8'));
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1665
|
+
return files.sort();
|
|
1666
|
+
}
|
|
1667
|
+
const metaChunk = chunks.find((c) => c.name === CHUNK_TYPE);
|
|
1668
|
+
if (metaChunk) {
|
|
1669
|
+
const dataBuf = Buffer.isBuffer(metaChunk.data)
|
|
1670
|
+
? metaChunk.data
|
|
1671
|
+
: Buffer.from(metaChunk.data);
|
|
1672
|
+
const markerIdx = dataBuf.indexOf(Buffer.from('rXFL'));
|
|
1673
|
+
if (markerIdx !== -1 && markerIdx + 8 <= dataBuf.length) {
|
|
1674
|
+
const jsonLen = dataBuf.readUInt32BE(markerIdx + 4);
|
|
1675
|
+
const jsonStart = markerIdx + 8;
|
|
1676
|
+
const jsonEnd = jsonStart + jsonLen;
|
|
1677
|
+
if (jsonEnd <= dataBuf.length) {
|
|
1678
|
+
const files = JSON.parse(dataBuf.slice(jsonStart, jsonEnd).toString('utf8'));
|
|
1679
|
+
return files.sort();
|
|
1628
1680
|
}
|
|
1629
1681
|
}
|
|
1630
|
-
const all = [...dirs, ...files];
|
|
1631
|
-
return all.sort();
|
|
1632
1682
|
}
|
|
1633
1683
|
}
|
|
1634
1684
|
catch (e) { }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roxify",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.9",
|
|
4
4
|
"description": "Encode binary data into PNG images with Zstd compression and decode them back. Supports CLI and programmatic API (Node.js ESM).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"build": "tsc",
|
|
17
17
|
"check-publish": "node ../scripts/check-publish.js roxify",
|
|
18
18
|
"cli": "node dist/cli.js",
|
|
19
|
-
"test": "npm run build && node test/pack.test.js && node test/screenshot.test.js"
|
|
19
|
+
"test": "npm run build && node test/pack.test.js && node test/screenshot.test.js && node test/list.test.js"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
22
22
|
"steganography",
|