use super::inflate::inflate;
const END_OF_CENTRAL_DIRECTORY: u32 = 0x0605_4b50;
const CENTRAL_FILE_HEADER: u32 = 0x0201_4b50;
const LOCAL_FILE_HEADER: u32 = 0x0403_4b50;
const MAX_END_RECORD_SEARCH: usize = 22 + 0xffff;
pub const MAX_ENTRIES: usize = 4096;
const ZIP64_MARKER_16: u16 = 0xffff;
const ZIP64_MARKER_32: u32 = 0xffff_ffff;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ZipFault {
Container,
Damaged,
Unsupported,
Bounds,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ZipError {
pub entry: Option<String>,
pub message: String,
pub fault: ZipFault,
}
impl ZipError {
fn container(message: impl Into<String>) -> Self {
Self { entry: None, message: message.into(), fault: ZipFault::Container }
}
fn damaged(name: &str, message: impl Into<String>) -> Self {
Self {
entry: Some(name.to_owned()),
message: message.into(),
fault: ZipFault::Damaged,
}
}
fn bounds(name: &str, message: impl Into<String>) -> Self {
Self {
entry: Some(name.to_owned()),
message: message.into(),
fault: ZipFault::Bounds,
}
}
fn unsupported(entry: Option<&str>, message: impl Into<String>) -> Self {
Self {
entry: entry.map(str::to_owned),
message: message.into(),
fault: ZipFault::Unsupported,
}
}
}
impl core::fmt::Display for ZipError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match &self.entry {
Some(entry) => write!(f, "{entry}: {}", self.message),
None => f.write_str(&self.message),
}
}
}
#[derive(Clone, Debug)]
pub struct ZipEntry {
pub name: String,
method: u16,
crc32: u32,
compressed_size: usize,
uncompressed_size: usize,
local_header_offset: usize,
}
pub struct ZipArchive<'a> {
data: &'a [u8],
entries: Vec<ZipEntry>,
}
fn u16_at(data: &[u8], at: usize) -> Result<u16, ZipError> {
let bytes = data
.get(at..at.checked_add(2).ok_or_else(|| ZipError::container("ZIP offset overflow"))?)
.ok_or_else(|| ZipError::container("ZIP record ends before its fields"))?;
Ok(u16::from_le_bytes([bytes[0], bytes[1]]))
}
fn u32_at(data: &[u8], at: usize) -> Result<u32, ZipError> {
let bytes = data
.get(at..at.checked_add(4).ok_or_else(|| ZipError::container("ZIP offset overflow"))?)
.ok_or_else(|| ZipError::container("ZIP record ends before its fields"))?;
Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
fn add(a: usize, b: usize) -> Result<usize, ZipError> {
a.checked_add(b)
.ok_or_else(|| ZipError::container("ZIP offset arithmetic overflowed"))
}
fn crc32(data: &[u8]) -> u32 {
let mut crc = 0xffff_ffffu32;
for &byte in data {
crc ^= u32::from(byte);
for _ in 0..8 {
let mask = (crc & 1).wrapping_neg();
crc = (crc >> 1) ^ (0xedb8_8320 & mask);
}
}
!crc
}
impl<'a> ZipArchive<'a> {
pub fn open(data: &'a [u8]) -> Result<Self, ZipError> {
let end = find_end_record(data)?;
if u16_at(data, end + 4)? != 0 || u16_at(data, end + 6)? != 0 {
return Err(ZipError::unsupported(None, "the package spans several disks"));
}
let entry_count = u16_at(data, end + 10)?;
let directory_size = u32_at(data, end + 12)?;
let directory_offset = u32_at(data, end + 16)?;
if entry_count == ZIP64_MARKER_16
|| directory_size == ZIP64_MARKER_32
|| directory_offset == ZIP64_MARKER_32
{
return Err(ZipError::unsupported(None, "the package is ZIP64"));
}
let entry_count = entry_count as usize;
if entry_count > MAX_ENTRIES {
return Err(ZipError::container(format!(
"the package declares {entry_count} entries; at most {MAX_ENTRIES} are read"
)));
}
let directory_offset = directory_offset as usize;
let directory_end = add(directory_offset, directory_size as usize)?;
if directory_end > data.len() {
return Err(ZipError::container(
"the central directory runs past the end of the package",
));
}
let mut entries = Vec::with_capacity(entry_count);
let mut at = directory_offset;
for _ in 0..entry_count {
if u32_at(data, at)? != CENTRAL_FILE_HEADER {
return Err(ZipError::container(
"a central-directory record has the wrong signature",
));
}
let flags = u16_at(data, add(at, 8)?)?;
let method = u16_at(data, add(at, 10)?)?;
let crc = u32_at(data, add(at, 16)?)?;
let compressed_size = u32_at(data, add(at, 20)?)?;
let uncompressed_size = u32_at(data, add(at, 24)?)?;
let name_length = u16_at(data, add(at, 28)?)? as usize;
let extra_length = u16_at(data, add(at, 30)?)? as usize;
let comment_length = u16_at(data, add(at, 32)?)? as usize;
let local_header_offset = u32_at(data, add(at, 42)?)?;
let name_start = add(at, 46)?;
let name_end = add(name_start, name_length)?;
let name_bytes = data
.get(name_start..name_end)
.ok_or_else(|| ZipError::container("an entry name runs past the package"))?;
let name = String::from_utf8_lossy(name_bytes).into_owned();
if flags & 0x1 != 0 {
return Err(ZipError::unsupported(Some(&name), "the entry is encrypted"));
}
if compressed_size == ZIP64_MARKER_32
|| uncompressed_size == ZIP64_MARKER_32
|| local_header_offset == ZIP64_MARKER_32
{
return Err(ZipError::unsupported(Some(&name), "the entry is ZIP64"));
}
entries.push(ZipEntry {
name,
method,
crc32: crc,
compressed_size: compressed_size as usize,
uncompressed_size: uncompressed_size as usize,
local_header_offset: local_header_offset as usize,
});
at = add(add(name_end, extra_length)?, comment_length)?;
}
Ok(Self { data, entries })
}
pub fn find(&self, name: &str) -> Option<&ZipEntry> {
let wanted = name.trim_start_matches('/');
self.entries
.iter()
.find(|entry| entry.name == wanted)
.or_else(|| {
self.entries
.iter()
.find(|entry| entry.name.eq_ignore_ascii_case(wanted))
})
}
pub fn read(&self, entry: &ZipEntry, cap: usize) -> Result<Vec<u8>, ZipError> {
if entry.uncompressed_size > cap {
return Err(ZipError::bounds(
&entry.name,
format!(
"the part declares {} bytes; at most {cap} are read",
entry.uncompressed_size
),
));
}
if u32_at(self.data, entry.local_header_offset)? != LOCAL_FILE_HEADER {
return Err(ZipError::damaged(
&entry.name,
"the local file header has the wrong signature",
));
}
let name_length = u16_at(self.data, add(entry.local_header_offset, 26)?)? as usize;
let extra_length = u16_at(self.data, add(entry.local_header_offset, 28)?)? as usize;
let start = add(add(add(entry.local_header_offset, 30)?, name_length)?, extra_length)?;
let end = add(start, entry.compressed_size)?;
let compressed = self
.data
.get(start..end)
.ok_or_else(|| ZipError::damaged(&entry.name, "the entry data runs past the package"))?;
let plain = match entry.method {
0 => {
if entry.compressed_size != entry.uncompressed_size {
return Err(ZipError::damaged(
&entry.name,
"the stored entry's sizes disagree",
));
}
compressed.to_vec()
}
8 => inflate(compressed, entry.uncompressed_size)
.map_err(|error| ZipError::damaged(&entry.name, error.0))?,
method => {
return Err(ZipError::unsupported(
Some(&entry.name),
format!("compression method {method} is not read (only stored and deflate)"),
))
}
};
if plain.len() != entry.uncompressed_size {
return Err(ZipError::damaged(
&entry.name,
format!(
"the part expanded to {} bytes, not the declared {}",
plain.len(),
entry.uncompressed_size
),
));
}
if crc32(&plain) != entry.crc32 {
return Err(ZipError::damaged(&entry.name, "the part fails its CRC-32 check"));
}
Ok(plain)
}
}
fn find_end_record(data: &[u8]) -> Result<usize, ZipError> {
if data.len() < 22 {
return Err(ZipError::container(
"the file is too short to be a ZIP package",
));
}
let earliest = data.len().saturating_sub(MAX_END_RECORD_SEARCH);
let mut at = data.len() - 22;
loop {
if u32_at(data, at)? == END_OF_CENTRAL_DIRECTORY {
return Ok(at);
}
if at == earliest {
return Err(ZipError::container(
"no ZIP end-of-central-directory record (not a 3MF package)",
));
}
at -= 1;
}
}