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/native/streaming.rs
CHANGED
|
@@ -2,12 +2,15 @@ use std::io::{Write, BufWriter};
|
|
|
2
2
|
use std::fs::File;
|
|
3
3
|
use std::path::Path;
|
|
4
4
|
|
|
5
|
+
use crate::png_chunk_writer::{ChunkedIdatWriter, write_png_chunk};
|
|
6
|
+
|
|
5
7
|
const PNG_HEADER: &[u8] = &[137, 80, 78, 71, 13, 10, 26, 10];
|
|
6
8
|
const PIXEL_MAGIC: &[u8] = b"PXL1";
|
|
7
9
|
const MARKER_START: [(u8, u8, u8); 3] = [(255, 0, 0), (0, 255, 0), (0, 0, 255)];
|
|
8
10
|
const MARKER_END: [(u8, u8, u8); 3] = [(0, 0, 255), (0, 255, 0), (255, 0, 0)];
|
|
9
11
|
const MARKER_ZSTD: (u8, u8, u8) = (0, 255, 0);
|
|
10
12
|
const MAGIC: &[u8] = b"ROX1";
|
|
13
|
+
const HEADER_VERSION_V2: u8 = 2;
|
|
11
14
|
|
|
12
15
|
pub fn encode_to_png_file(
|
|
13
16
|
data: &[u8],
|
|
@@ -75,10 +78,6 @@ pub fn encode_to_png_file(
|
|
|
75
78
|
|
|
76
79
|
let adler = crate::core::adler32_bytes(&scanlines);
|
|
77
80
|
|
|
78
|
-
const MAX_BLOCK: usize = 65535;
|
|
79
|
-
let num_blocks = (scanlines_total + MAX_BLOCK - 1) / MAX_BLOCK;
|
|
80
|
-
let idat_len = 2 + num_blocks * 5 + scanlines_total + 4;
|
|
81
|
-
|
|
82
81
|
let f = File::create(output_path)?;
|
|
83
82
|
let mut w = BufWriter::with_capacity(16 * 1024 * 1024, f);
|
|
84
83
|
|
|
@@ -89,15 +88,15 @@ pub fn encode_to_png_file(
|
|
|
89
88
|
ihdr[4..8].copy_from_slice(&(height as u32).to_be_bytes());
|
|
90
89
|
ihdr[8] = 8;
|
|
91
90
|
ihdr[9] = 2;
|
|
92
|
-
|
|
91
|
+
write_png_chunk(&mut w, b"IHDR", &ihdr)?;
|
|
93
92
|
|
|
94
|
-
write_idat_direct(&mut w, &scanlines,
|
|
93
|
+
write_idat_direct(&mut w, &scanlines, adler)?;
|
|
95
94
|
drop(scanlines);
|
|
96
95
|
|
|
97
96
|
if let Some(fl) = file_list {
|
|
98
|
-
|
|
97
|
+
write_png_chunk(&mut w, b"rXFL", fl.as_bytes())?;
|
|
99
98
|
}
|
|
100
|
-
|
|
99
|
+
write_png_chunk(&mut w, b"IEND", &[])?;
|
|
101
100
|
w.flush()?;
|
|
102
101
|
|
|
103
102
|
Ok(())
|
|
@@ -106,20 +105,14 @@ pub fn encode_to_png_file(
|
|
|
106
105
|
fn write_idat_direct<W: Write>(
|
|
107
106
|
w: &mut W,
|
|
108
107
|
scanlines: &[u8],
|
|
109
|
-
idat_len: usize,
|
|
110
108
|
adler: u32,
|
|
111
109
|
) -> anyhow::Result<()> {
|
|
112
110
|
const MAX_BLOCK: usize = 65535;
|
|
113
111
|
|
|
114
|
-
|
|
115
|
-
w.write_all(b"IDAT")?;
|
|
116
|
-
|
|
117
|
-
let mut crc = crc32fast::Hasher::new();
|
|
118
|
-
crc.update(b"IDAT");
|
|
112
|
+
let mut idat = ChunkedIdatWriter::new(w);
|
|
119
113
|
|
|
120
114
|
let zlib = [0x78u8, 0x01];
|
|
121
|
-
|
|
122
|
-
crc.update(&zlib);
|
|
115
|
+
idat.write_all(&zlib)?;
|
|
123
116
|
|
|
124
117
|
let mut offset = 0;
|
|
125
118
|
while offset < scanlines.len() {
|
|
@@ -132,20 +125,15 @@ fn write_idat_direct<W: Write>(
|
|
|
132
125
|
!chunk_size as u8,
|
|
133
126
|
(!(chunk_size >> 8)) as u8,
|
|
134
127
|
];
|
|
135
|
-
|
|
136
|
-
crc.update(&header);
|
|
128
|
+
idat.write_all(&header)?;
|
|
137
129
|
let slice = &scanlines[offset..offset + chunk_size];
|
|
138
|
-
|
|
139
|
-
crc.update(slice);
|
|
130
|
+
idat.write_all(slice)?;
|
|
140
131
|
offset += chunk_size;
|
|
141
132
|
}
|
|
142
133
|
|
|
143
134
|
let adler_bytes = adler.to_be_bytes();
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
w.write_all(&crc.finalize().to_be_bytes())?;
|
|
148
|
-
Ok(())
|
|
135
|
+
idat.write_all(&adler_bytes)?;
|
|
136
|
+
idat.finish()
|
|
149
137
|
}
|
|
150
138
|
|
|
151
139
|
fn build_flat_buffer(
|
|
@@ -175,12 +163,12 @@ fn build_flat_buffer(
|
|
|
175
163
|
}
|
|
176
164
|
|
|
177
165
|
fn build_meta_pixel(payload: &[u8], name: Option<&str>, file_list: Option<&str>) -> anyhow::Result<Vec<u8>> {
|
|
178
|
-
let version =
|
|
166
|
+
let version = HEADER_VERSION_V2;
|
|
179
167
|
let name_bytes = name.map(|n| n.as_bytes()).unwrap_or(&[]);
|
|
180
168
|
let name_len = name_bytes.len().min(255) as u8;
|
|
181
|
-
let payload_len_bytes = (payload.len() as
|
|
169
|
+
let payload_len_bytes = (payload.len() as u64).to_be_bytes();
|
|
182
170
|
|
|
183
|
-
let mut result = Vec::with_capacity(1 + 1 + name_len as usize +
|
|
171
|
+
let mut result = Vec::with_capacity(1 + 1 + name_len as usize + 8 + payload.len() + 256);
|
|
184
172
|
result.push(version);
|
|
185
173
|
result.push(name_len);
|
|
186
174
|
if name_len > 0 {
|
|
@@ -199,14 +187,3 @@ fn build_meta_pixel(payload: &[u8], name: Option<&str>, file_list: Option<&str>)
|
|
|
199
187
|
Ok(result)
|
|
200
188
|
}
|
|
201
189
|
|
|
202
|
-
fn write_chunk_small<W: Write>(w: &mut W, chunk_type: &[u8; 4], data: &[u8]) -> anyhow::Result<()> {
|
|
203
|
-
w.write_all(&(data.len() as u32).to_be_bytes())?;
|
|
204
|
-
w.write_all(chunk_type)?;
|
|
205
|
-
w.write_all(data)?;
|
|
206
|
-
|
|
207
|
-
let mut h = crc32fast::Hasher::new();
|
|
208
|
-
h.update(chunk_type);
|
|
209
|
-
h.update(data);
|
|
210
|
-
w.write_all(&h.finalize().to_be_bytes())?;
|
|
211
|
-
Ok(())
|
|
212
|
-
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
use std::
|
|
1
|
+
use std::fs::File;
|
|
2
|
+
use std::io::{Read, Seek, SeekFrom};
|
|
2
3
|
use std::path::Path;
|
|
3
4
|
use cipher::{KeyIvInit, StreamCipher};
|
|
4
5
|
|
|
5
6
|
const PIXEL_MAGIC: &[u8] = b"PXL1";
|
|
6
7
|
const MARKER_BYTES: usize = 12;
|
|
7
8
|
const PACK_MAGIC: [u8; 4] = 0x524f5850u32.to_be_bytes();
|
|
9
|
+
const HEADER_VERSION_V1: u8 = 1;
|
|
10
|
+
const HEADER_VERSION_V2: u8 = 2;
|
|
8
11
|
|
|
9
12
|
type Aes256Ctr = ctr::Ctr64BE<aes::Aes256>;
|
|
10
13
|
|
|
@@ -38,26 +41,20 @@ pub fn streaming_decode_selected_to_dir_encrypted_with_progress(
|
|
|
38
41
|
passphrase: Option<&str>,
|
|
39
42
|
progress: Option<DecodeProgressCallback>,
|
|
40
43
|
) -> Result<Vec<String>, String> {
|
|
41
|
-
let
|
|
42
|
-
let
|
|
43
|
-
let data = &mmap[..];
|
|
44
|
-
|
|
45
|
-
if data.len() < 8 || &data[0..8] != &[137, 80, 78, 71, 13, 10, 26, 10] {
|
|
46
|
-
return Err("Not a PNG file".into());
|
|
47
|
-
}
|
|
44
|
+
let mut meta_file = File::open(png_path).map_err(|e| format!("open: {}", e))?;
|
|
45
|
+
let (width, height, idat_ranges, total_expected) = parse_png_metadata(&mut meta_file)?;
|
|
48
46
|
|
|
49
47
|
if let Some(ref cb) = progress {
|
|
50
48
|
cb(2, 100, "parsing_png");
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
let (width, height, idat_data_start, idat_data_end) = parse_png_header(data)?;
|
|
54
|
-
let total_expected = parse_rxfl_total_bytes(data).unwrap_or(0);
|
|
55
|
-
|
|
56
51
|
if let Some(ref cb) = progress {
|
|
57
52
|
cb(5, 100, "reading_header");
|
|
58
53
|
}
|
|
59
54
|
|
|
60
|
-
let
|
|
55
|
+
let data_file = File::open(png_path).map_err(|e| format!("open data: {}", e))?;
|
|
56
|
+
let mut reader = DeflatePixelReader::new(data_file, width, height, idat_ranges)
|
|
57
|
+
.map_err(|e| format!("init deflate reader: {}", e))?;
|
|
61
58
|
|
|
62
59
|
let mut marker_buf = [0u8; MARKER_BYTES];
|
|
63
60
|
reader.read_exact(&mut marker_buf).map_err(|e| format!("read markers: {}", e))?;
|
|
@@ -78,25 +75,22 @@ pub fn streaming_decode_selected_to_dir_encrypted_with_progress(
|
|
|
78
75
|
reader.read_exact(&mut name_buf).map_err(|e| format!("read name: {}", e))?;
|
|
79
76
|
}
|
|
80
77
|
|
|
81
|
-
let mut
|
|
82
|
-
reader.read_exact(&mut plen_buf).map_err(|e| format!("read payload_len: {}", e))?;
|
|
83
|
-
let payload_len = u32::from_be_bytes(plen_buf) as u64;
|
|
78
|
+
let payload_len = read_payload_len(&mut reader, hdr[0])?;
|
|
84
79
|
|
|
85
80
|
if let Some(ref cb) = progress {
|
|
86
81
|
cb(8, 100, "decrypting");
|
|
87
82
|
}
|
|
88
83
|
|
|
89
|
-
let
|
|
90
|
-
|
|
91
|
-
let
|
|
92
|
-
let (enc_byte, remaining_reader) = first_byte_reader.into_parts()?;
|
|
84
|
+
let mut enc_byte = [0u8; 1];
|
|
85
|
+
reader.read_exact(&mut enc_byte).map_err(|e| format!("read first byte: {}", e))?;
|
|
86
|
+
let remaining_payload_len = payload_len.saturating_sub(1);
|
|
93
87
|
|
|
94
|
-
match enc_byte {
|
|
88
|
+
match enc_byte[0] {
|
|
95
89
|
0x00 => {
|
|
96
90
|
if let Some(ref cb) = progress {
|
|
97
91
|
cb(10, 100, "decompressing");
|
|
98
92
|
}
|
|
99
|
-
let mut decoder = zstd::stream::Decoder::new(
|
|
93
|
+
let mut decoder = zstd::stream::Decoder::new(reader)
|
|
100
94
|
.map_err(|e| format!("zstd decoder: {}", e))?;
|
|
101
95
|
decoder.window_log_max(31).map_err(|e| format!("zstd window_log_max: {}", e))?;
|
|
102
96
|
read_rox1_and_unpack_with_progress(decoder, out_dir, files_opt, progress, total_expected)
|
|
@@ -105,7 +99,7 @@ pub fn streaming_decode_selected_to_dir_encrypted_with_progress(
|
|
|
105
99
|
let pass = passphrase.ok_or("Passphrase required for AES-CTR decryption")?;
|
|
106
100
|
let mut salt = [0u8; 16];
|
|
107
101
|
let mut iv = [0u8; 16];
|
|
108
|
-
let mut r =
|
|
102
|
+
let mut r = reader.take(remaining_payload_len);
|
|
109
103
|
r.read_exact(&mut salt).map_err(|e| format!("read salt: {}", e))?;
|
|
110
104
|
r.read_exact(&mut iv).map_err(|e| format!("read iv: {}", e))?;
|
|
111
105
|
|
|
@@ -114,7 +108,7 @@ pub fn streaming_decode_selected_to_dir_encrypted_with_progress(
|
|
|
114
108
|
.map_err(|e| format!("AES-CTR init: {}", e))?;
|
|
115
109
|
|
|
116
110
|
let hmac_size = 32u64;
|
|
117
|
-
let encrypted_data_len =
|
|
111
|
+
let encrypted_data_len = remaining_payload_len - 16 - 16 - hmac_size;
|
|
118
112
|
let ctr_reader = CtrDecryptReader::new(r.take(encrypted_data_len), cipher);
|
|
119
113
|
|
|
120
114
|
if let Some(ref cb) = progress {
|
|
@@ -125,7 +119,23 @@ pub fn streaming_decode_selected_to_dir_encrypted_with_progress(
|
|
|
125
119
|
decoder.window_log_max(31).map_err(|e| format!("zstd window_log_max: {}", e))?;
|
|
126
120
|
read_rox1_and_unpack_with_progress(decoder, out_dir, files_opt, progress, total_expected)
|
|
127
121
|
}
|
|
128
|
-
_ => Err(format!("Unsupported encryption (enc=0x{:02x}) in streaming decode", enc_byte)),
|
|
122
|
+
_ => Err(format!("Unsupported encryption (enc=0x{:02x}) in streaming decode", enc_byte[0])),
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fn read_payload_len<R: Read>(reader: &mut R, version: u8) -> Result<u64, String> {
|
|
127
|
+
match version {
|
|
128
|
+
HEADER_VERSION_V1 => {
|
|
129
|
+
let mut plen_buf = [0u8; 4];
|
|
130
|
+
reader.read_exact(&mut plen_buf).map_err(|e| format!("read payload_len: {}", e))?;
|
|
131
|
+
Ok(u32::from_be_bytes(plen_buf) as u64)
|
|
132
|
+
}
|
|
133
|
+
HEADER_VERSION_V2 => {
|
|
134
|
+
let mut plen_buf = [0u8; 8];
|
|
135
|
+
reader.read_exact(&mut plen_buf).map_err(|e| format!("read payload_len64: {}", e))?;
|
|
136
|
+
Ok(u64::from_be_bytes(plen_buf))
|
|
137
|
+
}
|
|
138
|
+
other => Err(format!("Unsupported header version {}", other)),
|
|
129
139
|
}
|
|
130
140
|
}
|
|
131
141
|
|
|
@@ -155,84 +165,88 @@ fn read_rox1_and_unpack_with_progress<R: Read>(
|
|
|
155
165
|
}
|
|
156
166
|
}
|
|
157
167
|
|
|
158
|
-
fn
|
|
159
|
-
let mut
|
|
168
|
+
fn parse_png_metadata(file: &mut File) -> Result<(usize, usize, Vec<(u64, u64)>, u64), String> {
|
|
169
|
+
let mut sig = [0u8; 8];
|
|
170
|
+
file.read_exact(&mut sig).map_err(|e| format!("read sig: {}", e))?;
|
|
171
|
+
if sig != [137, 80, 78, 71, 13, 10, 26, 10] {
|
|
172
|
+
return Err("Not a PNG file".into());
|
|
173
|
+
}
|
|
160
174
|
|
|
161
175
|
let mut width = 0usize;
|
|
162
176
|
let mut height = 0usize;
|
|
163
|
-
let mut
|
|
164
|
-
let mut
|
|
177
|
+
let mut idat_ranges = Vec::new();
|
|
178
|
+
let mut total_expected = 0u64;
|
|
179
|
+
|
|
180
|
+
loop {
|
|
181
|
+
let mut header = [0u8; 8];
|
|
182
|
+
match file.read_exact(&mut header) {
|
|
183
|
+
Ok(()) => {}
|
|
184
|
+
Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => break,
|
|
185
|
+
Err(err) => return Err(format!("read chunk header: {}", err)),
|
|
186
|
+
}
|
|
165
187
|
|
|
166
|
-
|
|
167
|
-
let
|
|
168
|
-
let
|
|
169
|
-
let
|
|
188
|
+
let chunk_len = u32::from_be_bytes([header[0], header[1], header[2], header[3]]) as u64;
|
|
189
|
+
let chunk_type = &header[4..8];
|
|
190
|
+
let chunk_data_start = file.stream_position().map_err(|e| format!("stream position: {}", e))?;
|
|
191
|
+
let chunk_data_end = chunk_data_start
|
|
192
|
+
.checked_add(chunk_len)
|
|
193
|
+
.ok_or_else(|| "PNG chunk length overflow".to_string())?;
|
|
170
194
|
|
|
171
195
|
if chunk_type == b"IHDR" {
|
|
172
196
|
if chunk_len < 13 {
|
|
173
197
|
return Err("Invalid IHDR".into());
|
|
174
198
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
data[chunk_data_start + 4],
|
|
183
|
-
data[chunk_data_start + 5],
|
|
184
|
-
data[chunk_data_start + 6],
|
|
185
|
-
data[chunk_data_start + 7],
|
|
186
|
-
]) as usize;
|
|
199
|
+
let mut ihdr = [0u8; 13];
|
|
200
|
+
file.read_exact(&mut ihdr).map_err(|e| format!("read IHDR: {}", e))?;
|
|
201
|
+
width = u32::from_be_bytes([ihdr[0], ihdr[1], ihdr[2], ihdr[3]]) as usize;
|
|
202
|
+
height = u32::from_be_bytes([ihdr[4], ihdr[5], ihdr[6], ihdr[7]]) as usize;
|
|
203
|
+
if chunk_len > 13 {
|
|
204
|
+
file.seek(SeekFrom::Current((chunk_len - 13) as i64)).map_err(|e| format!("seek IHDR: {}", e))?;
|
|
205
|
+
}
|
|
187
206
|
} else if chunk_type == b"IDAT" {
|
|
188
|
-
|
|
189
|
-
|
|
207
|
+
idat_ranges.push((chunk_data_start, chunk_data_end));
|
|
208
|
+
file.seek(SeekFrom::Current(chunk_len as i64)).map_err(|e| format!("seek IDAT: {}", e))?;
|
|
209
|
+
} else if chunk_type == b"rXFL" {
|
|
210
|
+
let json_len = usize::try_from(chunk_len).map_err(|_| "rXFL too large".to_string())?;
|
|
211
|
+
let mut json = vec![0u8; json_len];
|
|
212
|
+
file.read_exact(&mut json).map_err(|e| format!("read rXFL: {}", e))?;
|
|
213
|
+
total_expected = parse_rxfl_total_bytes(&json).unwrap_or(total_expected);
|
|
190
214
|
} else if chunk_type == b"IEND" {
|
|
191
215
|
break;
|
|
216
|
+
} else {
|
|
217
|
+
file.seek(SeekFrom::Current(chunk_len as i64)).map_err(|e| format!("seek chunk: {}", e))?;
|
|
192
218
|
}
|
|
193
219
|
|
|
194
|
-
|
|
220
|
+
file.seek(SeekFrom::Current(4)).map_err(|e| format!("seek crc: {}", e))?;
|
|
195
221
|
}
|
|
196
222
|
|
|
197
223
|
if width == 0 || height == 0 {
|
|
198
224
|
return Err("IHDR not found".into());
|
|
199
225
|
}
|
|
200
|
-
if
|
|
226
|
+
if idat_ranges.is_empty() {
|
|
201
227
|
return Err("IDAT not found".into());
|
|
202
228
|
}
|
|
203
229
|
|
|
204
|
-
Ok((width, height,
|
|
230
|
+
Ok((width, height, idat_ranges, total_expected))
|
|
205
231
|
}
|
|
206
232
|
|
|
207
|
-
fn parse_rxfl_total_bytes(
|
|
208
|
-
let
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
let chunk_data_start = pos + 8;
|
|
213
|
-
|
|
214
|
-
if chunk_type == b"rXFL" && chunk_data_start + chunk_len <= data.len() {
|
|
215
|
-
let json_bytes = &data[chunk_data_start..chunk_data_start + chunk_len];
|
|
216
|
-
if let Ok(entries) = serde_json::from_slice::<Vec<serde_json::Value>>(json_bytes) {
|
|
217
|
-
let total: u64 = entries.iter()
|
|
218
|
-
.filter_map(|e| e.get("size").and_then(|s| s.as_u64()))
|
|
219
|
-
.sum();
|
|
220
|
-
return Some(total);
|
|
221
|
-
}
|
|
222
|
-
} else if chunk_type == b"IEND" {
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
pos = chunk_data_start + chunk_len + 4;
|
|
233
|
+
fn parse_rxfl_total_bytes(json_bytes: &[u8]) -> Option<u64> {
|
|
234
|
+
if let Ok(entries) = serde_json::from_slice::<Vec<serde_json::Value>>(json_bytes) {
|
|
235
|
+
return Some(entries.iter()
|
|
236
|
+
.filter_map(|e| e.get("size").and_then(|s| s.as_u64()))
|
|
237
|
+
.sum());
|
|
227
238
|
}
|
|
228
239
|
None
|
|
229
240
|
}
|
|
230
241
|
|
|
231
|
-
struct DeflatePixelReader
|
|
232
|
-
|
|
242
|
+
struct DeflatePixelReader {
|
|
243
|
+
file: File,
|
|
233
244
|
height: usize,
|
|
234
|
-
|
|
235
|
-
|
|
245
|
+
idat_ranges: Vec<(u64, u64)>,
|
|
246
|
+
range_index: usize,
|
|
247
|
+
offset: u64,
|
|
248
|
+
range_end: u64,
|
|
249
|
+
dropped_until: u64,
|
|
236
250
|
block_remaining: usize,
|
|
237
251
|
current_row: usize,
|
|
238
252
|
col_in_row: usize,
|
|
@@ -240,20 +254,104 @@ struct DeflatePixelReader<'a> {
|
|
|
240
254
|
row_bytes: usize,
|
|
241
255
|
}
|
|
242
256
|
|
|
243
|
-
impl
|
|
244
|
-
fn new(
|
|
257
|
+
impl DeflatePixelReader {
|
|
258
|
+
fn new(mut file: File, width: usize, height: usize, idat_ranges: Vec<(u64, u64)>) -> Result<Self, String> {
|
|
259
|
+
let Some(&(offset, range_end)) = idat_ranges.first() else {
|
|
260
|
+
return Err("IDAT not found".to_string());
|
|
261
|
+
};
|
|
262
|
+
crate::io_advice::advise_file_sequential(&file);
|
|
263
|
+
file.seek(SeekFrom::Start(offset)).map_err(|e| format!("seek first IDAT: {}", e))?;
|
|
245
264
|
let row_bytes = width * 3;
|
|
246
|
-
Self {
|
|
247
|
-
|
|
265
|
+
let mut reader = Self {
|
|
266
|
+
file,
|
|
248
267
|
height,
|
|
249
|
-
|
|
250
|
-
|
|
268
|
+
idat_ranges,
|
|
269
|
+
range_index: 0,
|
|
270
|
+
offset,
|
|
271
|
+
range_end,
|
|
272
|
+
dropped_until: offset,
|
|
251
273
|
block_remaining: 0,
|
|
252
274
|
current_row: 0,
|
|
253
275
|
col_in_row: 0,
|
|
254
276
|
scanline_filter_pending: true,
|
|
255
277
|
row_bytes,
|
|
278
|
+
};
|
|
279
|
+
reader.skip_stream_bytes(2)
|
|
280
|
+
.map_err(|e| format!("read zlib header: {}", e))?;
|
|
281
|
+
Ok(reader)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
fn advance_range(&mut self) -> Result<bool, std::io::Error> {
|
|
285
|
+
while self.offset >= self.range_end {
|
|
286
|
+
crate::io_advice::advise_drop(&self.file, self.dropped_until, self.range_end.saturating_sub(self.dropped_until));
|
|
287
|
+
self.dropped_until = self.range_end;
|
|
288
|
+
self.range_index += 1;
|
|
289
|
+
let Some(&(offset, end)) = self.idat_ranges.get(self.range_index) else {
|
|
290
|
+
return Ok(false);
|
|
291
|
+
};
|
|
292
|
+
self.file.seek(SeekFrom::Start(offset))?;
|
|
293
|
+
self.offset = offset;
|
|
294
|
+
self.range_end = end;
|
|
295
|
+
self.dropped_until = offset;
|
|
296
|
+
}
|
|
297
|
+
Ok(true)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
fn maybe_drop_consumed(&mut self) {
|
|
301
|
+
let consumed = self.offset.saturating_sub(self.dropped_until);
|
|
302
|
+
if consumed >= crate::io_advice::INPUT_DROP_GRANULARITY {
|
|
303
|
+
crate::io_advice::advise_drop(&self.file, self.dropped_until, consumed);
|
|
304
|
+
self.dropped_until = self.offset;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
fn read_stream_bytes(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
|
|
309
|
+
let mut written = 0;
|
|
310
|
+
while written < buf.len() {
|
|
311
|
+
if !self.advance_range()? {
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
let available = usize::try_from(self.range_end - self.offset).unwrap_or(buf.len() - written);
|
|
315
|
+
if available == 0 {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
let take = available.min(buf.len() - written);
|
|
319
|
+
let read = self.file.read(&mut buf[written..written + take])?;
|
|
320
|
+
if read == 0 {
|
|
321
|
+
return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "failed to fill whole buffer"));
|
|
322
|
+
}
|
|
323
|
+
self.offset += read as u64;
|
|
324
|
+
written += read;
|
|
325
|
+
self.maybe_drop_consumed();
|
|
326
|
+
}
|
|
327
|
+
Ok(written)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
fn read_stream_exact(&mut self, buf: &mut [u8]) -> Result<(), std::io::Error> {
|
|
331
|
+
let got = self.read_stream_bytes(buf)?;
|
|
332
|
+
if got == buf.len() {
|
|
333
|
+
return Ok(());
|
|
334
|
+
}
|
|
335
|
+
Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "failed to fill whole buffer"))
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
fn skip_stream_bytes(&mut self, count: usize) -> Result<(), std::io::Error> {
|
|
339
|
+
let mut remaining = count;
|
|
340
|
+
while remaining > 0 {
|
|
341
|
+
if !self.advance_range()? {
|
|
342
|
+
return Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "failed to fill whole buffer"));
|
|
343
|
+
}
|
|
344
|
+
let available = usize::try_from(self.range_end - self.offset).unwrap_or(remaining);
|
|
345
|
+
if available == 0 {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
let take = available.min(remaining);
|
|
349
|
+
self.file.seek(SeekFrom::Current(take as i64))?;
|
|
350
|
+
self.offset += take as u64;
|
|
351
|
+
remaining -= take;
|
|
352
|
+
self.maybe_drop_consumed();
|
|
256
353
|
}
|
|
354
|
+
Ok(())
|
|
257
355
|
}
|
|
258
356
|
|
|
259
357
|
fn ensure_block(&mut self) -> Result<(), std::io::Error> {
|
|
@@ -261,13 +359,11 @@ impl<'a> DeflatePixelReader<'a> {
|
|
|
261
359
|
return Ok(());
|
|
262
360
|
}
|
|
263
361
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
362
|
+
let mut header = [0u8; 5];
|
|
363
|
+
self.read_stream_exact(&mut header)?;
|
|
267
364
|
|
|
268
|
-
let len_lo =
|
|
269
|
-
let len_hi =
|
|
270
|
-
self.offset += 5;
|
|
365
|
+
let len_lo = header[1] as usize;
|
|
366
|
+
let len_hi = header[2] as usize;
|
|
271
367
|
|
|
272
368
|
self.block_remaining = len_lo | (len_hi << 8);
|
|
273
369
|
Ok(())
|
|
@@ -277,14 +373,16 @@ impl<'a> DeflatePixelReader<'a> {
|
|
|
277
373
|
let mut written = 0;
|
|
278
374
|
while written < count {
|
|
279
375
|
self.ensure_block()?;
|
|
280
|
-
let avail = self.block_remaining.min(count - written)
|
|
376
|
+
let avail = self.block_remaining.min(count - written);
|
|
281
377
|
if avail == 0 {
|
|
282
378
|
break;
|
|
283
379
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
380
|
+
let got = self.read_stream_bytes(&mut buf[written..written + avail])?;
|
|
381
|
+
if got == 0 {
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
self.block_remaining -= got;
|
|
385
|
+
written += got;
|
|
288
386
|
}
|
|
289
387
|
Ok(written)
|
|
290
388
|
}
|
|
@@ -293,11 +391,11 @@ impl<'a> DeflatePixelReader<'a> {
|
|
|
293
391
|
let mut remaining = count;
|
|
294
392
|
while remaining > 0 {
|
|
295
393
|
self.ensure_block()?;
|
|
296
|
-
let skip = self.block_remaining.min(remaining)
|
|
394
|
+
let skip = self.block_remaining.min(remaining);
|
|
297
395
|
if skip == 0 {
|
|
298
396
|
break;
|
|
299
397
|
}
|
|
300
|
-
self.
|
|
398
|
+
self.skip_stream_bytes(skip)?;
|
|
301
399
|
self.block_remaining -= skip;
|
|
302
400
|
remaining -= skip;
|
|
303
401
|
}
|
|
@@ -305,7 +403,7 @@ impl<'a> DeflatePixelReader<'a> {
|
|
|
305
403
|
}
|
|
306
404
|
}
|
|
307
405
|
|
|
308
|
-
impl
|
|
406
|
+
impl Read for DeflatePixelReader {
|
|
309
407
|
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
|
310
408
|
let mut filled = 0;
|
|
311
409
|
|
|
@@ -342,22 +440,6 @@ impl<'a> Read for DeflatePixelReader<'a> {
|
|
|
342
440
|
}
|
|
343
441
|
}
|
|
344
442
|
|
|
345
|
-
struct FirstByteReader<R: Read> {
|
|
346
|
-
inner: R,
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
impl<R: Read> FirstByteReader<R> {
|
|
350
|
-
fn new(inner: R) -> Self {
|
|
351
|
-
Self { inner }
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
fn into_parts(mut self) -> Result<(u8, impl Read), String> {
|
|
355
|
-
let mut byte = [0u8; 1];
|
|
356
|
-
self.inner.read_exact(&mut byte).map_err(|e| format!("read first byte: {}", e))?;
|
|
357
|
-
Ok((byte[0], self.inner))
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
443
|
struct CtrDecryptReader<R: Read> {
|
|
362
444
|
inner: R,
|
|
363
445
|
cipher: Aes256Ctr,
|
|
@@ -442,6 +524,8 @@ fn tar_unpack_from_reader_with_progress<R: Read>(
|
|
|
442
524
|
std::fs::File::create(&dest).map_err(|e| format!("create {:?}: {}", dest, e))?,
|
|
443
525
|
);
|
|
444
526
|
std::io::copy(&mut entry, &mut f).map_err(|e| format!("write {:?}: {}", dest, e))?;
|
|
527
|
+
let file = f.into_inner().map_err(|e| format!("flush {:?}: {}", dest, e.error()))?;
|
|
528
|
+
crate::io_advice::sync_and_drop(&file, entry_size);
|
|
445
529
|
written.push(safe.to_string_lossy().to_string());
|
|
446
530
|
if files_filter.is_some() {
|
|
447
531
|
remaining = remaining.saturating_sub(1);
|
|
@@ -470,3 +554,68 @@ fn tar_unpack_from_reader_with_progress<R: Read>(
|
|
|
470
554
|
|
|
471
555
|
Ok(written)
|
|
472
556
|
}
|
|
557
|
+
|
|
558
|
+
#[cfg(test)]
|
|
559
|
+
mod tests {
|
|
560
|
+
use super::*;
|
|
561
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
562
|
+
|
|
563
|
+
#[test]
|
|
564
|
+
fn parse_png_header_collects_all_idat_ranges() {
|
|
565
|
+
let mut png = Vec::new();
|
|
566
|
+
png.extend_from_slice(&[137, 80, 78, 71, 13, 10, 26, 10]);
|
|
567
|
+
|
|
568
|
+
let mut ihdr = [0u8; 13];
|
|
569
|
+
ihdr[0..4].copy_from_slice(&1u32.to_be_bytes());
|
|
570
|
+
ihdr[4..8].copy_from_slice(&1u32.to_be_bytes());
|
|
571
|
+
ihdr[8] = 8;
|
|
572
|
+
ihdr[9] = 2;
|
|
573
|
+
|
|
574
|
+
crate::png_chunk_writer::write_png_chunk(&mut png, b"IHDR", &ihdr).unwrap();
|
|
575
|
+
crate::png_chunk_writer::write_png_chunk(&mut png, b"IDAT", &[1, 2, 3]).unwrap();
|
|
576
|
+
crate::png_chunk_writer::write_png_chunk(&mut png, b"IDAT", &[4, 5]).unwrap();
|
|
577
|
+
crate::png_chunk_writer::write_png_chunk(&mut png, b"IEND", &[]).unwrap();
|
|
578
|
+
|
|
579
|
+
let ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis();
|
|
580
|
+
let path = std::env::temp_dir().join(format!("rox_png_header_test_{}.png", ms));
|
|
581
|
+
std::fs::write(&path, &png).unwrap();
|
|
582
|
+
|
|
583
|
+
let mut file = File::open(&path).unwrap();
|
|
584
|
+
let (_, _, ranges, _) = parse_png_metadata(&mut file).unwrap();
|
|
585
|
+
|
|
586
|
+
assert_eq!(ranges.len(), 2);
|
|
587
|
+
assert_eq!(&png[ranges[0].0 as usize..ranges[0].1 as usize], &[1, 2, 3]);
|
|
588
|
+
assert_eq!(&png[ranges[1].0 as usize..ranges[1].1 as usize], &[4, 5]);
|
|
589
|
+
|
|
590
|
+
let _ = std::fs::remove_file(path);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
#[test]
|
|
594
|
+
fn deflate_reader_reads_across_idat_boundaries() {
|
|
595
|
+
let scanline = [0u8, 10, 11, 12, 13, 14, 15];
|
|
596
|
+
let mut deflate = vec![0x78, 0x01, 0x01, 0x07, 0x00, 0xF8, 0xFF];
|
|
597
|
+
deflate.extend_from_slice(&scanline);
|
|
598
|
+
deflate.extend_from_slice(&crate::core::adler32_bytes(&scanline).to_be_bytes());
|
|
599
|
+
|
|
600
|
+
let mut data = Vec::new();
|
|
601
|
+
data.extend_from_slice(&deflate[0..4]);
|
|
602
|
+
data.extend_from_slice(&[200, 201, 202]);
|
|
603
|
+
data.extend_from_slice(&deflate[4..12]);
|
|
604
|
+
data.extend_from_slice(&[203, 204]);
|
|
605
|
+
data.extend_from_slice(&deflate[12..]);
|
|
606
|
+
|
|
607
|
+
let ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis();
|
|
608
|
+
let path = std::env::temp_dir().join(format!("rox_deflate_reader_test_{}.bin", ms));
|
|
609
|
+
std::fs::write(&path, &data).unwrap();
|
|
610
|
+
|
|
611
|
+
let ranges = vec![(0, 4), (7, 15), (17, 23)];
|
|
612
|
+
let file = File::open(&path).unwrap();
|
|
613
|
+
let mut reader = DeflatePixelReader::new(file, 2, 1, ranges).unwrap();
|
|
614
|
+
let mut out = Vec::new();
|
|
615
|
+
reader.read_to_end(&mut out).unwrap();
|
|
616
|
+
|
|
617
|
+
assert_eq!(out, vec![10, 11, 12, 13, 14, 15]);
|
|
618
|
+
|
|
619
|
+
let _ = std::fs::remove_file(path);
|
|
620
|
+
}
|
|
621
|
+
}
|