use crate::errors::{Error, ErrorKind};
use crate::utils::{le_u16, le_u32, le_u64};
use crate::{
END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE, Zip64EndOfCentralDirectory,
Zip64EndOfCentralDirectoryRecord, ZipFileHeaderFixed, ZipSliceArchive,
};
use core::num::NonZeroU64;
const END_OF_CENTRAL_DIR_SIGNATURE: u32 = 0x06054b50;
#[cfg(any(feature = "std", test))]
pub(crate) const END_OF_CENTRAL_DIR_SIGNATURE_BYTES: [u8; 4] =
END_OF_CENTRAL_DIR_SIGNATURE.to_le_bytes();
#[cfg(feature = "std")]
mod reader;
const END_OF_CENTRAL_DIR_MAX_OFFSET: u64 = 1 << 20;
#[derive(Debug)]
pub struct ZipLocator {
max_search_space: u64,
}
impl Default for ZipLocator {
fn default() -> Self {
Self::new()
}
}
impl ZipLocator {
pub fn new() -> Self {
ZipLocator {
max_search_space: END_OF_CENTRAL_DIR_MAX_OFFSET,
}
}
pub fn max_search_space(mut self, max_search_space: u64) -> Self {
self.max_search_space = max_search_space;
self
}
fn locate_in_byte_slice(&self, data: &[u8]) -> Result<EndOfCentralDirectory, Error> {
let location = find_end_of_central_dir_signature(data, self.max_search_space as usize)
.ok_or(ErrorKind::MissingEndOfCentralDirectory)?;
let mut eocd = self
.locate_in_byte_slice_impl(data, location)
.map_err(|e| e.with_eocd_offset(location as u64))?;
let first_entry = data
.get(eocd.central_dir_offset as usize..)
.filter(|d| ZipFileHeaderFixed::parse(d).is_ok());
match first_entry {
None if !eocd.is_zip64() => {
let cd_offset = eocd.eocd_offset.saturating_sub(eocd.central_dir_size);
let first_entry = data
.get(cd_offset as usize..)
.filter(|d| ZipFileHeaderFixed::parse(d).is_ok());
if first_entry.is_some() {
eocd.base_offset = cd_offset.saturating_sub(eocd.central_dir_offset);
eocd.central_dir_offset = cd_offset;
}
Ok(eocd)
}
_ => Ok(eocd),
}
}
fn locate_in_byte_slice_impl(
&self,
data: &[u8],
location: usize,
) -> Result<EndOfCentralDirectory, Error> {
let eocd = EndOfCentralDirectoryRecordFixed::parse(&data[location..])?;
let comment_start = location + EndOfCentralDirectoryRecordFixed::SIZE;
let comment_len = eocd.comment_len as usize;
if comment_start + comment_len > data.len() {
return Err(Error::from(ErrorKind::Eof));
}
let zip64_locator = location
.checked_sub(Zip64EndOfCentralDirectoryLocatorRecord::SIZE)
.and_then(|start| Zip64EndOfCentralDirectoryLocatorRecord::parse(&data[start..]).ok());
let Some(zip64_locator) = zip64_locator else {
return EndOfCentralDirectory::create(EndOfCentralDirectoryRecord::from_parts(
location as u64,
&eocd,
));
};
let zip64_eocd = &data[(zip64_locator.directory_offset as usize).min(data.len())..];
let zip64_record = match parse_zip64_candidate(
zip64_eocd,
&eocd,
&zip64_locator,
location as u64 - Zip64EndOfCentralDirectoryLocatorRecord::SIZE as u64,
) {
Some(record) => record,
None => {
return EndOfCentralDirectory::create(EndOfCentralDirectoryRecord::from_parts(
location as u64,
&eocd,
));
}
};
let zip64 =
Zip64EndOfCentralDirectory::from_parts(zip64_locator.directory_offset, zip64_record);
let eocd = EndOfCentralDirectoryRecord::from_parts(location as u64, &eocd);
EndOfCentralDirectory::create_zip64(eocd, zip64)
}
pub fn locate_in_slice<T: AsRef<[u8]>>(
&self,
data: T,
) -> Result<ZipSliceArchive<T>, (T, Error)> {
match self.locate_in_byte_slice(data.as_ref()) {
Ok(eocd) => Ok(ZipSliceArchive::new(data, eocd)),
Err(e) => Err((data, e)),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct EndOfCentralDirectory {
eocd_offset: u64,
zip64_eocd_offset: Option<NonZeroU64>,
central_dir_size: u64,
central_dir_offset: u64,
num_entries: u64,
comment_len: u16,
base_offset: u64,
}
impl EndOfCentralDirectory {
pub(crate) fn create(eocd: EndOfCentralDirectoryRecord) -> Result<Self, Error> {
let result = EndOfCentralDirectory {
eocd_offset: eocd.offset,
zip64_eocd_offset: None,
central_dir_size: u64::from(eocd.central_dir_size),
central_dir_offset: u64::from(eocd.central_dir_offset),
num_entries: u64::from(eocd.num_entries),
comment_len: eocd.comment_len,
base_offset: 0,
};
result.validate()?;
Ok(result)
}
pub(crate) fn create_zip64(
eocd: EndOfCentralDirectoryRecord,
zip64: Zip64EndOfCentralDirectory,
) -> Result<Self, Error> {
let result = EndOfCentralDirectory {
eocd_offset: eocd.offset,
zip64_eocd_offset: NonZeroU64::new(zip64.offset),
central_dir_size: zip64.central_dir_size,
central_dir_offset: zip64.central_dir_offset,
num_entries: zip64.num_entries,
comment_len: eocd.comment_len,
base_offset: 0,
};
result.validate()?;
Ok(result)
}
fn validate(&self) -> Result<(), Error> {
if self.directory_offset() > self.head_eocd_offset() {
return Err(Error::from(ErrorKind::InvalidEndOfCentralDirectory));
}
Ok(())
}
#[inline]
pub(crate) fn is_zip64(&self) -> bool {
self.zip64_eocd_offset.is_some()
}
pub(crate) fn base_offset(&self) -> u64 {
self.base_offset
}
#[inline]
pub(crate) fn head_eocd_offset(&self) -> u64 {
self.zip64_eocd_offset
.map(core::num::NonZero::get)
.unwrap_or(self.eocd_offset)
}
#[inline]
pub(crate) fn tail_eocd_offset(&self) -> u64 {
self.eocd_offset
}
#[inline]
pub(crate) fn directory_offset(&self) -> u64 {
self.central_dir_offset
}
#[inline]
pub(crate) fn entries(&self) -> u64 {
self.num_entries
}
#[inline]
pub(crate) fn comment_len(&self) -> usize {
self.comment_len as usize
}
}
#[derive(Debug, Clone)]
pub(crate) struct EndOfCentralDirectoryRecord {
pub(crate) offset: u64,
pub(crate) central_dir_size: u32,
pub(crate) central_dir_offset: u32,
pub(crate) num_entries: u16,
pub(crate) comment_len: u16,
}
impl EndOfCentralDirectoryRecord {
#[inline]
pub fn from_parts(offset: u64, eocd: &EndOfCentralDirectoryRecordFixed) -> Self {
Self {
offset,
central_dir_size: eocd.central_dir_size,
central_dir_offset: eocd.central_dir_offset,
num_entries: eocd.total_entries,
comment_len: eocd.comment_len,
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct EndOfCentralDirectoryRecordFixed {
pub(crate) signature: u32,
#[allow(dead_code)]
pub(crate) disk_number: u16,
#[allow(dead_code)]
pub(crate) eocd_disk: u16,
#[allow(dead_code)]
pub(crate) num_entries: u16,
pub(crate) total_entries: u16,
pub(crate) central_dir_size: u32,
pub(crate) central_dir_offset: u32,
pub(crate) comment_len: u16,
}
impl EndOfCentralDirectoryRecordFixed {
pub(crate) const SIZE: usize = 22;
pub fn parse(data: &[u8]) -> Result<EndOfCentralDirectoryRecordFixed, Error> {
if data.len() < Self::SIZE {
return Err(Error::from(ErrorKind::Eof));
}
let result = EndOfCentralDirectoryRecordFixed {
signature: le_u32(&data[0..4]),
disk_number: le_u16(&data[4..6]),
eocd_disk: le_u16(&data[6..8]),
num_entries: le_u16(&data[8..10]),
total_entries: le_u16(&data[10..12]),
central_dir_size: le_u32(&data[12..16]),
central_dir_offset: le_u32(&data[16..20]),
comment_len: le_u16(&data[20..22]),
};
if result.signature != END_OF_CENTRAL_DIR_SIGNATURE {
return Err(Error::from(ErrorKind::InvalidSignature {
expected: END_OF_CENTRAL_DIR_SIGNATURE,
actual: result.signature,
}));
}
Ok(result)
}
}
#[derive(Debug)]
#[allow(dead_code)]
struct Zip64EndOfCentralDirectoryLocatorRecord {
pub signature: u32,
pub eocd_disk: u32,
pub directory_offset: u64,
pub total_disks: u32,
}
impl Zip64EndOfCentralDirectoryLocatorRecord {
const SIZE: usize = 20;
pub fn parse(data: &[u8]) -> Result<Zip64EndOfCentralDirectoryLocatorRecord, Error> {
if data.len() < Self::SIZE {
return Err(Error::from(ErrorKind::Eof));
}
let result = Zip64EndOfCentralDirectoryLocatorRecord {
signature: le_u32(&data[0..4]),
eocd_disk: le_u32(&data[4..8]),
directory_offset: le_u64(&data[8..16]),
total_disks: le_u32(&data[16..20]),
};
if result.signature != END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE {
return Err(Error::from(ErrorKind::InvalidSignature {
expected: END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE,
actual: result.signature,
}));
}
Ok(result)
}
}
fn parse_zip64_candidate(
data: &[u8],
classic: &EndOfCentralDirectoryRecordFixed,
locator: &Zip64EndOfCentralDirectoryLocatorRecord,
locator_offset: u64,
) -> Option<Zip64EndOfCentralDirectoryRecord> {
const ZIP64_SIZE_PREFIX: u64 = 12;
const ZIP64_MIN_SIZE: u64 = Zip64EndOfCentralDirectoryRecord::SIZE as u64 - ZIP64_SIZE_PREFIX;
let zip64 = Zip64EndOfCentralDirectoryRecord::parse(data).ok()?;
if zip64.size < ZIP64_MIN_SIZE {
return None;
}
let record_end = locator
.directory_offset
.checked_add(ZIP64_SIZE_PREFIX)?
.checked_add(zip64.size)?;
let consistent = record_end == locator_offset
&& (classic.num_entries == u16::MAX || u64::from(classic.num_entries) == zip64.num_entries)
&& (classic.total_entries == u16::MAX
|| u64::from(classic.total_entries) == zip64.total_entries)
&& (classic.central_dir_size == u32::MAX
|| u64::from(classic.central_dir_size) == zip64.central_dir_size)
&& (classic.central_dir_offset == u32::MAX
|| u64::from(classic.central_dir_offset) == zip64.central_dir_offset);
consistent.then_some(zip64)
}
pub(crate) fn find_end_of_central_dir_signature(
data: &[u8],
max_search_space: usize,
) -> Option<usize> {
let start_search = data.len().saturating_sub(max_search_space);
rfind::<END_OF_CENTRAL_DIR_SIGNATURE>(&data[start_search..]).map(|pos| pos + start_search)
}
fn rfind<const NEEDLE: u32>(haystack: &[u8]) -> Option<usize> {
const N: usize = core::mem::size_of::<u32>();
if haystack.len() < N {
return None;
}
let search = &haystack[N - 1..];
const LANES: usize = core::mem::size_of::<u64>();
let lo = u64::from_ne_bytes([0x01; LANES]);
let lo7 = u64::from_ne_bytes([0x7F; LANES]);
let hi = u64::from_ne_bytes([0x80; LANES]);
let haszero = |word: u64| word.wrapping_sub(lo) & !word & hi;
let iszero = |word: u64| !(((word & lo7) + lo7) | word) & hi;
let bytes = NEEDLE.to_le_bytes();
let bc = bytes.map(|x| u64::from_ne_bytes([x; LANES]));
let load = |at: usize| -> u64 {
let c = &haystack[at..at + LANES];
u64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]])
};
let mut chunks = search.rchunks_exact(LANES);
let mut start = search.len();
for c in chunks.by_ref() {
start -= LANES;
let word = u64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]]);
let cand = haszero(word ^ bc[N - 1]);
if cand == 0 || cand & haszero(load(start) ^ bc[0]) == 0 {
continue;
}
if haszero(load(start + 1) ^ bc[1]) & haszero(load(start + 2) ^ bc[2]) == 0 {
continue;
}
let matches = iszero(word ^ bc[N - 1])
& iszero(load(start) ^ bc[0])
& iszero(load(start + 1) ^ bc[1])
& iszero(load(start + 2) ^ bc[2]);
if matches != 0 {
return Some(start + matches.ilog2() as usize / 8);
}
}
haystack[..chunks.remainder().len() + N - 1]
.windows(N)
.rposition(|window| le_u32(window) == NEEDLE)
}
#[cfg(test)]
mod tests {
use super::*;
use quickcheck_macros::quickcheck;
use rstest::rstest;
#[rstest]
#[case(&[], None)]
#[case(&[0x50, 0x4b, 0x05, 0x06], Some(0))]
#[case(&[0x50, 0x4b, 0x05, 0x06, 0x50, 0x4b, 0x05, 0x06], Some(4))]
#[case(&[0x50, 0x51, 0x4b, 0x05, 0x06, 0xff, 0xff, 0x07, 0x01, 0x50, 0x00], None)]
fn test_rfind(#[case] input: &[u8], #[case] expected: Option<usize>) {
assert_eq!(rfind::<END_OF_CENTRAL_DIR_SIGNATURE>(input), expected);
}
#[quickcheck]
fn test_rfind_matches_windows_rposition(data: Vec<u8>) {
let expected = data
.windows(END_OF_CENTRAL_DIR_SIGNATURE_BYTES.len())
.rposition(|window| window == END_OF_CENTRAL_DIR_SIGNATURE_BYTES);
assert_eq!(rfind::<END_OF_CENTRAL_DIR_SIGNATURE>(&data), expected);
}
}