#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
mod archive;
mod bytes;
mod codec;
mod cp437;
mod crypto;
pub use archive::{
ArchiveSummary, CompressionMethod, EntryLayout, HeaderFields, ZipArchive, ZipFile,
};
use std::io::Read;
use std::path::{Path, PathBuf};
#[derive(Debug, thiserror::Error)]
pub enum ZipCoreError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("malformed ZIP container: {0}")]
Format(#[from] FormatError),
#[error("unsupported compression method: {0:?}")]
UnsupportedMethod(CompressionMethod),
#[error(
"CRC-32 mismatch in entry {entry}: expected {expected:#010x}, computed {actual:#010x}"
)]
CrcMismatch {
entry: String,
expected: u32,
actual: u32,
},
#[error("entry is encrypted (password required): {0}")]
EncryptedNoPassword(String),
#[error("incorrect password for entry: {0}")]
WrongPassword(String),
#[error("unsupported encryption for entry {entry}: {reason}")]
UnsupportedEncryption {
entry: String,
reason: String,
},
#[error("entry not found: {0}")]
EntryNotFound(String),
#[error("entry index out of bounds: {0}")]
IndexOutOfBounds(usize),
#[error("malformed deflate stream in entry {entry}: {reason}")]
Malformed {
entry: String,
reason: String,
},
}
#[derive(Debug, thiserror::Error)]
pub enum FormatError {
#[error("unexpected end of data")]
Truncated,
#[error("End Of Central Directory record not found")]
NoEocd,
#[error("bad signature for {what} at offset {offset}")]
BadSignature {
what: &'static str,
offset: u64,
},
#[error("Zip64 archive not yet supported")]
Zip64Unsupported,
#[error("Zip64 sentinel without a matching Zip64 record/extra field")]
Zip64Inconsistent,
#[error("central directory out of range: offset {cd_offset}, size {cd_size}")]
CentralDirOutOfRange {
cd_offset: u64,
cd_size: u64,
},
#[error("declared entry count {0} exceeds the safety ceiling")]
TooManyEntries(usize),
}
#[derive(Debug, Clone, Copy)]
struct StoredBlock {
uncomp_start: u64,
len: u64,
file_offset: u64,
}
enum Layout {
StoredBlocks(Vec<StoredBlock>),
Fallback { path: PathBuf, name: String },
}
pub struct StoredZipEntry {
file: std::fs::File,
uncompressed_size: u64,
layout: Layout,
}
impl StoredZipEntry {
pub fn len(&self) -> u64 {
self.uncompressed_size
}
pub fn is_empty(&self) -> bool {
self.uncompressed_size == 0
}
pub fn is_stored_block_indexed(&self) -> bool {
matches!(self.layout, Layout::StoredBlocks(_))
}
pub fn block_count(&self) -> usize {
match &self.layout {
Layout::StoredBlocks(b) => b.len(),
Layout::Fallback { .. } => 0,
}
}
pub fn read_at(&self, buf: &mut [u8], offset: u64) -> std::io::Result<usize> {
if offset >= self.uncompressed_size || buf.is_empty() {
return Ok(0);
}
let want_end = (offset + buf.len() as u64).min(self.uncompressed_size);
let total = (want_end - offset) as usize;
match &self.layout {
Layout::StoredBlocks(blocks) => {
let mut filled = 0usize;
let mut cur = offset;
while cur < want_end {
let bi = blocks.partition_point(|b| b.uncomp_start + b.len <= cur);
let Some(b) = blocks.get(bi) else {
break; };
let within = cur - b.uncomp_start;
let avail = b.len - within;
let n = avail.min(want_end - cur) as usize;
pread_exact(
&self.file,
&mut buf[filled..filled + n],
b.file_offset + within,
)?;
filled += n;
cur += n as u64;
}
Ok(filled)
}
Layout::Fallback { path, name } => {
let mut archive =
ZipArchive::new(std::fs::File::open(path)?).map_err(std::io::Error::other)?;
let mut entry = archive.by_name(name).map_err(std::io::Error::other)?;
let mut all = Vec::with_capacity(self.uncompressed_size as usize);
entry.read_to_end(&mut all)?;
let start = offset as usize;
let end = (start + total).min(all.len());
let slice = &all[start..end];
buf[..slice.len()].copy_from_slice(slice);
Ok(slice.len())
}
}
}
}
pub fn open_entry(path: &Path, name: &str) -> Result<StoredZipEntry, ZipCoreError> {
let file = std::fs::File::open(path)?;
let mut archive = ZipArchive::new(std::fs::File::open(path)?)?;
let entry = archive.by_name(name)?;
let uncompressed_size = entry.size();
let compressed_size = entry.compressed_size();
let data_start = entry.data_start();
let is_deflate = entry.compression() == CompressionMethod::Deflated;
let is_stored = entry.compression() == CompressionMethod::Stored;
drop(entry);
drop(archive);
let layout = if is_stored {
Layout::StoredBlocks(vec![StoredBlock {
uncomp_start: 0,
len: uncompressed_size,
file_offset: data_start,
}])
} else if is_deflate {
match index_stored_blocks(&file, name, data_start, compressed_size, uncompressed_size)? {
Some(blocks) => Layout::StoredBlocks(blocks),
None => Layout::Fallback {
path: path.to_path_buf(),
name: name.to_string(),
},
}
} else {
Layout::Fallback {
path: path.to_path_buf(),
name: name.to_string(),
}
};
Ok(StoredZipEntry {
file,
uncompressed_size,
layout,
})
}
fn index_stored_blocks(
file: &std::fs::File,
name: &str,
data_start: u64,
compressed_size: u64,
uncompressed_size: u64,
) -> Result<Option<Vec<StoredBlock>>, ZipCoreError> {
let end = data_start + compressed_size;
let mut blocks = Vec::new();
let mut foff = data_start;
let mut uoff = 0u64;
loop {
if foff + 5 > end {
return Ok(None);
}
let mut hdr = [0u8; 5];
pread_exact(file, &mut hdr, foff)?;
let bfinal = hdr[0] & 1;
let btype = (hdr[0] >> 1) & 0b11;
if btype != 0 {
return Ok(None); }
let len = u16::from_le_bytes([hdr[1], hdr[2]]);
let nlen = u16::from_le_bytes([hdr[3], hdr[4]]);
if nlen != !len {
return Err(ZipCoreError::Malformed {
entry: name.to_string(),
reason: format!("stored block LEN/NLEN mismatch at file offset {foff}"),
});
}
let len = u64::from(len);
let data_off = foff + 5;
if data_off + len > end {
return Err(ZipCoreError::Malformed {
entry: name.to_string(),
reason: format!("stored block overruns compressed data at offset {data_off}"),
});
}
blocks.push(StoredBlock {
uncomp_start: uoff,
len,
file_offset: data_off,
});
uoff += len;
foff = data_off + len;
if bfinal == 1 {
break;
}
}
if uoff != uncompressed_size {
return Err(ZipCoreError::Malformed {
entry: name.to_string(),
reason: format!(
"stored-block total {uoff} != entry uncompressed size {uncompressed_size}"
),
});
}
Ok(Some(blocks))
}
#[cfg(unix)]
fn pread_exact(file: &std::fs::File, buf: &mut [u8], offset: u64) -> std::io::Result<()> {
use std::os::unix::fs::FileExt;
file.read_exact_at(buf, offset)
}
#[cfg(windows)]
fn pread_exact(file: &std::fs::File, buf: &mut [u8], offset: u64) -> std::io::Result<()> {
use std::os::windows::fs::FileExt;
let mut read = 0usize;
while read < buf.len() {
let n = file.seek_read(&mut buf[read..], offset + read as u64)?;
if n == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"short positioned read",
));
}
read += n;
}
Ok(())
}