mod layout;
mod source;
mod writer;
use std::io::{self, Read, Seek, SeekFrom};
use zip::ZipArchive;
use zip::read::{ArchiveOffset, Config};
pub use layout::ApkLayout;
pub(crate) use layout::SIGNING_BLOCK_MAGIC;
pub(crate) use source::ReadAt;
pub(crate) use writer::ApkWriter;
pub use writer::{Compression, EntryOptions};
use crate::types::ArchivePathError;
pub use crate::Content;
pub use crate::digest::DigestAlgorithm;
pub(crate) fn is_signed_entry(name: &str) -> bool {
!name.ends_with('/') && !is_signature_file(name)
}
pub(crate) fn is_signature_file(name: &str) -> bool {
let Some(file) = name.strip_prefix("META-INF/").filter(|f| !f.contains('/')) else { return false };
let file = file.to_ascii_lowercase();
file == "manifest.mf"
|| file.starts_with("sig-")
|| [".sf", ".rsa", ".dsa", ".ec"].iter().any(|ext| file.ends_with(ext))
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum ZipLimit {
Size,
EntryCount,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ZipError {
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Zip(#[from] zip::result::ZipError),
#[error("no end of central directory record found")]
MissingEndOfCentralDirectory,
#[error("ZIP64 archives are not supported by APK signature schemes")]
Zip64,
#[error("multi-disk archives are not supported")]
MultiDisk,
#[error("central directory at {offset:#x} with size {size} does not end at the EOCD record at {eocd:#x}")]
InconsistentCentralDirectory { offset: u64, size: u64, eocd: u64 },
#[error("the central directory reads differently to different ZIP parsers")]
AmbiguousCentralDirectory,
#[error("the first entry starts at offset {offset:#x}, not at the start of the file")]
DataBeforeEntries { offset: u64 },
#[error("entry {index}'s local header is malformed")]
BadLocalHeader { index: usize },
#[error("entry {index}'s data runs past the end of the entries, into the signing block or central directory")]
EntryOutOfBounds { index: usize },
#[error("APK signing block size fields disagree ({leading} vs {trailing})")]
SigningBlockSizeMismatch { leading: u64, trailing: u64 },
#[error("APK signing block of {size} bytes does not fit before the central directory")]
SigningBlockOutOfBounds { size: u64 },
#[error("entry {index} has a name that is not valid UTF-8")]
NonUtf8Name { index: usize },
#[error("entry name {name:?} is unsafe: {source}")]
UnsafeName { name: String, source: ArchivePathError },
#[error("the archive has no entry {name:?}")]
MissingEntry { name: String },
#[error("entry {name:?} decompresses to more than {limit} bytes")]
EntryTooLarge { name: String, limit: u64 },
#[error("entry {name:?} appears more than once")]
DuplicateEntry { name: String },
#[error("the central directory lists {listed} entries but only {distinct} distinct names")]
DuplicateNames { listed: u16, distinct: usize },
#[error("entry {name:?} is encrypted")]
EncryptedEntry { name: String },
#[error("archive exceeds the classic ZIP {limit:?} limit")]
LimitExceeded { limit: ZipLimit },
#[error("input {name:?} changed size while it was archived (expected {expected} bytes, read {actual})")]
SourceChanged { name: String, expected: u64, actual: u64 },
}
pub(crate) fn open<R: Read + Seek>(mut reader: R) -> Result<(ZipArchive<R>, ApkLayout), ZipError> {
let layout = ApkLayout::read(&mut reader)?;
let cd = &layout.central_directory;
let mut central_directory = vec![0; (cd.end - cd.start) as usize];
read_at(&mut reader, cd.start, &mut central_directory)?;
let records = central_records(¢ral_directory)?;
if records.len() != usize::from(layout.entry_count()) {
return Err(ZipError::AmbiguousCentralDirectory);
}
check_placement(&mut reader, &records, layout.entries_end)?;
let config = Config { archive_offset: ArchiveOffset::Known(0) };
let mut archive = ZipArchive::with_config(config, reader)?;
if archive.central_directory_start() != layout.central_directory.start {
return Err(ZipError::AmbiguousCentralDirectory);
}
if archive.len() != records.len() {
return Err(ZipError::DuplicateNames { listed: layout.entry_count(), distinct: archive.len() });
}
archive.validate_header_consistency()?;
for (index, record) in records.iter().enumerate() {
let entry = archive.by_index_data(index)?;
let agrees = entry.name_raw() == record.name
&& entry.header_start() == u64::from(record.offset)
&& entry.compressed_size() == u64::from(record.compressed_size)
&& entry.size() == u64::from(record.size);
if !agrees {
return Err(ZipError::AmbiguousCentralDirectory);
}
}
Ok((archive, layout))
}
struct CentralRecord<'a> {
name: &'a [u8],
flags: u16,
compressed_size: u32,
size: u32,
offset: u32,
}
fn central_records(mut central_directory: &[u8]) -> Result<Vec<CentralRecord<'_>>, ZipError> {
let mut records = Vec::new();
while !central_directory.is_empty() {
let fixed = central_directory.get(..46).filter(|fixed| fixed[..4] == *b"PK\x01\x02");
let fixed = fixed.ok_or(ZipError::AmbiguousCentralDirectory)?;
let u16_at = |at: usize| u16::from_le_bytes([fixed[at], fixed[at + 1]]);
let u32_at = |at: usize| u32::from_le_bytes([fixed[at], fixed[at + 1], fixed[at + 2], fixed[at + 3]]);
let [name_len, extra_len, comment_len] = [28, 30, 32].map(|at| usize::from(u16_at(at)));
let (compressed_size, size, offset) = (u32_at(20), u32_at(24), u32_at(42));
if [compressed_size, size, offset].contains(&u32::MAX) {
return Err(ZipError::Zip64);
}
let record_len = 46 + name_len + extra_len + comment_len;
let name = central_directory.get(46..46 + name_len).ok_or(ZipError::AmbiguousCentralDirectory)?;
records.push(CentralRecord { name, flags: u16_at(8), compressed_size, size, offset });
central_directory = central_directory.get(record_len..).ok_or(ZipError::AmbiguousCentralDirectory)?;
}
Ok(records)
}
fn check_placement(
reader: &mut (impl Read + Seek),
records: &[CentralRecord<'_>],
entries_end: u64,
) -> Result<(), ZipError> {
if let Some(first) = records.iter().map(|record| record.offset).min()
&& first != 0
{
return Err(ZipError::DataBeforeEntries { offset: first.into() });
}
const DATA_DESCRIPTOR: u16 = 1 << 3;
for (index, record) in records.iter().enumerate() {
let start = u64::from(record.offset);
let mut local = [0; 30];
read_at(reader, start, &mut local).map_err(|_| ZipError::BadLocalHeader { index })?;
if local[..4] != *b"PK\x03\x04" {
return Err(ZipError::BadLocalHeader { index });
}
let variable = u64::from(u16::from_le_bytes([local[26], local[27]]))
+ u64::from(u16::from_le_bytes([local[28], local[29]]));
let mut end = start + 30 + variable + u64::from(record.compressed_size);
if record.flags & DATA_DESCRIPTOR != 0 {
let mut signature = [0; 4];
let signed =
end + 4 <= entries_end && read_at(reader, end, &mut signature).is_ok() && signature == *b"PK\x07\x08";
end += if signed { 16 } else { 12 };
}
if end > entries_end {
return Err(ZipError::EntryOutOfBounds { index });
}
}
Ok(())
}
fn read_at(reader: &mut (impl Read + Seek), offset: u64, buf: &mut [u8]) -> Result<(), ZipError> {
reader.seek(SeekFrom::Start(offset))?;
reader.read_exact(buf)?;
Ok(())
}
pub(crate) fn entry_name<R: Read + Seek>(archive: &ZipArchive<R>, index: usize) -> Result<String, ZipError> {
let file = archive.by_index_data(index)?;
String::from_utf8(file.name_raw().to_vec()).map_err(|_| ZipError::NonUtf8Name { index })
}
pub(crate) fn read_entry<R: Read + Seek>(
archive: &mut ZipArchive<R>,
name: &str,
limit: u64,
) -> Result<Vec<u8>, ZipError> {
let entry = match archive.by_name(name) {
Ok(entry) => entry,
Err(zip::result::ZipError::FileNotFound) => return Err(ZipError::MissingEntry { name: name.to_owned() }),
Err(error) => return Err(error.into()),
};
read_capped(entry, name, limit)
}
pub(crate) fn read_capped(reader: impl Read, name: &str, limit: u64) -> Result<Vec<u8>, ZipError> {
const INITIAL_CAPACITY: u64 = 1 << 20;
let mut bytes = Vec::with_capacity(limit.min(INITIAL_CAPACITY) as usize);
reader.take(limit.saturating_add(1)).read_to_end(&mut bytes)?;
if bytes.len() as u64 > limit {
return Err(ZipError::EntryTooLarge { name: name.to_owned(), limit });
}
Ok(bytes)
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use zip::write::SimpleFileOptions;
use super::*;
use crate::types::ArchivePath;
fn archive() -> Vec<u8> {
let mut writer = ApkWriter::new(Cursor::new(Vec::new()));
writer.add(&ArchivePath::known("a.txt"), b"alpha", EntryOptions::STORED).unwrap();
writer.add(&ArchivePath::known("b.txt"), b"beta", EntryOptions::DEFLATED).unwrap();
writer.finish().unwrap().into_inner()
}
fn record_offsets(bytes: &[u8]) -> Vec<usize> {
let layout = ApkLayout::read(&mut Cursor::new(bytes)).unwrap();
let mut offsets = Vec::new();
let mut at = layout.central_directory.start as usize;
while bytes[at..at + 4] == *b"PK\x01\x02" {
offsets.push(at);
let len = |field: usize| usize::from(u16::from_le_bytes([bytes[at + field], bytes[at + field + 1]]));
at += 46 + len(28) + len(30) + len(32);
}
offsets
}
fn add_u32(bytes: &mut [u8], at: usize, delta: u32) {
let value = u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()) + delta;
bytes[at..at + 4].copy_from_slice(&value.to_le_bytes());
}
#[test]
fn opens_well_formed_archives() {
let (archive, layout) = open(Cursor::new(archive())).unwrap();
assert_eq!((archive.len(), layout.entry_count()), (2, 2));
}
#[test]
fn data_before_the_first_entry_is_rejected() {
let original = archive();
let prefix = b"dex\n035\0";
let mut bytes = [prefix.as_slice(), &original].concat();
let shift = prefix.len() as u32;
for record in record_offsets(&original) {
add_u32(&mut bytes, record + prefix.len() + 42, shift);
}
let eocd = bytes.len() - 22;
add_u32(&mut bytes, eocd + 16, shift);
assert!(matches!(open(Cursor::new(&bytes)), Err(ZipError::DataBeforeEntries { offset: 8 })));
}
#[test]
fn entry_data_must_end_before_the_central_directory() {
let mut bytes = archive();
let last = *record_offsets(&bytes).last().unwrap();
add_u32(&mut bytes, last + 20, 1000);
assert!(matches!(open(Cursor::new(&bytes)), Err(ZipError::EntryOutOfBounds { index: 1 })));
}
#[test]
fn unicode_path_fields_cannot_rename_entries() {
let crc32 = |bytes: &[u8]| {
let mut crc = !0u32;
for &byte in bytes {
crc ^= u32::from(byte);
for _ in 0..8 {
crc = if crc & 1 != 0 { (crc >> 1) ^ 0xedb8_8320 } else { crc >> 1 };
}
}
!crc
};
let field = [&[1][..], &crc32(b"a.txt").to_le_bytes(), b"evil.txt"].concat();
let mut options = SimpleFileOptions::default().into_full_options();
options.add_extra_field(0x7075, &field, false).unwrap();
let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
zip.start_file("a.txt", options).unwrap();
io::Write::write_all(&mut zip, b"alpha").unwrap();
let bytes = zip.finish().unwrap().into_inner();
assert!(matches!(open(Cursor::new(&bytes)), Err(ZipError::AmbiguousCentralDirectory)));
}
}