roxify 1.13.4 → 1.13.5
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 +2 -1
- package/README.md +11 -0
- package/dist/rox-macos-universal +0 -0
- package/dist/roxify_native +0 -0
- package/dist/roxify_native-macos-arm64 +0 -0
- package/dist/roxify_native-macos-x64 +0 -0
- package/dist/roxify_native.exe +0 -0
- package/dist/utils/decoder.js +62 -58
- package/dist/utils/encoder.js +13 -6
- package/native/encoder.rs +10 -22
- package/native/io_advice.rs +43 -0
- package/native/lib.rs +2 -0
- package/native/main.rs +37 -25
- package/native/packer.rs +188 -82
- package/native/png_chunk_writer.rs +146 -0
- package/native/png_utils.rs +70 -54
- package/native/streaming.rs +16 -39
- package/native/streaming_decode.rs +258 -109
- package/native/streaming_encode.rs +22 -55
- package/package.json +1 -1
- package/roxify_native-aarch64-apple-darwin.node +0 -0
- package/roxify_native-aarch64-pc-windows-msvc.node +0 -0
- package/roxify_native-aarch64-unknown-linux-gnu.node +0 -0
- package/roxify_native-i686-pc-windows-msvc.node +0 -0
- package/roxify_native-i686-unknown-linux-gnu.node +0 -0
- package/roxify_native-x86_64-apple-darwin.node +0 -0
- package/roxify_native-x86_64-pc-windows-msvc.node +0 -0
- package/roxify_native-x86_64-unknown-linux-gnu.node +0 -0
package/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "roxify_native"
|
|
3
|
-
version = "1.13.
|
|
3
|
+
version = "1.13.5"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
publish = false
|
|
6
6
|
|
|
@@ -59,6 +59,7 @@ bytemuck = { version = "1.14", features = ["derive"] }
|
|
|
59
59
|
tokio = { version = "1", features = ["sync", "rt"], optional = true }
|
|
60
60
|
parking_lot = "0.12"
|
|
61
61
|
libsais = { version = "0.2.0", default-features = false }
|
|
62
|
+
libc = "0.2"
|
|
62
63
|
|
|
63
64
|
[features]
|
|
64
65
|
default = []
|
package/README.md
CHANGED
|
@@ -77,6 +77,17 @@ Roxify 1.13.4 adds adaptive parallel preload for small files before feeding Zstd
|
|
|
77
77
|
| Glados-Disc (19,645 files) | NTFS under Linux | 81.608 s | 2.189 s | 37.3x |
|
|
78
78
|
| Gmod (3,936 files) | NTFS under Linux | 22.578 s | 4.517 s | 5.0x |
|
|
79
79
|
|
|
80
|
+
### Portal 2 comparative reference: ZIP vs PNG
|
|
81
|
+
|
|
82
|
+
Measured on the full `Portal 2` game directory (`3,731 files`, `193 folders`, `12.83 GiB` logical source) to compare classic ZIP packaging against Roxify PNG packing on the same dataset.
|
|
83
|
+
|
|
84
|
+
| Format | Time (s) | Time (min:sec) | Throughput | Compression ratio |
|
|
85
|
+
| --- | ---: | --- | ---: | ---: |
|
|
86
|
+
| ZIP Encode | 633,87 | 10 min 33 s | 21,73 Mo/s | 36,08% |
|
|
87
|
+
| ZIP Decode | 232,88 | 3 min 52 s | 59,15 Mo/s | - |
|
|
88
|
+
| PNG Encode | 157,80 | 2 min 37 s | 87,30 Mo/s | 41,09% |
|
|
89
|
+
| PNG Decode | 156,00 | 2 min 36 s | 88,30 Mo/s | - |
|
|
90
|
+
|
|
80
91
|
### Data integrity
|
|
81
92
|
|
|
82
93
|
All benchmark runs completed with byte-exact roundtrip validation. Decode output matched original logical source bytes on every dataset.
|
package/dist/rox-macos-universal
CHANGED
|
Binary file
|
package/dist/roxify_native
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/roxify_native.exe
CHANGED
|
Binary file
|
package/dist/utils/decoder.js
CHANGED
|
@@ -12,9 +12,51 @@ import { cropAndReconstitute } from './reconstitution.js';
|
|
|
12
12
|
import { decodeRobustAudio, isRobustAudioWav } from './robust-audio.js';
|
|
13
13
|
import { decodeRobustImage, isRobustImage } from './robust-image.js';
|
|
14
14
|
import { parallelZstdDecompress } from './zstd.js';
|
|
15
|
+
const HEADER_VERSION_V1 = 1;
|
|
16
|
+
const HEADER_VERSION_V2 = 2;
|
|
15
17
|
function isColorMatch(r1, g1, b1, r2, g2, b2) {
|
|
16
18
|
return Math.abs(r1 - r2) + Math.abs(g1 - g2) + Math.abs(b1 - b2) < 50;
|
|
17
19
|
}
|
|
20
|
+
function readPayloadLength(buf, idx, version) {
|
|
21
|
+
if (version === HEADER_VERSION_V1) {
|
|
22
|
+
if (idx + 4 > buf.length) {
|
|
23
|
+
throw new DataFormatError('Truncated payload length');
|
|
24
|
+
}
|
|
25
|
+
return { payloadLen: buf.readUInt32BE(idx), nextIdx: idx + 4 };
|
|
26
|
+
}
|
|
27
|
+
if (version === HEADER_VERSION_V2) {
|
|
28
|
+
if (idx + 8 > buf.length) {
|
|
29
|
+
throw new DataFormatError('Truncated payload length');
|
|
30
|
+
}
|
|
31
|
+
const payloadLen = buf.readBigUInt64BE(idx);
|
|
32
|
+
if (payloadLen > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
33
|
+
throw new DataFormatError('Payload too large for JS decoder');
|
|
34
|
+
}
|
|
35
|
+
return { payloadLen: Number(payloadLen), nextIdx: idx + 8 };
|
|
36
|
+
}
|
|
37
|
+
throw new DataFormatError(`Unsupported pixel header version ${version}`);
|
|
38
|
+
}
|
|
39
|
+
function readPixelPayloadHeader(buf, idx) {
|
|
40
|
+
if (idx + 2 > buf.length) {
|
|
41
|
+
throw new DataFormatError('Pixel payload header truncated');
|
|
42
|
+
}
|
|
43
|
+
const version = buf[idx++];
|
|
44
|
+
const nameLen = buf[idx++];
|
|
45
|
+
let name;
|
|
46
|
+
if (nameLen > 0) {
|
|
47
|
+
if (idx + nameLen > buf.length) {
|
|
48
|
+
throw new DataFormatError('Pixel payload name truncated');
|
|
49
|
+
}
|
|
50
|
+
name = buf.subarray(idx, idx + nameLen).toString('utf8');
|
|
51
|
+
idx += nameLen;
|
|
52
|
+
}
|
|
53
|
+
const { payloadLen, nextIdx } = readPayloadLength(buf, idx, version);
|
|
54
|
+
const available = buf.length - nextIdx;
|
|
55
|
+
if (available < payloadLen) {
|
|
56
|
+
throw new DataFormatError(`Pixel payload truncated: expected ${payloadLen} bytes but only ${available} available`);
|
|
57
|
+
}
|
|
58
|
+
return { version, name, payloadOffset: nextIdx, payloadLen };
|
|
59
|
+
}
|
|
18
60
|
/**
|
|
19
61
|
* Un-stretch an image that was nearest-neighbor scaled.
|
|
20
62
|
* 1. Crops to non-background bounding box
|
|
@@ -318,18 +360,12 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
318
360
|
const pcmData = wavToBytes(processedBuf);
|
|
319
361
|
// The WAV payload starts with PIXEL_MAGIC ("PXL1")
|
|
320
362
|
if (pcmData.length >= 4 && pcmData.subarray(0, 4).equals(PIXEL_MAGIC)) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
idx += nameLen;
|
|
328
|
-
}
|
|
329
|
-
const payloadLen = pcmData.readUInt32BE(idx);
|
|
330
|
-
idx += 4;
|
|
331
|
-
const rawPayload = pcmData.subarray(idx, idx + payloadLen);
|
|
332
|
-
idx += payloadLen;
|
|
363
|
+
const header = readPixelPayloadHeader(pcmData, 4);
|
|
364
|
+
let idx = header.payloadOffset;
|
|
365
|
+
const version = header.version;
|
|
366
|
+
const name = header.name;
|
|
367
|
+
const rawPayload = pcmData.subarray(idx, idx + header.payloadLen);
|
|
368
|
+
idx += header.payloadLen;
|
|
333
369
|
// Check for rXFL file list after payload
|
|
334
370
|
let fileListJson;
|
|
335
371
|
if (idx + 8 < pcmData.length &&
|
|
@@ -652,21 +688,10 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
652
688
|
if (logicalData.length < 8 + PIXEL_MAGIC.length) {
|
|
653
689
|
throw new DataFormatError('Pixel mode data too short');
|
|
654
690
|
}
|
|
655
|
-
|
|
656
|
-
const version =
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
if (nameLen > 0 && nameLen < 256) {
|
|
660
|
-
name = logicalData.slice(idx, idx + nameLen).toString('utf8');
|
|
661
|
-
idx += nameLen;
|
|
662
|
-
}
|
|
663
|
-
const payloadLen = logicalData.readUInt32BE(idx);
|
|
664
|
-
idx += 4;
|
|
665
|
-
const available = logicalData.length - idx;
|
|
666
|
-
if (available < payloadLen) {
|
|
667
|
-
throw new DataFormatError(`Pixel payload truncated: expected ${payloadLen} bytes but only ${available} available`);
|
|
668
|
-
}
|
|
669
|
-
const rawPayload = logicalData.slice(idx, idx + payloadLen);
|
|
691
|
+
const header = readPixelPayloadHeader(logicalData, 8 + PIXEL_MAGIC.length);
|
|
692
|
+
const version = header.version;
|
|
693
|
+
const name = header.name;
|
|
694
|
+
const rawPayload = logicalData.slice(header.payloadOffset, header.payloadOffset + header.payloadLen);
|
|
670
695
|
let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
|
|
671
696
|
try {
|
|
672
697
|
payload = await tryDecompress(payload, (info) => {
|
|
@@ -945,31 +970,10 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
945
970
|
}
|
|
946
971
|
}
|
|
947
972
|
if (idx > 0) {
|
|
948
|
-
const
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
name = pixelBytes.slice(idx, idx + nameLen).toString('utf8');
|
|
953
|
-
idx += nameLen;
|
|
954
|
-
}
|
|
955
|
-
const payloadLen = pixelBytes.readUInt32BE(idx);
|
|
956
|
-
idx += 4;
|
|
957
|
-
if (idx + 4 <= pixelBytes.length) {
|
|
958
|
-
const marker = pixelBytes.slice(idx, idx + 4).toString('utf8');
|
|
959
|
-
if (marker === 'rXFL') {
|
|
960
|
-
idx += 4;
|
|
961
|
-
if (idx + 4 <= pixelBytes.length) {
|
|
962
|
-
const jsonLen = pixelBytes.readUInt32BE(idx);
|
|
963
|
-
idx += 4;
|
|
964
|
-
idx += jsonLen;
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
const available = pixelBytes.length - idx;
|
|
969
|
-
if (available < payloadLen) {
|
|
970
|
-
throw new DataFormatError(`Pixel payload truncated: expected ${payloadLen} bytes but only ${available} available`);
|
|
971
|
-
}
|
|
972
|
-
const rawPayload = pixelBytes.slice(idx, idx + payloadLen);
|
|
973
|
+
const header = readPixelPayloadHeader(pixelBytes, idx);
|
|
974
|
+
const version = header.version;
|
|
975
|
+
const name = header.name;
|
|
976
|
+
const rawPayload = pixelBytes.slice(header.payloadOffset, header.payloadOffset + header.payloadLen);
|
|
973
977
|
let payload = tryDecryptIfNeeded(rawPayload, opts.passphrase);
|
|
974
978
|
try {
|
|
975
979
|
payload = await tryDecompress(payload, (info) => {
|
|
@@ -1162,10 +1166,10 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
1162
1166
|
ii = found + PIXEL_MAGIC.length;
|
|
1163
1167
|
}
|
|
1164
1168
|
if (ii > 0) {
|
|
1165
|
-
const
|
|
1166
|
-
const
|
|
1167
|
-
const
|
|
1168
|
-
const rawPayload2 = pixelBytes2.slice(
|
|
1169
|
+
const header2 = readPixelPayloadHeader(pixelBytes2, ii);
|
|
1170
|
+
const version2 = header2.version;
|
|
1171
|
+
const name2 = header2.name;
|
|
1172
|
+
const rawPayload2 = pixelBytes2.slice(header2.payloadOffset, header2.payloadOffset + header2.payloadLen);
|
|
1169
1173
|
let payload2 = tryDecryptIfNeeded(rawPayload2, opts.passphrase);
|
|
1170
1174
|
payload2 = await tryDecompress(payload2, (info) => {
|
|
1171
1175
|
if (opts.onProgress)
|
|
@@ -1181,13 +1185,13 @@ export async function decodePngToBinary(input, opts = {}) {
|
|
|
1181
1185
|
if (opts.onProgress)
|
|
1182
1186
|
opts.onProgress({ phase: 'done' });
|
|
1183
1187
|
progressBar?.stop();
|
|
1184
|
-
return { files: unpacked2.files, meta: { name } };
|
|
1188
|
+
return { files: unpacked2.files, meta: { name: name2 } };
|
|
1185
1189
|
}
|
|
1186
1190
|
}
|
|
1187
1191
|
if (opts.onProgress)
|
|
1188
1192
|
opts.onProgress({ phase: 'done' });
|
|
1189
1193
|
progressBar?.stop();
|
|
1190
|
-
return { buf: payload2, meta: { name } };
|
|
1194
|
+
return { buf: payload2, meta: { name: name2 } };
|
|
1191
1195
|
}
|
|
1192
1196
|
}
|
|
1193
1197
|
throw new DataFormatError('Screenshot mode zstd decompression failed: ' + errMsg);
|
package/dist/utils/encoder.js
CHANGED
|
@@ -9,6 +9,15 @@ import { native } from './native.js';
|
|
|
9
9
|
import { encodeRobustAudio } from './robust-audio.js';
|
|
10
10
|
import { encodeRobustImage } from './robust-image.js';
|
|
11
11
|
import { parallelZstdCompress } from './zstd.js';
|
|
12
|
+
const HEADER_VERSION_V2 = 2;
|
|
13
|
+
function writePayloadLength(payloadLen) {
|
|
14
|
+
if (!Number.isSafeInteger(payloadLen) || payloadLen < 0) {
|
|
15
|
+
throw new RangeError(`Invalid payload length: ${payloadLen}`);
|
|
16
|
+
}
|
|
17
|
+
const buf = Buffer.alloc(8);
|
|
18
|
+
buf.writeBigUInt64BE(BigInt(payloadLen), 0);
|
|
19
|
+
return buf;
|
|
20
|
+
}
|
|
12
21
|
/**
|
|
13
22
|
* Encode a buffer or array of buffers into a PNG image (ROX format).
|
|
14
23
|
*
|
|
@@ -309,9 +318,8 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
309
318
|
if (opts.container === 'sound') {
|
|
310
319
|
const nameBuf = opts.name ? Buffer.from(opts.name, 'utf8') : Buffer.alloc(0);
|
|
311
320
|
const nameLen = nameBuf.length;
|
|
312
|
-
const payloadLenBuf =
|
|
313
|
-
|
|
314
|
-
const version = 1;
|
|
321
|
+
const payloadLenBuf = writePayloadLength(payloadTotalLen);
|
|
322
|
+
const version = HEADER_VERSION_V2;
|
|
315
323
|
let wavPayload = [
|
|
316
324
|
PIXEL_MAGIC,
|
|
317
325
|
Buffer.from([version]),
|
|
@@ -357,9 +365,8 @@ export async function encodeBinaryToPng(input, opts = {}) {
|
|
|
357
365
|
{
|
|
358
366
|
const nameBuf = opts.name ? Buffer.from(opts.name, 'utf8') : Buffer.alloc(0);
|
|
359
367
|
const nameLen = nameBuf.length;
|
|
360
|
-
const payloadLenBuf =
|
|
361
|
-
|
|
362
|
-
const version = 1;
|
|
368
|
+
const payloadLenBuf = writePayloadLength(payloadTotalLen);
|
|
369
|
+
const version = HEADER_VERSION_V2;
|
|
363
370
|
let metaPixel = [
|
|
364
371
|
Buffer.from([version]),
|
|
365
372
|
Buffer.from([nameLen]),
|
package/native/encoder.rs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
use anyhow::Result;
|
|
2
2
|
|
|
3
|
+
use crate::png_chunk_writer::{write_chunked_idat_bytes, write_png_chunk};
|
|
4
|
+
|
|
3
5
|
const MAGIC: &[u8] = b"ROX1";
|
|
4
6
|
const PIXEL_MAGIC: &[u8] = b"PXL1";
|
|
5
7
|
const PNG_HEADER: &[u8] = &[137, 80, 78, 71, 13, 10, 26, 10];
|
|
8
|
+
const HEADER_VERSION_V2: u8 = 2;
|
|
6
9
|
|
|
7
10
|
const MARKER_START: [(u8, u8, u8); 3] = [(255, 0, 0), (0, 255, 0), (0, 0, 255)];
|
|
8
11
|
const MARKER_END: [(u8, u8, u8); 3] = [(0, 0, 255), (0, 255, 0), (255, 0, 0)];
|
|
@@ -204,12 +207,12 @@ fn build_flat_pixel_buffer(
|
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
fn build_meta_pixel_with_name_and_filelist(payload: &[u8], name: Option<&str>, file_list: Option<&str>) -> Result<Vec<u8>> {
|
|
207
|
-
let version =
|
|
210
|
+
let version = HEADER_VERSION_V2;
|
|
208
211
|
let name_bytes = name.map(|n| n.as_bytes()).unwrap_or(&[]);
|
|
209
212
|
let name_len = name_bytes.len().min(255) as u8;
|
|
210
|
-
let payload_len_bytes = (payload.len() as
|
|
213
|
+
let payload_len_bytes = (payload.len() as u64).to_be_bytes();
|
|
211
214
|
|
|
212
|
-
let mut result = Vec::with_capacity(1 + 1 + name_len as usize +
|
|
215
|
+
let mut result = Vec::with_capacity(1 + 1 + name_len as usize + 8 + payload.len() + 256);
|
|
213
216
|
result.push(version);
|
|
214
217
|
result.push(name_len);
|
|
215
218
|
|
|
@@ -245,33 +248,18 @@ fn build_png(width: usize, height: usize, idat_data: &[u8], file_list: Option<&s
|
|
|
245
248
|
ihdr_data[11] = 0;
|
|
246
249
|
ihdr_data[12] = 0;
|
|
247
250
|
|
|
248
|
-
|
|
249
|
-
|
|
251
|
+
write_png_chunk(&mut png, b"IHDR", &ihdr_data)?;
|
|
252
|
+
write_chunked_idat_bytes(&mut png, idat_data)?;
|
|
250
253
|
|
|
251
254
|
if let Some(file_list_json) = file_list {
|
|
252
|
-
|
|
255
|
+
write_png_chunk(&mut png, b"rXFL", file_list_json.as_bytes())?;
|
|
253
256
|
}
|
|
254
257
|
|
|
255
|
-
|
|
258
|
+
write_png_chunk(&mut png, b"IEND", &[])?;
|
|
256
259
|
|
|
257
260
|
Ok(png)
|
|
258
261
|
}
|
|
259
262
|
|
|
260
|
-
fn write_chunk(out: &mut Vec<u8>, chunk_type: &[u8; 4], data: &[u8]) -> Result<()> {
|
|
261
|
-
let len = data.len() as u32;
|
|
262
|
-
out.extend_from_slice(&len.to_be_bytes());
|
|
263
|
-
out.extend_from_slice(chunk_type);
|
|
264
|
-
out.extend_from_slice(data);
|
|
265
|
-
|
|
266
|
-
let mut hasher = crc32fast::Hasher::new();
|
|
267
|
-
hasher.update(chunk_type);
|
|
268
|
-
hasher.update(data);
|
|
269
|
-
let crc = hasher.finalize();
|
|
270
|
-
|
|
271
|
-
out.extend_from_slice(&crc.to_be_bytes());
|
|
272
|
-
Ok(())
|
|
273
|
-
}
|
|
274
|
-
|
|
275
263
|
fn create_raw_deflate_from_rows(flat: &[u8], row_bytes: usize, height: usize) -> Vec<u8> {
|
|
276
264
|
let stride = row_bytes + 1;
|
|
277
265
|
let scanlines_total = height * stride;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
use std::fs::File;
|
|
2
|
+
#[cfg(target_os = "linux")]
|
|
3
|
+
use std::os::fd::AsRawFd;
|
|
4
|
+
|
|
5
|
+
pub const INPUT_DROP_GRANULARITY: u64 = 8 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
pub fn advise_file_sequential(file: &File) {
|
|
8
|
+
#[cfg(target_os = "linux")]
|
|
9
|
+
unsafe {
|
|
10
|
+
let _ = libc::posix_fadvise(file.as_raw_fd(), 0, 0, libc::POSIX_FADV_SEQUENTIAL);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#[cfg(not(target_os = "linux"))]
|
|
14
|
+
let _ = file;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
pub fn advise_drop(file: &File, offset: u64, len: u64) {
|
|
18
|
+
if len == 0 {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#[cfg(target_os = "linux")]
|
|
23
|
+
unsafe {
|
|
24
|
+
let _ = libc::posix_fadvise(
|
|
25
|
+
file.as_raw_fd(),
|
|
26
|
+
offset as libc::off_t,
|
|
27
|
+
len as libc::off_t,
|
|
28
|
+
libc::POSIX_FADV_DONTNEED,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[cfg(not(target_os = "linux"))]
|
|
33
|
+
let _ = (file, offset, len);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub fn sync_and_drop(file: &File, len: u64) {
|
|
37
|
+
if len == 0 {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let _ = file.sync_data();
|
|
42
|
+
advise_drop(file, 0, len);
|
|
43
|
+
}
|
package/native/lib.rs
CHANGED
package/native/main.rs
CHANGED
|
@@ -11,6 +11,8 @@ mod encoder;
|
|
|
11
11
|
mod packer;
|
|
12
12
|
mod crypto;
|
|
13
13
|
mod png_utils;
|
|
14
|
+
mod png_chunk_writer;
|
|
15
|
+
mod io_advice;
|
|
14
16
|
mod audio;
|
|
15
17
|
mod reconstitution;
|
|
16
18
|
mod archive;
|
|
@@ -255,32 +257,39 @@ fn parse_requested_files(files: &str) -> anyhow::Result<Vec<String>> {
|
|
|
255
257
|
}
|
|
256
258
|
Commands::List { input } => {
|
|
257
259
|
let mut file = File::open(&input)?;
|
|
258
|
-
let
|
|
260
|
+
let mut chunk_scan_error: Option<anyhow::Error> = None;
|
|
259
261
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
match png_utils::extract_png_chunks_streaming(&mut file) {
|
|
263
|
+
Ok(chunks) => {
|
|
264
|
+
if let Some(rxfl_chunk) = chunks.iter().find(|c| c.name == "rXFL") {
|
|
265
|
+
println!("{}", String::from_utf8_lossy(&rxfl_chunk.data));
|
|
266
|
+
return Ok(());
|
|
267
|
+
}
|
|
264
268
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
269
|
+
if let Some(meta_chunk) = chunks.iter().find(|c| c.name == "rOXm") {
|
|
270
|
+
if let Some(pos) = meta_chunk.data.windows(4).position(|w| w == b"rXFL") {
|
|
271
|
+
if pos + 8 <= meta_chunk.data.len() {
|
|
272
|
+
let json_len = u32::from_be_bytes([
|
|
273
|
+
meta_chunk.data[pos + 4],
|
|
274
|
+
meta_chunk.data[pos + 5],
|
|
275
|
+
meta_chunk.data[pos + 6],
|
|
276
|
+
meta_chunk.data[pos + 7],
|
|
277
|
+
]) as usize;
|
|
278
|
+
|
|
279
|
+
let json_start = pos + 8;
|
|
280
|
+
let json_end = json_start + json_len;
|
|
281
|
+
|
|
282
|
+
if json_end <= meta_chunk.data.len() {
|
|
283
|
+
println!("{}", String::from_utf8_lossy(&meta_chunk.data[json_start..json_end]));
|
|
284
|
+
return Ok(());
|
|
285
|
+
}
|
|
286
|
+
}
|
|
281
287
|
}
|
|
282
288
|
}
|
|
283
289
|
}
|
|
290
|
+
Err(err) => {
|
|
291
|
+
chunk_scan_error = Some(anyhow::anyhow!(err));
|
|
292
|
+
}
|
|
284
293
|
}
|
|
285
294
|
|
|
286
295
|
let png_data = std::fs::read(&input)?;
|
|
@@ -289,11 +298,14 @@ fn parse_requested_files(files: &str) -> anyhow::Result<Vec<String>> {
|
|
|
289
298
|
println!("{}", json);
|
|
290
299
|
return Ok(());
|
|
291
300
|
}
|
|
292
|
-
Err(
|
|
301
|
+
Err(pixel_err) => {
|
|
302
|
+
if let Some(chunk_err) = chunk_scan_error {
|
|
303
|
+
return Err(anyhow::anyhow!("chunk scan: {}; pixel scan: {}", chunk_err, pixel_err));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
293
306
|
}
|
|
294
307
|
|
|
295
|
-
|
|
296
|
-
std::process::exit(1);
|
|
308
|
+
return Err(anyhow::anyhow!("No file list found in PNG"));
|
|
297
309
|
}
|
|
298
310
|
Commands::Havepassphrase { input } => {
|
|
299
311
|
let buf = read_all(&input)?;
|
|
@@ -377,7 +389,7 @@ fn parse_requested_files(files: &str) -> anyhow::Result<Vec<String>> {
|
|
|
377
389
|
return Ok(());
|
|
378
390
|
}
|
|
379
391
|
Err(e) => {
|
|
380
|
-
|
|
392
|
+
return Err(anyhow::anyhow!("Streaming decode failed: {}", e));
|
|
381
393
|
}
|
|
382
394
|
}
|
|
383
395
|
}
|