1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
//! Solid block handling.
//!
//! This module provides functions for working with solid archives where
//! multiple files are compressed together in a single block.
use std::io::{Read, Seek};
#[cfg(feature = "lzma")]
use std::io::{SeekFrom, Write};
use crate::format::SIGNATURE_HEADER_SIZE;
use crate::format::streams::Folder;
use crate::{Error, Result};
#[cfg(feature = "lzma")]
use crate::{READ_BUFFER_SIZE, codec};
use super::Archive;
#[cfg(feature = "lzma")]
use super::{ExtractionLimits, map_io_error};
impl<R: Read + Seek> Archive<R> {
/// Calculates the pack position for a folder.
pub(crate) fn calculate_pack_position(&self, folder_idx: usize) -> Result<u64> {
let pack_info = self
.header
.pack_info
.as_ref()
.ok_or_else(|| Error::InvalidFormat("missing pack info".into()))?;
// Start after SFX stub (if any) + signature header (32 bytes) + pack_pos
let mut offset = self.sfx_offset + SIGNATURE_HEADER_SIZE + pack_info.pack_pos;
// Sum the packed streams the folders before this one own, which is not
// one apiece: a folder whose chain takes several inputs - BCJ2 takes
// four - owns that many. Counting folders instead of streams put every
// folder after a BCJ2 one at the wrong offset, so an archive this crate
// wrote, and real 7-Zip reads, failed its own checksums here.
let base = self.calculate_folder_pack_base(folder_idx)?;
for size in pack_info.pack_sizes.iter().take(base) {
offset += size;
}
Ok(offset)
}
/// Returns how many bytes of packed data a folder occupies.
///
/// The sum of its own packed streams. Reading the single size that happens
/// to sit at the folder's index is right only while every folder owns
/// exactly one stream, which stops being true the moment a BCJ2 folder is
/// in the archive.
pub(crate) fn folder_pack_size(&self, folder: &Folder, folder_idx: usize) -> Result<u64> {
let pack_info = self
.header
.pack_info
.as_ref()
.ok_or_else(|| Error::InvalidFormat("missing pack info".into()))?;
let base = self.calculate_folder_pack_base(folder_idx)?;
let mut total = 0u64;
for i in 0..folder.packed_streams.len().max(1) {
total += pack_info
.pack_sizes
.get(base + i)
.copied()
.ok_or_else(|| Error::InvalidFormat("missing pack size".into()))?;
}
Ok(total)
}
/// Checks if a folder is a solid block (contains multiple files).
pub(crate) fn is_solid_block(&self, folder_idx: usize) -> bool {
self.header
.substreams_info
.as_ref()
.and_then(|ss| ss.num_unpack_streams_in_folders.get(folder_idx))
.map(|&count| count > 1)
.unwrap_or(false)
}
/// Gets entry sizes for a solid block.
pub(crate) fn get_solid_block_entry_sizes(&self, folder_idx: usize) -> Result<Vec<u64>> {
let substreams = self
.header
.substreams_info
.as_ref()
.ok_or_else(|| Error::InvalidFormat("missing substreams info".into()))?;
let num_streams = *substreams
.num_unpack_streams_in_folders
.get(folder_idx)
.ok_or_else(|| Error::InvalidFormat("folder index out of range".into()))?
as usize;
// Calculate the starting stream index for this folder
let stream_offset: usize = substreams
.num_unpack_streams_in_folders
.iter()
.take(folder_idx)
.map(|&n| n as usize)
.sum();
// Get sizes from substreams info
let sizes: Vec<u64> = (0..num_streams)
.map(|i| {
substreams
.unpack_sizes
.get(stream_offset + i)
.copied()
.unwrap_or(0)
})
.collect();
Ok(sizes)
}
/// Calculates the pack stream base index for a folder.
///
/// For multi-stream folders (like BCJ2), we need to know where this folder's
/// pack streams start in the global PackInfo.pack_sizes array.
pub(crate) fn calculate_folder_pack_base(&self, folder_idx: usize) -> Result<usize> {
if self.header.unpack_info.is_none() {
return Err(Error::InvalidFormat("missing unpack info".into()));
}
Ok(crate::streaming::packed_stream_base(
&self.header,
folder_idx,
))
}
/// Reads all pack streams for a folder.
///
/// Returns a Vec of Vec<u8>, one for each pack stream in the folder.
#[cfg(feature = "lzma")]
pub(crate) fn read_folder_pack_streams(
&mut self,
folder: &Folder,
folder_idx: usize,
) -> Result<Vec<Vec<u8>>> {
let pack_info = self
.header
.pack_info
.as_ref()
.ok_or_else(|| Error::InvalidFormat("missing pack info".into()))?;
let pack_base = self.calculate_folder_pack_base(folder_idx)?;
let num_pack_streams = folder.packed_streams.len();
// Calculate the starting offset for this folder's pack streams
// Include SFX offset for self-extracting archives
let mut pack_offset = self.sfx_offset + SIGNATURE_HEADER_SIZE + pack_info.pack_pos;
for i in 0..pack_base {
if i < pack_info.pack_sizes.len() {
pack_offset += pack_info.pack_sizes[i];
}
}
let mut pack_data = Vec::with_capacity(num_pack_streams);
for i in 0..num_pack_streams {
let pack_idx = pack_base + i;
let pack_size = pack_info.pack_sizes.get(pack_idx).copied().ok_or_else(|| {
Error::InvalidFormat(format!(
"missing pack size for stream {} (pack_idx {})",
i, pack_idx
))
})?;
self.reader
.seek(SeekFrom::Start(pack_offset))
.map_err(Error::Io)?;
let mut data = vec![0u8; pack_size as usize];
self.reader.read_exact(&mut data).map_err(Error::Io)?;
pack_data.push(data);
pack_offset += pack_size;
}
Ok(pack_data)
}
/// Extracts a BCJ2-compressed entry.
///
/// For solid archives, extracts only the specified stream (file) from the block.
#[cfg(feature = "lzma")]
pub(crate) fn extract_bcj2(
&mut self,
folder: &Folder,
folder_idx: usize,
stream_index: Option<usize>,
output: &mut impl Write,
limits: &ExtractionLimits,
) -> Result<u64> {
// Read all pack streams for this folder
let pack_data = self.read_folder_pack_streams(folder, folder_idx)?;
// Calculate total compressed size for ratio limiting
let compressed_size: u64 = pack_data.iter().map(|p| p.len() as u64).sum();
// Build BCJ2 decoder
let mut decoder = codec::build_bcj2_folder_decoder(folder, &pack_data)?;
// Check if this is a solid block (multiple files in one folder)
// Use is_solid_block() first to avoid requiring SubStreamsInfo for non-solid BCJ2
let is_solid = self.is_solid_block(folder_idx);
if is_solid {
let entry_sizes = self.get_solid_block_entry_sizes(folder_idx)?;
let stream_idx = stream_index.unwrap_or(0);
if stream_idx >= entry_sizes.len() {
return Err(Error::InvalidFormat(format!(
"stream index {} out of range for solid BCJ2 block",
stream_idx
)));
}
// Skip entries before the target (no limit enforcement on skipped data)
let mut buf = [0u8; READ_BUFFER_SIZE];
for &skip_size in entry_sizes.iter().take(stream_idx) {
let mut remaining = skip_size;
while remaining > 0 {
let to_read = buf.len().min(remaining as usize);
let n = decoder.read(&mut buf[..to_read]).map_err(Error::Io)?;
if n == 0 {
return Err(Error::InvalidFormat(
"unexpected end of BCJ2 stream while skipping".into(),
));
}
remaining -= n as u64;
}
}
// Read only the target entry with limit enforcement
let target_size = entry_sizes[stream_idx];
let mut limited_decoder = limits.wrap_reader(&mut decoder, compressed_size);
let mut remaining = target_size;
let mut total_written = 0u64;
while remaining > 0 {
let to_read = buf.len().min(remaining as usize);
let n = limited_decoder
.read(&mut buf[..to_read])
.map_err(map_io_error)?;
if n == 0 {
break;
}
output.write_all(&buf[..n]).map_err(Error::Io)?;
total_written += n as u64;
remaining -= n as u64;
}
Ok(total_written)
} else {
// Non-solid: decompress and write everything with limit enforcement
let mut limited_decoder = limits.wrap_reader(&mut decoder, compressed_size);
let mut total_written = 0u64;
let mut buf = [0u8; READ_BUFFER_SIZE];
loop {
let n = limited_decoder.read(&mut buf).map_err(map_io_error)?;
if n == 0 {
break;
}
output.write_all(&buf[..n]).map_err(Error::Io)?;
total_written += n as u64;
}
Ok(total_written)
}
}
}