roxify 1.13.3 → 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 +3 -1
- package/README.md +26 -13
- 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 +81 -43
- package/native/packer.rs +232 -75
- 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 +313 -114
- package/native/streaming_encode.rs +272 -128
- 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
|
@@ -1,16 +1,20 @@
|
|
|
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;
|
|
8
|
+
const PACK_MAGIC: [u8; 4] = 0x524f5850u32.to_be_bytes();
|
|
9
|
+
const HEADER_VERSION_V1: u8 = 1;
|
|
10
|
+
const HEADER_VERSION_V2: u8 = 2;
|
|
7
11
|
|
|
8
12
|
type Aes256Ctr = ctr::Ctr64BE<aes::Aes256>;
|
|
9
13
|
|
|
10
14
|
pub type DecodeProgressCallback = Box<dyn Fn(u64, u64, &str) + Send>;
|
|
11
15
|
|
|
12
16
|
pub fn streaming_decode_to_dir(png_path: &Path, out_dir: &Path) -> Result<Vec<String>, String> {
|
|
13
|
-
|
|
17
|
+
streaming_decode_selected_to_dir_encrypted_with_progress(png_path, out_dir, None, None, None)
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
pub fn streaming_decode_to_dir_encrypted(
|
|
@@ -18,7 +22,7 @@ pub fn streaming_decode_to_dir_encrypted(
|
|
|
18
22
|
out_dir: &Path,
|
|
19
23
|
passphrase: Option<&str>,
|
|
20
24
|
) -> Result<Vec<String>, String> {
|
|
21
|
-
|
|
25
|
+
streaming_decode_selected_to_dir_encrypted_with_progress(png_path, out_dir, None, passphrase, None)
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
pub fn streaming_decode_to_dir_encrypted_with_progress(
|
|
@@ -27,26 +31,30 @@ pub fn streaming_decode_to_dir_encrypted_with_progress(
|
|
|
27
31
|
passphrase: Option<&str>,
|
|
28
32
|
progress: Option<DecodeProgressCallback>,
|
|
29
33
|
) -> Result<Vec<String>, String> {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
let data = &mmap[..];
|
|
34
|
+
streaming_decode_selected_to_dir_encrypted_with_progress(png_path, out_dir, None, passphrase, progress)
|
|
35
|
+
}
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
pub fn streaming_decode_selected_to_dir_encrypted_with_progress(
|
|
38
|
+
png_path: &Path,
|
|
39
|
+
out_dir: &Path,
|
|
40
|
+
files_opt: Option<&[String]>,
|
|
41
|
+
passphrase: Option<&str>,
|
|
42
|
+
progress: Option<DecodeProgressCallback>,
|
|
43
|
+
) -> Result<Vec<String>, String> {
|
|
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)?;
|
|
37
46
|
|
|
38
47
|
if let Some(ref cb) = progress {
|
|
39
48
|
cb(2, 100, "parsing_png");
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
let (width, height, idat_data_start, idat_data_end) = parse_png_header(data)?;
|
|
43
|
-
let total_expected = parse_rxfl_total_bytes(data).unwrap_or(0);
|
|
44
|
-
|
|
45
51
|
if let Some(ref cb) = progress {
|
|
46
52
|
cb(5, 100, "reading_header");
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
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))?;
|
|
50
58
|
|
|
51
59
|
let mut marker_buf = [0u8; MARKER_BYTES];
|
|
52
60
|
reader.read_exact(&mut marker_buf).map_err(|e| format!("read markers: {}", e))?;
|
|
@@ -67,34 +75,31 @@ pub fn streaming_decode_to_dir_encrypted_with_progress(
|
|
|
67
75
|
reader.read_exact(&mut name_buf).map_err(|e| format!("read name: {}", e))?;
|
|
68
76
|
}
|
|
69
77
|
|
|
70
|
-
let mut
|
|
71
|
-
reader.read_exact(&mut plen_buf).map_err(|e| format!("read payload_len: {}", e))?;
|
|
72
|
-
let payload_len = u32::from_be_bytes(plen_buf) as u64;
|
|
78
|
+
let payload_len = read_payload_len(&mut reader, hdr[0])?;
|
|
73
79
|
|
|
74
80
|
if let Some(ref cb) = progress {
|
|
75
81
|
cb(8, 100, "decrypting");
|
|
76
82
|
}
|
|
77
83
|
|
|
78
|
-
let
|
|
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);
|
|
79
87
|
|
|
80
|
-
|
|
81
|
-
let (enc_byte, remaining_reader) = first_byte_reader.into_parts()?;
|
|
82
|
-
|
|
83
|
-
match enc_byte {
|
|
88
|
+
match enc_byte[0] {
|
|
84
89
|
0x00 => {
|
|
85
90
|
if let Some(ref cb) = progress {
|
|
86
91
|
cb(10, 100, "decompressing");
|
|
87
92
|
}
|
|
88
|
-
let mut decoder = zstd::stream::Decoder::new(
|
|
93
|
+
let mut decoder = zstd::stream::Decoder::new(reader)
|
|
89
94
|
.map_err(|e| format!("zstd decoder: {}", e))?;
|
|
90
95
|
decoder.window_log_max(31).map_err(|e| format!("zstd window_log_max: {}", e))?;
|
|
91
|
-
|
|
96
|
+
read_rox1_and_unpack_with_progress(decoder, out_dir, files_opt, progress, total_expected)
|
|
92
97
|
}
|
|
93
98
|
0x03 => {
|
|
94
99
|
let pass = passphrase.ok_or("Passphrase required for AES-CTR decryption")?;
|
|
95
100
|
let mut salt = [0u8; 16];
|
|
96
101
|
let mut iv = [0u8; 16];
|
|
97
|
-
let mut r =
|
|
102
|
+
let mut r = reader.take(remaining_payload_len);
|
|
98
103
|
r.read_exact(&mut salt).map_err(|e| format!("read salt: {}", e))?;
|
|
99
104
|
r.read_exact(&mut iv).map_err(|e| format!("read iv: {}", e))?;
|
|
100
105
|
|
|
@@ -103,7 +108,7 @@ pub fn streaming_decode_to_dir_encrypted_with_progress(
|
|
|
103
108
|
.map_err(|e| format!("AES-CTR init: {}", e))?;
|
|
104
109
|
|
|
105
110
|
let hmac_size = 32u64;
|
|
106
|
-
let encrypted_data_len =
|
|
111
|
+
let encrypted_data_len = remaining_payload_len - 16 - 16 - hmac_size;
|
|
107
112
|
let ctr_reader = CtrDecryptReader::new(r.take(encrypted_data_len), cipher);
|
|
108
113
|
|
|
109
114
|
if let Some(ref cb) = progress {
|
|
@@ -112,15 +117,32 @@ pub fn streaming_decode_to_dir_encrypted_with_progress(
|
|
|
112
117
|
let mut decoder = zstd::stream::Decoder::new(ctr_reader)
|
|
113
118
|
.map_err(|e| format!("zstd decoder: {}", e))?;
|
|
114
119
|
decoder.window_log_max(31).map_err(|e| format!("zstd window_log_max: {}", e))?;
|
|
115
|
-
|
|
120
|
+
read_rox1_and_unpack_with_progress(decoder, out_dir, files_opt, progress, total_expected)
|
|
121
|
+
}
|
|
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)
|
|
116
132
|
}
|
|
117
|
-
|
|
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)),
|
|
118
139
|
}
|
|
119
140
|
}
|
|
120
141
|
|
|
121
|
-
fn
|
|
142
|
+
fn read_rox1_and_unpack_with_progress<R: Read>(
|
|
122
143
|
mut decoder: R,
|
|
123
144
|
out_dir: &Path,
|
|
145
|
+
files_opt: Option<&[String]>,
|
|
124
146
|
progress: Option<DecodeProgressCallback>,
|
|
125
147
|
total_expected: u64,
|
|
126
148
|
) -> Result<Vec<String>, String> {
|
|
@@ -130,87 +152,101 @@ fn read_rox1_and_untar_with_progress<R: Read>(
|
|
|
130
152
|
return Err(format!("Expected ROX1, got {:?}", magic));
|
|
131
153
|
}
|
|
132
154
|
std::fs::create_dir_all(out_dir).map_err(|e| format!("mkdir: {}", e))?;
|
|
133
|
-
|
|
155
|
+
|
|
156
|
+
let mut prefix = [0u8; 4];
|
|
157
|
+
decoder.read_exact(&mut prefix).map_err(|e| format!("read payload magic: {}", e))?;
|
|
158
|
+
let mut chained = std::io::Cursor::new(prefix).chain(decoder);
|
|
159
|
+
|
|
160
|
+
if prefix == PACK_MAGIC {
|
|
161
|
+
crate::packer::unpack_stream_to_dir(&mut chained, out_dir, files_opt, progress.as_deref(), total_expected)
|
|
162
|
+
.map_err(|e| format!("pack unpack: {}", e))
|
|
163
|
+
} else {
|
|
164
|
+
tar_unpack_from_reader_with_progress(chained, out_dir, files_opt, progress, total_expected)
|
|
165
|
+
}
|
|
134
166
|
}
|
|
135
167
|
|
|
136
|
-
fn
|
|
137
|
-
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
|
+
}
|
|
138
174
|
|
|
139
175
|
let mut width = 0usize;
|
|
140
176
|
let mut height = 0usize;
|
|
141
|
-
let mut
|
|
142
|
-
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
|
+
}
|
|
143
187
|
|
|
144
|
-
|
|
145
|
-
let
|
|
146
|
-
let
|
|
147
|
-
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())?;
|
|
148
194
|
|
|
149
195
|
if chunk_type == b"IHDR" {
|
|
150
196
|
if chunk_len < 13 {
|
|
151
197
|
return Err("Invalid IHDR".into());
|
|
152
198
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
data[chunk_data_start + 4],
|
|
161
|
-
data[chunk_data_start + 5],
|
|
162
|
-
data[chunk_data_start + 6],
|
|
163
|
-
data[chunk_data_start + 7],
|
|
164
|
-
]) 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
|
+
}
|
|
165
206
|
} else if chunk_type == b"IDAT" {
|
|
166
|
-
|
|
167
|
-
|
|
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);
|
|
168
214
|
} else if chunk_type == b"IEND" {
|
|
169
215
|
break;
|
|
216
|
+
} else {
|
|
217
|
+
file.seek(SeekFrom::Current(chunk_len as i64)).map_err(|e| format!("seek chunk: {}", e))?;
|
|
170
218
|
}
|
|
171
219
|
|
|
172
|
-
|
|
220
|
+
file.seek(SeekFrom::Current(4)).map_err(|e| format!("seek crc: {}", e))?;
|
|
173
221
|
}
|
|
174
222
|
|
|
175
223
|
if width == 0 || height == 0 {
|
|
176
224
|
return Err("IHDR not found".into());
|
|
177
225
|
}
|
|
178
|
-
if
|
|
226
|
+
if idat_ranges.is_empty() {
|
|
179
227
|
return Err("IDAT not found".into());
|
|
180
228
|
}
|
|
181
229
|
|
|
182
|
-
Ok((width, height,
|
|
230
|
+
Ok((width, height, idat_ranges, total_expected))
|
|
183
231
|
}
|
|
184
232
|
|
|
185
|
-
fn parse_rxfl_total_bytes(
|
|
186
|
-
let
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
let chunk_data_start = pos + 8;
|
|
191
|
-
|
|
192
|
-
if chunk_type == b"rXFL" && chunk_data_start + chunk_len <= data.len() {
|
|
193
|
-
let json_bytes = &data[chunk_data_start..chunk_data_start + chunk_len];
|
|
194
|
-
if let Ok(entries) = serde_json::from_slice::<Vec<serde_json::Value>>(json_bytes) {
|
|
195
|
-
let total: u64 = entries.iter()
|
|
196
|
-
.filter_map(|e| e.get("size").and_then(|s| s.as_u64()))
|
|
197
|
-
.sum();
|
|
198
|
-
return Some(total);
|
|
199
|
-
}
|
|
200
|
-
} else if chunk_type == b"IEND" {
|
|
201
|
-
break;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
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());
|
|
205
238
|
}
|
|
206
239
|
None
|
|
207
240
|
}
|
|
208
241
|
|
|
209
|
-
struct DeflatePixelReader
|
|
210
|
-
|
|
242
|
+
struct DeflatePixelReader {
|
|
243
|
+
file: File,
|
|
211
244
|
height: usize,
|
|
212
|
-
|
|
213
|
-
|
|
245
|
+
idat_ranges: Vec<(u64, u64)>,
|
|
246
|
+
range_index: usize,
|
|
247
|
+
offset: u64,
|
|
248
|
+
range_end: u64,
|
|
249
|
+
dropped_until: u64,
|
|
214
250
|
block_remaining: usize,
|
|
215
251
|
current_row: usize,
|
|
216
252
|
col_in_row: usize,
|
|
@@ -218,20 +254,104 @@ struct DeflatePixelReader<'a> {
|
|
|
218
254
|
row_bytes: usize,
|
|
219
255
|
}
|
|
220
256
|
|
|
221
|
-
impl
|
|
222
|
-
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))?;
|
|
223
264
|
let row_bytes = width * 3;
|
|
224
|
-
Self {
|
|
225
|
-
|
|
265
|
+
let mut reader = Self {
|
|
266
|
+
file,
|
|
226
267
|
height,
|
|
227
|
-
|
|
228
|
-
|
|
268
|
+
idat_ranges,
|
|
269
|
+
range_index: 0,
|
|
270
|
+
offset,
|
|
271
|
+
range_end,
|
|
272
|
+
dropped_until: offset,
|
|
229
273
|
block_remaining: 0,
|
|
230
274
|
current_row: 0,
|
|
231
275
|
col_in_row: 0,
|
|
232
276
|
scanline_filter_pending: true,
|
|
233
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();
|
|
234
353
|
}
|
|
354
|
+
Ok(())
|
|
235
355
|
}
|
|
236
356
|
|
|
237
357
|
fn ensure_block(&mut self) -> Result<(), std::io::Error> {
|
|
@@ -239,13 +359,11 @@ impl<'a> DeflatePixelReader<'a> {
|
|
|
239
359
|
return Ok(());
|
|
240
360
|
}
|
|
241
361
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
362
|
+
let mut header = [0u8; 5];
|
|
363
|
+
self.read_stream_exact(&mut header)?;
|
|
245
364
|
|
|
246
|
-
let len_lo =
|
|
247
|
-
let len_hi =
|
|
248
|
-
self.offset += 5;
|
|
365
|
+
let len_lo = header[1] as usize;
|
|
366
|
+
let len_hi = header[2] as usize;
|
|
249
367
|
|
|
250
368
|
self.block_remaining = len_lo | (len_hi << 8);
|
|
251
369
|
Ok(())
|
|
@@ -255,14 +373,16 @@ impl<'a> DeflatePixelReader<'a> {
|
|
|
255
373
|
let mut written = 0;
|
|
256
374
|
while written < count {
|
|
257
375
|
self.ensure_block()?;
|
|
258
|
-
let avail = self.block_remaining.min(count - written)
|
|
376
|
+
let avail = self.block_remaining.min(count - written);
|
|
259
377
|
if avail == 0 {
|
|
260
378
|
break;
|
|
261
379
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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;
|
|
266
386
|
}
|
|
267
387
|
Ok(written)
|
|
268
388
|
}
|
|
@@ -271,11 +391,11 @@ impl<'a> DeflatePixelReader<'a> {
|
|
|
271
391
|
let mut remaining = count;
|
|
272
392
|
while remaining > 0 {
|
|
273
393
|
self.ensure_block()?;
|
|
274
|
-
let skip = self.block_remaining.min(remaining)
|
|
394
|
+
let skip = self.block_remaining.min(remaining);
|
|
275
395
|
if skip == 0 {
|
|
276
396
|
break;
|
|
277
397
|
}
|
|
278
|
-
self.
|
|
398
|
+
self.skip_stream_bytes(skip)?;
|
|
279
399
|
self.block_remaining -= skip;
|
|
280
400
|
remaining -= skip;
|
|
281
401
|
}
|
|
@@ -283,7 +403,7 @@ impl<'a> DeflatePixelReader<'a> {
|
|
|
283
403
|
}
|
|
284
404
|
}
|
|
285
405
|
|
|
286
|
-
impl
|
|
406
|
+
impl Read for DeflatePixelReader {
|
|
287
407
|
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
|
288
408
|
let mut filled = 0;
|
|
289
409
|
|
|
@@ -320,22 +440,6 @@ impl<'a> Read for DeflatePixelReader<'a> {
|
|
|
320
440
|
}
|
|
321
441
|
}
|
|
322
442
|
|
|
323
|
-
struct FirstByteReader<R: Read> {
|
|
324
|
-
inner: R,
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
impl<R: Read> FirstByteReader<R> {
|
|
328
|
-
fn new(inner: R) -> Self {
|
|
329
|
-
Self { inner }
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
fn into_parts(mut self) -> Result<(u8, impl Read), String> {
|
|
333
|
-
let mut byte = [0u8; 1];
|
|
334
|
-
self.inner.read_exact(&mut byte).map_err(|e| format!("read first byte: {}", e))?;
|
|
335
|
-
Ok((byte[0], self.inner))
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
443
|
struct CtrDecryptReader<R: Read> {
|
|
340
444
|
inner: R,
|
|
341
445
|
cipher: Aes256Ctr,
|
|
@@ -360,6 +464,7 @@ impl<R: Read> Read for CtrDecryptReader<R> {
|
|
|
360
464
|
fn tar_unpack_from_reader_with_progress<R: Read>(
|
|
361
465
|
reader: R,
|
|
362
466
|
output_dir: &Path,
|
|
467
|
+
files_opt: Option<&[String]>,
|
|
363
468
|
progress: Option<DecodeProgressCallback>,
|
|
364
469
|
total_expected: u64,
|
|
365
470
|
) -> Result<Vec<String>, String> {
|
|
@@ -369,12 +474,16 @@ fn tar_unpack_from_reader_with_progress<R: Read>(
|
|
|
369
474
|
let mut created_dirs = std::collections::HashSet::new();
|
|
370
475
|
let mut bytes_extracted: u64 = 0;
|
|
371
476
|
let mut last_pct: u64 = 10;
|
|
477
|
+
let files_filter: Option<std::collections::HashSet<&str>> = files_opt.map(|files| files.iter().map(|file| file.as_str()).collect());
|
|
478
|
+
let mut remaining = files_filter.as_ref().map(|files| files.len()).unwrap_or(usize::MAX);
|
|
372
479
|
|
|
373
480
|
let entries = archive.entries().map_err(|e| format!("tar entries: {}", e))?;
|
|
374
481
|
for entry in entries {
|
|
375
482
|
let mut entry = entry.map_err(|e| format!("tar entry: {}", e))?;
|
|
376
483
|
let entry_size = entry.size();
|
|
377
484
|
let path = entry.path().map_err(|e| format!("tar path: {}", e))?.to_path_buf();
|
|
485
|
+
let logical_path = path.to_string_lossy().replace('\\', "/");
|
|
486
|
+
let should_write = files_filter.as_ref().map(|files| files.contains(logical_path.as_str())).unwrap_or(true);
|
|
378
487
|
|
|
379
488
|
let mut safe = std::path::PathBuf::new();
|
|
380
489
|
for comp in path.components() {
|
|
@@ -386,6 +495,23 @@ fn tar_unpack_from_reader_with_progress<R: Read>(
|
|
|
386
495
|
continue;
|
|
387
496
|
}
|
|
388
497
|
|
|
498
|
+
if !should_write {
|
|
499
|
+
std::io::copy(&mut entry, &mut std::io::sink()).map_err(|e| format!("skip {:?}: {}", safe, e))?;
|
|
500
|
+
bytes_extracted += entry_size;
|
|
501
|
+
if let Some(ref cb) = progress {
|
|
502
|
+
let pct = if total_expected > 0 {
|
|
503
|
+
10 + (bytes_extracted * 89 / total_expected).min(89)
|
|
504
|
+
} else {
|
|
505
|
+
(10 + (bytes_extracted / (1024 * 1024))).min(99)
|
|
506
|
+
};
|
|
507
|
+
if pct > last_pct {
|
|
508
|
+
last_pct = pct;
|
|
509
|
+
cb(pct, 100, "extracting");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
|
|
389
515
|
let dest = output_dir.join(&safe);
|
|
390
516
|
if let Some(parent) = dest.parent() {
|
|
391
517
|
if created_dirs.insert(parent.to_path_buf()) {
|
|
@@ -398,7 +524,12 @@ fn tar_unpack_from_reader_with_progress<R: Read>(
|
|
|
398
524
|
std::fs::File::create(&dest).map_err(|e| format!("create {:?}: {}", dest, e))?,
|
|
399
525
|
);
|
|
400
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);
|
|
401
529
|
written.push(safe.to_string_lossy().to_string());
|
|
530
|
+
if files_filter.is_some() {
|
|
531
|
+
remaining = remaining.saturating_sub(1);
|
|
532
|
+
}
|
|
402
533
|
|
|
403
534
|
bytes_extracted += entry_size;
|
|
404
535
|
if let Some(ref cb) = progress {
|
|
@@ -412,6 +543,9 @@ fn tar_unpack_from_reader_with_progress<R: Read>(
|
|
|
412
543
|
cb(pct, 100, "extracting");
|
|
413
544
|
}
|
|
414
545
|
}
|
|
546
|
+
if remaining == 0 {
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
415
549
|
}
|
|
416
550
|
|
|
417
551
|
if let Some(ref cb) = progress {
|
|
@@ -420,3 +554,68 @@ fn tar_unpack_from_reader_with_progress<R: Read>(
|
|
|
420
554
|
|
|
421
555
|
Ok(written)
|
|
422
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
|
+
}
|