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 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';
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 { data, info } = await loadRaw(input);
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
- const meta = Buffer.concat(metaParts);
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
- const metaPixel = Buffer.concat([
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
- const metaPixel = Buffer.concat([
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, idxChunk, totalChunks) => {
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, rej) => ws.end(() => 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
- const dirs = new Set();
1622
- for (const file of files) {
1623
- const parts = file.split('/');
1624
- let path = '';
1625
- for (let i = 0; i < parts.length - 1; i++) {
1626
- path += parts[i] + '/';
1627
- dirs.add(path);
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.8",
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",