1#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
14
15mod archive;
16mod bytes;
17mod codec;
18mod cp437;
19mod crypto;
20mod deflate64_seek;
21#[cfg(feature = "vfs")]
22mod vfs;
23
24pub use archive::{
25 ArchiveSummary, CompressionMethod, EntryLayout, ExtraFields, HeaderFields, ZipArchive, ZipFile,
26};
27
28#[cfg(feature = "vfs")]
29pub use vfs::ZipVfs;
30
31use std::io::Read;
32use std::path::{Path, PathBuf};
33
34#[derive(Debug, thiserror::Error)]
36pub enum ZipCoreError {
37 #[error("I/O error: {0}")]
39 Io(#[from] std::io::Error),
40
41 #[error("malformed ZIP container: {0}")]
43 Format(#[from] FormatError),
44
45 #[error("unsupported compression method: {0:?}")]
47 UnsupportedMethod(CompressionMethod),
48
49 #[error(
51 "CRC-32 mismatch in entry {entry}: expected {expected:#010x}, computed {actual:#010x}"
52 )]
53 CrcMismatch {
54 entry: String,
56 expected: u32,
58 actual: u32,
60 },
61
62 #[error("entry is encrypted (password required): {0}")]
64 EncryptedNoPassword(String),
65
66 #[error("incorrect password for entry: {0}")]
68 WrongPassword(String),
69
70 #[error("unsupported encryption for entry {entry}: {reason}")]
72 UnsupportedEncryption {
73 entry: String,
75 reason: String,
77 },
78
79 #[error("entry not found: {0}")]
81 EntryNotFound(String),
82
83 #[error("entry index out of bounds: {0}")]
85 IndexOutOfBounds(usize),
86
87 #[error("entry {entry} is on disk {disk} of a spanned archive (not supported)")]
90 SpannedArchive {
91 entry: String,
93 disk: u32,
95 },
96
97 #[error("malformed deflate stream in entry {entry}: {reason}")]
99 Malformed {
100 entry: String,
102 reason: String,
104 },
105}
106
107#[derive(Debug, thiserror::Error)]
110pub enum FormatError {
111 #[error("unexpected end of data")]
113 Truncated,
114
115 #[error("End Of Central Directory record not found")]
117 NoEocd,
118
119 #[error("bad signature for {what} at offset {offset}")]
121 BadSignature {
122 what: &'static str,
124 offset: u64,
126 },
127
128 #[error("Zip64 archive not yet supported")]
130 Zip64Unsupported,
131
132 #[error("Zip64 sentinel without a matching Zip64 record/extra field")]
135 Zip64Inconsistent,
136
137 #[error("central directory out of range: offset {cd_offset}, size {cd_size}")]
139 CentralDirOutOfRange {
140 cd_offset: u64,
142 cd_size: u64,
144 },
145
146 #[error("declared entry count {0} exceeds the safety ceiling")]
148 TooManyEntries(usize),
149}
150
151#[derive(Debug, Clone, Copy)]
153struct StoredBlock {
154 uncomp_start: u64,
156 len: u64,
158 file_offset: u64,
160}
161
162enum Layout {
164 StoredBlocks(Vec<StoredBlock>),
166 Deflate64(deflate64_seek::Deflate64Index),
168 Fallback { path: PathBuf, name: String },
170}
171
172pub struct StoredZipEntry {
174 file: std::fs::File,
175 uncompressed_size: u64,
176 layout: Layout,
177}
178
179impl StoredZipEntry {
180 pub fn len(&self) -> u64 {
182 self.uncompressed_size
183 }
184
185 pub fn is_empty(&self) -> bool {
187 self.uncompressed_size == 0
188 }
189
190 pub fn is_stored_block_indexed(&self) -> bool {
193 matches!(self.layout, Layout::StoredBlocks(_))
194 }
195
196 pub fn block_count(&self) -> usize {
198 match &self.layout {
199 Layout::StoredBlocks(b) => b.len(),
200 Layout::Deflate64(_) | Layout::Fallback { .. } => 0,
201 }
202 }
203
204 pub fn is_deflate64_checkpoint_indexed(&self) -> bool {
207 matches!(self.layout, Layout::Deflate64(_))
208 }
209
210 pub fn checkpoint_count(&self) -> usize {
212 match &self.layout {
213 Layout::Deflate64(index) => index.checkpoint_count(),
214 Layout::StoredBlocks(_) | Layout::Fallback { .. } => 0,
215 }
216 }
217
218 pub fn read_at(&self, buf: &mut [u8], offset: u64) -> std::io::Result<usize> {
223 if offset >= self.uncompressed_size || buf.is_empty() {
224 return Ok(0);
225 }
226 let want_end = (offset + buf.len() as u64).min(self.uncompressed_size);
227 let total = (want_end - offset) as usize;
228 match &self.layout {
229 Layout::StoredBlocks(blocks) => {
230 let mut filled = 0usize;
231 let mut cur = offset;
232 while cur < want_end {
233 let bi = blocks.partition_point(|b| b.uncomp_start + b.len <= cur);
235 let Some(b) = blocks.get(bi) else {
236 break; };
238 let within = cur - b.uncomp_start;
239 let avail = b.len - within;
240 let n = avail.min(want_end - cur) as usize;
241 pread_exact(
242 &self.file,
243 &mut buf[filled..filled + n],
244 b.file_offset + within,
245 )?;
246 filled += n;
247 cur += n as u64;
248 }
249 Ok(filled)
250 }
251 Layout::Deflate64(index) => index.read_at(&self.file, buf, offset),
252 Layout::Fallback { path, name } => {
253 let mut archive =
257 ZipArchive::new(std::fs::File::open(path)?).map_err(std::io::Error::other)?;
258 let mut entry = archive.by_name(name).map_err(std::io::Error::other)?;
259 let mut all = Vec::with_capacity(self.uncompressed_size as usize);
260 entry.read_to_end(&mut all)?;
261 let start = offset as usize;
262 let end = (start + total).min(all.len());
263 let slice = &all[start..end];
264 buf[..slice.len()].copy_from_slice(slice);
265 Ok(slice.len())
266 }
267 }
268 }
269}
270
271pub fn open_entry(path: &Path, name: &str) -> Result<StoredZipEntry, ZipCoreError> {
273 let file = std::fs::File::open(path)?;
274 let mut archive = ZipArchive::new(std::fs::File::open(path)?)?;
275 let entry = archive.by_name(name)?;
276 let uncompressed_size = entry.size();
277 let compressed_size = entry.compressed_size();
278 let data_start = entry.data_start();
279 let is_deflate = entry.compression() == CompressionMethod::Deflated;
280 let is_deflate64 = entry.compression() == CompressionMethod::Deflate64;
281 let is_stored = entry.compression() == CompressionMethod::Stored;
282 drop(entry);
283 drop(archive);
284
285 let layout = if is_stored {
286 Layout::StoredBlocks(vec![StoredBlock {
288 uncomp_start: 0,
289 len: uncompressed_size,
290 file_offset: data_start,
291 }])
292 } else if is_deflate {
293 match index_stored_blocks(&file, name, data_start, compressed_size, uncompressed_size)? {
294 Some(blocks) => Layout::StoredBlocks(blocks),
295 None => Layout::Fallback {
296 path: path.to_path_buf(),
297 name: name.to_string(),
298 },
299 }
300 } else if is_deflate64 {
301 match index_stored_blocks(&file, name, data_start, compressed_size, uncompressed_size)? {
305 Some(blocks) => Layout::StoredBlocks(blocks),
306 None => Layout::Deflate64(deflate64_seek::build_index(
307 &file,
308 name,
309 data_start,
310 compressed_size,
311 uncompressed_size,
312 deflate64_seek::DEFAULT_CHECKPOINT_INTERVAL,
313 )?),
314 }
315 } else {
316 Layout::Fallback {
317 path: path.to_path_buf(),
318 name: name.to_string(),
319 }
320 };
321
322 Ok(StoredZipEntry {
323 file,
324 uncompressed_size,
325 layout,
326 })
327}
328
329fn index_stored_blocks(
333 file: &std::fs::File,
334 name: &str,
335 data_start: u64,
336 compressed_size: u64,
337 uncompressed_size: u64,
338) -> Result<Option<Vec<StoredBlock>>, ZipCoreError> {
339 let end = data_start + compressed_size;
340 let mut blocks = Vec::new();
341 let mut foff = data_start;
342 let mut uoff = 0u64;
343 loop {
344 if foff + 5 > end {
345 return Ok(None);
347 }
348 let mut hdr = [0u8; 5];
349 pread_exact(file, &mut hdr, foff)?;
350 let bfinal = hdr[0] & 1;
351 let btype = (hdr[0] >> 1) & 0b11;
352 if btype != 0 {
353 return Ok(None); }
355 let len = u16::from_le_bytes([hdr[1], hdr[2]]);
356 let nlen = u16::from_le_bytes([hdr[3], hdr[4]]);
357 if nlen != !len {
358 return Err(ZipCoreError::Malformed {
359 entry: name.to_string(),
360 reason: format!("stored block LEN/NLEN mismatch at file offset {foff}"),
361 });
362 }
363 let len = u64::from(len);
364 let data_off = foff + 5;
365 if data_off + len > end {
366 return Err(ZipCoreError::Malformed {
367 entry: name.to_string(),
368 reason: format!("stored block overruns compressed data at offset {data_off}"),
369 });
370 }
371 blocks.push(StoredBlock {
372 uncomp_start: uoff,
373 len,
374 file_offset: data_off,
375 });
376 uoff += len;
377 foff = data_off + len;
378 if bfinal == 1 {
379 break;
380 }
381 }
382 if uoff != uncompressed_size {
383 return Err(ZipCoreError::Malformed {
384 entry: name.to_string(),
385 reason: format!(
386 "stored-block total {uoff} != entry uncompressed size {uncompressed_size}"
387 ),
388 });
389 }
390 Ok(Some(blocks))
391}
392
393#[cfg(unix)]
394fn pread_exact(file: &std::fs::File, buf: &mut [u8], offset: u64) -> std::io::Result<()> {
395 use std::os::unix::fs::FileExt;
396 file.read_exact_at(buf, offset)
397}
398
399#[cfg(windows)]
400fn pread_exact(file: &std::fs::File, buf: &mut [u8], offset: u64) -> std::io::Result<()> {
401 use std::os::windows::fs::FileExt;
402 let mut read = 0usize;
403 while read < buf.len() {
404 let n = file.seek_read(&mut buf[read..], offset + read as u64)?;
405 if n == 0 {
406 return Err(std::io::Error::new(
407 std::io::ErrorKind::UnexpectedEof,
408 "short positioned read",
409 ));
410 }
411 read += n;
412 }
413 Ok(())
414}