use pith_digest::{Error, Result, crc32};
use pith_inflate::{Limits, inflate_raw};
const SIG_LOCAL: u32 = 0x0403_4b50;
const SIG_CENTRAL: u32 = 0x0201_4b50;
const SIG_EOCD: u32 = 0x0605_4b50;
const SIG_DESCRIPTOR: u32 = 0x0807_4b50;
const METHOD_STORED: u16 = 0;
const METHOD_DEFLATE: u16 = 8;
const EOCD_LEN: usize = 22;
const CENTRAL_LEN: usize = 46;
const LOCAL_LEN: usize = 30;
const EOCD_MAX_COMMENT: usize = 65_535;
const FLAG_ENCRYPTED: u16 = 1 << 0;
const FLAG_DESCRIPTOR: u16 = 1 << 3;
const FLAG_STRONG_ENCRYPTED: u16 = 1 << 6;
const FLAG_MASKED_LOCAL: u16 = 1 << 13;
fn le16(data: &[u8], off: usize) -> Option<u16> {
let b = data.get(off..off + 2)?;
Some(u16::from_le_bytes([b[0], b[1]]))
}
fn le32(data: &[u8], off: usize) -> Option<u32> {
let b = data.get(off..off + 4)?;
Some(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
}
fn take<'a>(data: &'a [u8], off: usize, len: usize, what: &'static str) -> Result<&'a [u8]> {
let end = off
.checked_add(len)
.ok_or_else(|| Error::truncated(what, usize::MAX, data.len()))?;
data.get(off..end)
.ok_or_else(|| Error::truncated(what, end, data.len()))
}
struct Eocd {
entries: u16,
cd_size: u32,
cd_offset: u32,
offset: usize,
}
fn find_eocd(data: &[u8]) -> Result<Eocd> {
let start = data.len().saturating_sub(EOCD_LEN + EOCD_MAX_COMMENT);
let mut off = data.len().saturating_sub(EOCD_LEN);
loop {
if le32(data, off) == Some(SIG_EOCD) {
let comment_len = le16(data, off + 20).ok_or_else(|| {
Error::truncated("end of central directory", off + EOCD_LEN, data.len())
})? as usize;
if off + EOCD_LEN + comment_len == data.len() {
return parse_eocd(data, off);
}
}
if off == start {
return Err(Error::InvalidMagic {
what: "zip end of central directory",
});
}
off -= 1;
}
}
fn parse_eocd(data: &[u8], off: usize) -> Result<Eocd> {
let fields = take(data, off + 4, EOCD_LEN - 4, "end of central directory")?;
let disk = u16::from_le_bytes([fields[0], fields[1]]);
let cd_disk = u16::from_le_bytes([fields[2], fields[3]]);
let entries_disk = u16::from_le_bytes([fields[4], fields[5]]);
let entries = u16::from_le_bytes([fields[6], fields[7]]);
let cd_size = u32::from_le_bytes([fields[8], fields[9], fields[10], fields[11]]);
let cd_offset = u32::from_le_bytes([fields[12], fields[13], fields[14], fields[15]]);
if disk != 0 || cd_disk != 0 || entries_disk != entries {
return Err(Error::Unsupported("multi-disk zip archive"));
}
if entries == 0xFFFF || cd_size == 0xFFFF_FFFF || cd_offset == 0xFFFF_FFFF {
return Err(Error::Unsupported("zip64 archive"));
}
Ok(Eocd {
entries,
cd_size,
cd_offset,
offset: off,
})
}
#[derive(Copy, Clone, Debug)]
pub struct ZipEntry<'a> {
name: &'a str,
flags: u16,
method: u16,
crc32: u32,
compressed_size: u32,
uncompressed_size: u32,
local_offset: u32,
}
impl<'a> ZipEntry<'a> {
pub fn name(&self) -> &'a str {
self.name
}
pub fn method(&self) -> u16 {
self.method
}
pub fn compressed_size(&self) -> u64 {
u64::from(self.compressed_size)
}
pub fn uncompressed_size(&self) -> u64 {
u64::from(self.uncompressed_size)
}
pub fn crc32(&self) -> u32 {
self.crc32
}
pub fn uses_data_descriptor(&self) -> bool {
self.flags & FLAG_DESCRIPTOR != 0
}
}
#[derive(Clone, Debug)]
pub struct ZipArchive<'a> {
data: &'a [u8],
entries: Vec<ZipEntry<'a>>,
}
impl<'a> ZipArchive<'a> {
pub fn new(data: &'a [u8]) -> Result<Self> {
let eocd = find_eocd(data)?;
let cd_offset = usize::try_from(eocd.cd_offset).unwrap_or(usize::MAX);
let cd_size = usize::try_from(eocd.cd_size).unwrap_or(usize::MAX);
let cd_end = cd_offset
.checked_add(cd_size)
.ok_or_else(|| Error::truncated("zip central directory", usize::MAX, data.len()))?;
if cd_end > eocd.offset {
return Err(Error::truncated(
"zip central directory",
cd_end,
eocd.offset,
));
}
let dir = take(data, cd_offset, cd_size, "zip central directory")?;
let mut entries = Vec::with_capacity(usize::from(eocd.entries));
let mut pos = 0usize;
for _ in 0..eocd.entries {
let fixed = take(dir, pos, CENTRAL_LEN, "zip central directory entry")?;
let magic = u32::from_le_bytes([fixed[0], fixed[1], fixed[2], fixed[3]]);
if magic != SIG_CENTRAL {
return Err(Error::InvalidMagic {
what: "zip central directory entry",
});
}
let flags = u16::from_le_bytes([fixed[8], fixed[9]]);
let method = u16::from_le_bytes([fixed[10], fixed[11]]);
let crc32 = u32::from_le_bytes([fixed[16], fixed[17], fixed[18], fixed[19]]);
let compressed_size = u32::from_le_bytes([fixed[20], fixed[21], fixed[22], fixed[23]]);
let uncompressed_size =
u32::from_le_bytes([fixed[24], fixed[25], fixed[26], fixed[27]]);
let name_len = usize::from(u16::from_le_bytes([fixed[28], fixed[29]]));
let extra_len = usize::from(u16::from_le_bytes([fixed[30], fixed[31]]));
let comment_len = usize::from(u16::from_le_bytes([fixed[32], fixed[33]]));
let local_offset = u32::from_le_bytes([fixed[42], fixed[43], fixed[44], fixed[45]]);
if compressed_size == 0xFFFF_FFFF
|| uncompressed_size == 0xFFFF_FFFF
|| local_offset == 0xFFFF_FFFF
{
return Err(Error::Unsupported("zip64 central directory entry"));
}
let name_bytes = take(
dir,
pos + CENTRAL_LEN,
name_len,
"zip entry name in central directory",
)?;
let name = core::str::from_utf8(name_bytes)
.map_err(|_| Error::BadValue("zip entry name is not UTF-8"))?;
let entry_len = CENTRAL_LEN
.checked_add(name_len)
.and_then(|n| n.checked_add(extra_len))
.and_then(|n| n.checked_add(comment_len))
.ok_or(Error::BadValue("zip central directory entry length"))?;
if entry_len > dir.len() - pos {
return Err(Error::truncated(
"zip central directory entry",
pos + entry_len,
dir.len(),
));
}
entries.push(ZipEntry {
name,
flags,
method,
crc32,
compressed_size,
uncompressed_size,
local_offset,
});
pos += entry_len;
}
if pos != dir.len() {
return Err(Error::BadValue("zip central directory size"));
}
Ok(ZipArchive { data, entries })
}
pub fn entries(&self) -> &[ZipEntry<'a>] {
&self.entries
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn by_name(&self, name: &str) -> Option<&ZipEntry<'a>> {
self.entries.iter().find(|e| e.name == name)
}
pub fn extract(&self, entry: &ZipEntry<'a>) -> Result<Vec<u8>> {
self.extract_with(entry, &Limits::default())
}
pub fn extract_by_name(&self, name: &str) -> Result<Vec<u8>> {
let entry = self
.by_name(name)
.ok_or(Error::BadValue("no such zip entry"))?;
self.extract(entry)
}
pub fn extract_with(&self, entry: &ZipEntry<'a>, limits: &Limits) -> Result<Vec<u8>> {
let base = usize::try_from(entry.local_offset)
.map_err(|_| Error::BadValue("zip local header offset"))?;
let hdr = take(self.data, base, LOCAL_LEN, "zip local file header")?;
let magic = u32::from_le_bytes([hdr[0], hdr[1], hdr[2], hdr[3]]);
if magic != SIG_LOCAL {
return Err(Error::InvalidMagic {
what: "zip local file header",
});
}
let local_flags = u16::from_le_bytes([hdr[6], hdr[7]]);
let local_method = u16::from_le_bytes([hdr[8], hdr[9]]);
if local_flags != entry.flags {
return Err(Error::BadValue("zip flags differ local vs central"));
}
if local_method != entry.method {
return Err(Error::BadValue("zip method differs local vs central"));
}
let name_len = usize::from(u16::from_le_bytes([hdr[26], hdr[27]]));
let extra_len = usize::from(u16::from_le_bytes([hdr[28], hdr[29]]));
let data_start = base
.checked_add(LOCAL_LEN)
.and_then(|n| n.checked_add(name_len))
.and_then(|n| n.checked_add(extra_len))
.ok_or(Error::BadValue("zip local header offset"))?;
if entry.flags & (FLAG_ENCRYPTED | FLAG_STRONG_ENCRYPTED | FLAG_MASKED_LOCAL) != 0 {
return Err(Error::Unsupported("encrypted zip entry"));
}
let cs = usize::try_from(entry.compressed_size).unwrap_or(usize::MAX);
let payload = take(self.data, data_start, cs, "zip entry payload")?;
if entry.flags & FLAG_DESCRIPTOR != 0 {
let dd_at = data_start
.checked_add(cs)
.ok_or(Error::BadValue("zip data descriptor offset"))?;
self.verify_descriptor(entry, dd_at)?;
}
let out = match entry.method {
METHOD_STORED => {
if entry.uncompressed_size != entry.compressed_size {
return Err(Error::BadValue("zip stored entry size"));
}
if cs > limits.max_output {
return Err(Error::too_large("zip entry", limits.max_output));
}
payload.to_vec()
}
METHOD_DEFLATE => inflate_raw(payload, limits)?,
_ => return Err(Error::Unsupported("zip compression method")),
};
if out.len() as u64 != u64::from(entry.uncompressed_size) {
return Err(Error::BadValue("zip entry size mismatch"));
}
if crc32(&out) != entry.crc32 {
return Err(Error::BadValue("zip entry crc32 mismatch"));
}
Ok(out)
}
fn verify_descriptor(&self, entry: &ZipEntry<'a>, at: usize) -> Result<()> {
let base = if le32(self.data, at) == Some(SIG_DESCRIPTOR) {
at + 4
} else {
at
};
let dd = take(self.data, base, 12, "zip data descriptor")?;
let dd_crc = u32::from_le_bytes([dd[0], dd[1], dd[2], dd[3]]);
let dd_cs = u32::from_le_bytes([dd[4], dd[5], dd[6], dd[7]]);
let dd_us = u32::from_le_bytes([dd[8], dd[9], dd[10], dd[11]]);
if dd_crc == entry.crc32
&& dd_cs == entry.compressed_size
&& dd_us == entry.uncompressed_size
{
Ok(())
} else {
Err(Error::BadValue("zip data descriptor mismatch"))
}
}
}