roxify 1.13.6 → 1.13.8

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/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "roxify_native"
3
- version = "1.13.6"
3
+ version = "1.13.8"
4
4
  edition = "2021"
5
5
  publish = false
6
6
 
package/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ async function loadJsEngine() {
20
20
  VFSIndexEntry: undefined,
21
21
  };
22
22
  }
23
- const VERSION = '1.13.6';
23
+ const VERSION = '1.13.8';
24
24
  function getDirectorySize(dirPath) {
25
25
  let totalSize = 0;
26
26
  try {
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1,7 +1,42 @@
1
1
  import { DecodeOptions, DecodeResult } from './types.js';
2
2
  /**
3
3
  * Decode a ROX PNG or buffer into the original binary payload or files list.
4
- * This function uses the Rust native implementation exclusively.
4
+ * This function extracts pixels, parses the payload header, handles encryption/decryption,
5
+ * decompresses with zstd, and returns the decoded buffer - all in memory.
6
+ *
7
+ * @example
8
+ * ```js
9
+ * import { readFileSync, writeFileSync } from 'fs';
10
+ * import { decodePngToBinary } from 'roxify';
11
+ *
12
+ * // Decode a PNG file
13
+ * const png = readFileSync('config.png');
14
+ * const result = await decodePngToBinary(png);
15
+ *
16
+ * console.log(result.buf.toString()); // Original content
17
+ * console.log(result.meta?.name); // Original filename (e.g., "config.json")
18
+ *
19
+ * // Save with original filename
20
+ * writeFileSync(result.meta?.name || 'output.bin', result.buf);
21
+ * ```
22
+ *
23
+ * @example
24
+ * ```js
25
+ * // Decode from file path
26
+ * const result = await decodePngToBinary('config.png');
27
+ * writeFileSync('output.bin', result.buf);
28
+ * ```
29
+ *
30
+ * @example
31
+ * ```js
32
+ * // Handle multi-file archives
33
+ * const result = await decodePngToBinary(png);
34
+ * if (result.files) {
35
+ * for (const file of result.files) {
36
+ * writeFileSync(file.path, file.buf);
37
+ * }
38
+ * }
39
+ * ```
5
40
  *
6
41
  * @param input - Buffer or path to a PNG file.
7
42
  * @param opts - Optional decode options.
@@ -1,15 +1,123 @@
1
1
  import { readFileSync } from 'fs';
2
2
  import { native } from './native.js';
3
3
  import { unpackBuffer } from '../pack.js';
4
+ const PXL1_MAGIC = Buffer.from([0x50, 0x58, 0x4c, 0x31]); // "PXL1"
5
+ /**
6
+ * Find PXL1 magic in pixel buffer
7
+ */
8
+ function findPxl1Offset(pixels) {
9
+ for (let i = 0; i <= pixels.length - 4; i++) {
10
+ if (pixels[i] === 0x50 && pixels[i + 1] === 0x58 &&
11
+ pixels[i + 2] === 0x4c && pixels[i + 3] === 0x31) {
12
+ return i;
13
+ }
14
+ }
15
+ return -1;
16
+ }
17
+ /**
18
+ * Parse payload header and extract compressed data from pixels
19
+ */
20
+ function extractPayloadFromPixels(pixels) {
21
+ const pos = findPxl1Offset(pixels);
22
+ if (pos < 0) {
23
+ throw new Error('PXL1 magic not found in pixels');
24
+ }
25
+ let offset = pos + 4; // Skip "PXL1"
26
+ // Read version (1 byte)
27
+ if (offset >= pixels.length) {
28
+ throw new Error('Truncated header: missing version');
29
+ }
30
+ const version = pixels[offset];
31
+ offset += 1;
32
+ // Read name length (1 byte)
33
+ if (offset >= pixels.length) {
34
+ throw new Error('Truncated header: missing name length');
35
+ }
36
+ const nameLen = pixels[offset];
37
+ offset += 1;
38
+ // Read name if present
39
+ let name;
40
+ if (nameLen > 0) {
41
+ if (offset + nameLen > pixels.length) {
42
+ throw new Error('Truncated header: name exceeds buffer');
43
+ }
44
+ name = pixels.subarray(offset, offset + nameLen).toString('utf8');
45
+ offset += nameLen;
46
+ }
47
+ // Read payload length
48
+ if (version === 1) {
49
+ if (offset + 4 > pixels.length) {
50
+ throw new Error('Truncated header: missing payload length (V1)');
51
+ }
52
+ const payloadLen = pixels.readUInt32BE(offset);
53
+ offset += 4;
54
+ if (offset + payloadLen > pixels.length) {
55
+ throw new Error('Truncated payload data');
56
+ }
57
+ const payload = pixels.subarray(offset, offset + payloadLen);
58
+ return { payload, name };
59
+ }
60
+ else if (version === 2) {
61
+ if (offset + 8 > pixels.length) {
62
+ throw new Error('Truncated header: missing payload length (V2)');
63
+ }
64
+ const payloadLen = Number(pixels.readBigUInt64BE(offset));
65
+ offset += 8;
66
+ if (offset + payloadLen > pixels.length) {
67
+ throw new Error('Truncated payload data');
68
+ }
69
+ const payload = pixels.subarray(offset, offset + payloadLen);
70
+ return { payload, name };
71
+ }
72
+ else {
73
+ throw new Error(`Unsupported header version: ${version}`);
74
+ }
75
+ }
4
76
  /**
5
77
  * Decode a ROX PNG or buffer into the original binary payload or files list.
6
- * This function uses the Rust native implementation exclusively.
78
+ * This function extracts pixels, parses the payload header, handles encryption/decryption,
79
+ * decompresses with zstd, and returns the decoded buffer - all in memory.
80
+ *
81
+ * @example
82
+ * ```js
83
+ * import { readFileSync, writeFileSync } from 'fs';
84
+ * import { decodePngToBinary } from 'roxify';
85
+ *
86
+ * // Decode a PNG file
87
+ * const png = readFileSync('config.png');
88
+ * const result = await decodePngToBinary(png);
89
+ *
90
+ * console.log(result.buf.toString()); // Original content
91
+ * console.log(result.meta?.name); // Original filename (e.g., "config.json")
92
+ *
93
+ * // Save with original filename
94
+ * writeFileSync(result.meta?.name || 'output.bin', result.buf);
95
+ * ```
96
+ *
97
+ * @example
98
+ * ```js
99
+ * // Decode from file path
100
+ * const result = await decodePngToBinary('config.png');
101
+ * writeFileSync('output.bin', result.buf);
102
+ * ```
103
+ *
104
+ * @example
105
+ * ```js
106
+ * // Handle multi-file archives
107
+ * const result = await decodePngToBinary(png);
108
+ * if (result.files) {
109
+ * for (const file of result.files) {
110
+ * writeFileSync(file.path, file.buf);
111
+ * }
112
+ * }
113
+ * ```
7
114
  *
8
115
  * @param input - Buffer or path to a PNG file.
9
116
  * @param opts - Optional decode options.
10
117
  * @returns A Promise resolving to DecodeResult ({ buf, meta } or { files }).
11
118
  */
12
119
  export async function decodePngToBinary(input, opts = {}) {
120
+ // Get PNG buffer
13
121
  let pngBuf;
14
122
  if (Buffer.isBuffer(input)) {
15
123
  pngBuf = input;
@@ -17,51 +125,49 @@ export async function decodePngToBinary(input, opts = {}) {
17
125
  else {
18
126
  pngBuf = readFileSync(input);
19
127
  }
20
- // --- Native decoder: let Rust handle extraction/decompression/decryption ---
21
- const payload = Buffer.from(native.extractPayloadFromPng(pngBuf));
128
+ // Decode PNG to RGB pixels
129
+ const rgbResult = native.pngToRgb(pngBuf);
130
+ const pixels = Buffer.from(rgbResult.pixels);
131
+ // Extract payload from pixels
132
+ const { payload, name } = extractPayloadFromPixels(pixels);
22
133
  if (payload.length === 0) {
23
- throw new Error('No payload found in PNG');
134
+ throw new Error('Empty payload extracted');
24
135
  }
25
- // Extract name from payload header (version byte, name length, name)
26
- let name;
27
- let dataOffset = 0;
28
- if (payload.length > 2) {
29
- const version = payload[0];
30
- const nameLen = payload[1];
31
- dataOffset = 2;
32
- if (nameLen > 0 && payload.length >= 2 + nameLen + 8) {
33
- name = payload.subarray(2, 2 + nameLen).toString('utf8');
34
- dataOffset = 2 + nameLen;
35
- }
36
- // Read payload length (8 bytes, big-endian, after name)
37
- const payloadLen = Number(payload.readBigUInt64BE(dataOffset));
38
- dataOffset += 8;
39
- // The actual compressed/encrypted data starts at dataOffset
40
- let compressedData = payload.subarray(dataOffset, dataOffset + payloadLen);
41
- // Try to decompress with zstd if needed
42
- let decompressed;
43
- try {
44
- decompressed = Buffer.from(native.nativeZstdDecompress(compressedData));
45
- }
46
- catch {
47
- // If decompression fails, use raw data
48
- decompressed = compressedData;
49
- }
50
- // Check for ROX1 magic
51
- if (decompressed.length >= 4 && decompressed.subarray(0, 4).toString() === 'ROX1') {
52
- decompressed = decompressed.subarray(4);
53
- }
54
- // Try to unpack as multi-file archive
55
- try {
56
- const unpacked = unpackBuffer(decompressed);
57
- if (unpacked && unpacked.files && unpacked.files.length > 0) {
58
- return { files: unpacked.files, meta: { name } };
59
- }
60
- }
61
- catch {
62
- // Fall through to raw buffer return
136
+ // Handle encryption flag (first byte)
137
+ // 0x00 = none, 0x01 = XOR, 0x02 = AES, 0x03 = AES-CTR
138
+ let data;
139
+ if (payload[0] !== 0x00) {
140
+ // Encrypted payload - not supported in current decoder
141
+ // The native encoder handles encryption, but decoder needs native decrypt support
142
+ throw new Error('Encrypted payload requires passphrase (not yet implemented in decoder)');
143
+ }
144
+ else {
145
+ // Non-encrypted: skip the flag byte
146
+ data = payload.subarray(1);
147
+ }
148
+ // Decompress with zstd
149
+ let decompressed;
150
+ try {
151
+ decompressed = Buffer.from(native.nativeZstdDecompress(data));
152
+ }
153
+ catch (e) {
154
+ // If decompression fails, try using raw data (might be uncompressed)
155
+ decompressed = data;
156
+ }
157
+ // Remove ROX1 prefix if present
158
+ if (decompressed.length >= 4 && decompressed.subarray(0, 4).toString() === 'ROX1') {
159
+ decompressed = decompressed.subarray(4);
160
+ }
161
+ // Try to unpack as multi-file archive
162
+ try {
163
+ const unpacked = unpackBuffer(decompressed);
164
+ if (unpacked && unpacked.files && unpacked.files.length > 0) {
165
+ // Return files directly as PackedFile[]
166
+ return { files: unpacked.files, meta: { name } };
63
167
  }
64
- return { buf: decompressed, meta: { name } };
65
168
  }
66
- return { buf: payload, meta: { name } };
169
+ catch {
170
+ // Not a multi-file archive, return as single buffer
171
+ }
172
+ return { buf: decompressed, meta: { name } };
67
173
  }
@@ -1,7 +1,36 @@
1
1
  import { EncodeOptions } from './types.js';
2
2
  /**
3
3
  * Encode a buffer or array of buffers into a PNG image (ROX format).
4
- * This function uses the Rust native implementation exclusively.
4
+ * This function uses the native Rust encoder directly.
5
+ *
6
+ * @example
7
+ * ```js
8
+ * import { readFileSync, writeFileSync } from 'fs';
9
+ * import { encodeBinaryToPng } from 'roxify';
10
+ *
11
+ * // Encode a file with a custom filename
12
+ * const input = readFileSync('config.json');
13
+ * const png = await encodeBinaryToPng(input, { name: 'config.json' });
14
+ * writeFileSync('config.png', png);
15
+ * ```
16
+ *
17
+ * @example
18
+ * ```js
19
+ * // Encode without filename
20
+ * const input = Buffer.from('Hello World');
21
+ * const png = await encodeBinaryToPng(input);
22
+ * ```
23
+ *
24
+ * @example
25
+ * ```js
26
+ * // Encode with encryption (AES)
27
+ * const input = readFileSync('secret.txt');
28
+ * const png = await encodeBinaryToPng(input, {
29
+ * name: 'secret.txt',
30
+ * passphrase: 'my-secret-key',
31
+ * encrypt: 'aes'
32
+ * });
33
+ * ```
5
34
  *
6
35
  * @param input - The buffer or array of buffers to encode.
7
36
  * @param opts - Optional encoding options.
@@ -1,31 +1,44 @@
1
1
  import { native } from './native.js';
2
- function normalizeNativeFileList(fileList) {
3
- if (!fileList)
4
- return '[]';
5
- return JSON.stringify(fileList.map((entry) => {
6
- if (typeof entry === 'string') {
7
- return { name: entry, size: 0 };
8
- }
9
- if (entry && typeof entry === 'object') {
10
- if (entry.name)
11
- return { name: entry.name, size: entry.size ?? 0 };
12
- if (entry.path)
13
- return { name: entry.path, size: entry.size ?? 0 };
14
- }
15
- return { name: String(entry), size: 0 };
16
- }));
17
- }
18
2
  /**
19
3
  * Encode a buffer or array of buffers into a PNG image (ROX format).
20
- * This function uses the Rust native implementation exclusively.
4
+ * This function uses the native Rust encoder directly.
5
+ *
6
+ * @example
7
+ * ```js
8
+ * import { readFileSync, writeFileSync } from 'fs';
9
+ * import { encodeBinaryToPng } from 'roxify';
10
+ *
11
+ * // Encode a file with a custom filename
12
+ * const input = readFileSync('config.json');
13
+ * const png = await encodeBinaryToPng(input, { name: 'config.json' });
14
+ * writeFileSync('config.png', png);
15
+ * ```
16
+ *
17
+ * @example
18
+ * ```js
19
+ * // Encode without filename
20
+ * const input = Buffer.from('Hello World');
21
+ * const png = await encodeBinaryToPng(input);
22
+ * ```
23
+ *
24
+ * @example
25
+ * ```js
26
+ * // Encode with encryption (AES)
27
+ * const input = readFileSync('secret.txt');
28
+ * const png = await encodeBinaryToPng(input, {
29
+ * name: 'secret.txt',
30
+ * passphrase: 'my-secret-key',
31
+ * encrypt: 'aes'
32
+ * });
33
+ * ```
21
34
  *
22
35
  * @param input - The buffer or array of buffers to encode.
23
36
  * @param opts - Optional encoding options.
24
37
  * @returns A Promise that resolves to a PNG Buffer containing the encoded data.
25
38
  */
26
39
  export async function encodeBinaryToPng(input, opts = {}) {
27
- const compressionLevel = opts.compressionLevel ?? 19;
28
40
  const inputBuf = Array.isArray(input) ? Buffer.concat(input) : input;
41
+ const compressionLevel = opts.compressionLevel ?? 3;
29
42
  const fileName = opts.name || undefined;
30
43
  const fileListJson = opts.includeFileList && opts.fileList
31
44
  ? normalizeNativeFileList(opts.fileList)
@@ -54,3 +67,6 @@ export async function encodeBinaryToPng(input, opts = {}) {
54
67
  }
55
68
  }
56
69
  }
70
+ function normalizeNativeFileList(fileList) {
71
+ return JSON.stringify(fileList);
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "roxify",
3
- "version": "1.13.6",
3
+ "version": "1.13.8",
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",
@@ -103,7 +103,7 @@
103
103
  "devDependencies": {
104
104
  "@types/node": "^22.0.0",
105
105
  "pkg": "^5.8.1",
106
- "typescript": "^5.6.0"
106
+ "typescript": "^5.9.3"
107
107
  },
108
108
  "engines": {
109
109
  "node": ">=18.0.0"
@@ -111,4 +111,4 @@
111
111
  "dependencies": {
112
112
  "pngjs": "^7.0.0"
113
113
  }
114
- }
114
+ }