use std::io::{Cursor, Read, Seek, SeekFrom};
use std::ops::Range;
use binrw::{BinRead, BinWrite, binrw};
use super::{ZipError, ZipLimit};
pub(crate) const SIGNING_BLOCK_MAGIC: [u8; 16] = *b"APK Sig Block 42";
const SIGNING_BLOCK_FOOTER: u64 = 24;
const ZIP64_LOCATOR: (u64, [u8; 4]) = (20, *b"PK\x06\x07");
#[binrw]
#[brw(little, magic = b"PK\x05\x06")]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct EndOfCentralDirectory {
disk: u16,
central_directory_disk: u16,
disk_entries: u16,
total_entries: u16,
central_directory_size: u32,
central_directory_offset: u32,
#[br(temp)]
#[bw(calc = comment.len() as u16)]
comment_len: u16,
#[br(count = comment_len)]
comment: Vec<u8>,
}
impl EndOfCentralDirectory {
const FIXED_SIZE: usize = 22;
const SIGNATURE: [u8; 4] = *b"PK\x05\x06";
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ApkLayout {
pub entries_end: u64,
pub signing_block: Option<Range<u64>>,
pub central_directory: Range<u64>,
pub eocd_offset: u64,
eocd: EndOfCentralDirectory,
}
impl ApkLayout {
pub fn read(source: &mut (impl Read + Seek)) -> Result<Self, ZipError> {
let (eocd_offset, eocd) = find_eocd(source)?;
let (locator_len, locator) = ZIP64_LOCATOR;
if eocd_offset >= locator_len {
let mut signature = [0; 4];
read_at(source, eocd_offset - locator_len, &mut signature)?;
if signature == locator {
return Err(ZipError::Zip64);
}
}
if eocd.total_entries == u16::MAX
|| eocd.central_directory_offset == u32::MAX
|| eocd.central_directory_size == u32::MAX
{
return Err(ZipError::Zip64);
}
if eocd.disk != 0 || eocd.central_directory_disk != 0 || eocd.disk_entries != eocd.total_entries {
return Err(ZipError::MultiDisk);
}
let cd_start = u64::from(eocd.central_directory_offset);
let signing_block = find_signing_block(source, cd_start)?;
let entries_end = signing_block.as_ref().map_or(cd_start, |block| block.start);
Ok(Self { entries_end, signing_block, central_directory: cd_start..eocd_offset, eocd_offset, eocd })
}
pub fn entry_count(&self) -> u16 {
self.eocd.total_entries
}
pub fn comment(&self) -> &[u8] {
&self.eocd.comment
}
pub(crate) fn eocd_with_central_directory_at(&self, offset: u64) -> Result<Vec<u8>, ZipError> {
let offset = u32::try_from(offset)
.ok()
.filter(|&offset| offset != u32::MAX)
.ok_or(ZipError::LimitExceeded { limit: ZipLimit::Size })?;
let eocd = EndOfCentralDirectory { central_directory_offset: offset, ..self.eocd.clone() };
let mut bytes = Cursor::new(Vec::with_capacity(EndOfCentralDirectory::FIXED_SIZE + eocd.comment.len()));
eocd.write(&mut bytes).expect("an EOCD record read from an archive encodes into a Vec");
Ok(bytes.into_inner())
}
}
fn find_eocd(source: &mut (impl Read + Seek)) -> Result<(u64, EndOfCentralDirectory), ZipError> {
let len = source.seek(SeekFrom::End(0))?;
let fixed = EndOfCentralDirectory::FIXED_SIZE as u64;
if len < fixed {
return Err(ZipError::MissingEndOfCentralDirectory);
}
let tail_len = len.min(fixed + u64::from(u16::MAX));
let tail_start = len - tail_len;
let mut tail = vec![0; tail_len as usize];
read_at(source, tail_start, &mut tail)?;
let mut inconsistent = None;
for pos in (0..=tail.len() - EndOfCentralDirectory::FIXED_SIZE).rev() {
if tail[pos..pos + 4] != EndOfCentralDirectory::SIGNATURE {
continue;
}
let comment_len = usize::from(u16::from_le_bytes([tail[pos + 20], tail[pos + 21]]));
if pos + EndOfCentralDirectory::FIXED_SIZE + comment_len != tail.len() {
continue;
}
let eocd = EndOfCentralDirectory::read(&mut Cursor::new(&tail[pos..]))
.expect("a signature-prefixed record that spans the tail parses");
let offset = tail_start + pos as u64;
let (cd_offset, cd_size) = (u64::from(eocd.central_directory_offset), u64::from(eocd.central_directory_size));
if cd_offset + cd_size == offset {
return Ok((offset, eocd));
}
inconsistent.get_or_insert(ZipError::InconsistentCentralDirectory {
offset: cd_offset,
size: cd_size,
eocd: offset,
});
}
Err(inconsistent.unwrap_or(ZipError::MissingEndOfCentralDirectory))
}
fn find_signing_block(source: &mut (impl Read + Seek), cd_start: u64) -> Result<Option<Range<u64>>, ZipError> {
if cd_start < SIGNING_BLOCK_FOOTER + 8 {
return Ok(None);
}
let mut footer = [0u8; SIGNING_BLOCK_FOOTER as usize];
read_at(source, cd_start - SIGNING_BLOCK_FOOTER, &mut footer)?;
if footer[8..] != SIGNING_BLOCK_MAGIC {
return Ok(None);
}
let size = u64::from_le_bytes(footer[..8].try_into().expect("eight bytes"));
let start = cd_start
.checked_sub(size)
.and_then(|start| start.checked_sub(8))
.filter(|_| size >= SIGNING_BLOCK_FOOTER)
.ok_or(ZipError::SigningBlockOutOfBounds { size })?;
let mut leading = [0u8; 8];
read_at(source, start, &mut leading)?;
let leading = u64::from_le_bytes(leading);
if leading != size {
return Err(ZipError::SigningBlockSizeMismatch { leading, trailing: size });
}
Ok(Some(start..cd_start))
}
fn read_at(source: &mut (impl Read + Seek), offset: u64, buf: &mut [u8]) -> Result<(), ZipError> {
source.seek(SeekFrom::Start(offset))?;
source.read_exact(buf)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn read_layout(bytes: &[u8]) -> Result<ApkLayout, ZipError> {
ApkLayout::read(&mut Cursor::new(bytes))
}
fn archive(entries: &[u8], comment: &[u8]) -> Vec<u8> {
let eocd = EndOfCentralDirectory {
central_directory_size: entries.len() as u32,
comment: comment.to_vec(),
..Default::default()
};
let mut bytes = Cursor::new(entries.to_vec());
bytes.set_position(entries.len() as u64);
eocd.write(&mut bytes).unwrap();
bytes.into_inner()
}
#[test]
fn finds_the_eocd_behind_a_comment_that_imitates_one() {
let mut fake = EndOfCentralDirectory::SIGNATURE.to_vec();
fake.resize(EndOfCentralDirectory::FIXED_SIZE, 0);
let bytes = archive(b"central", &fake);
let layout = read_layout(&bytes).unwrap();
assert_eq!((layout.central_directory.clone(), layout.eocd_offset), (0..7, 7));
assert_eq!(layout.comment(), fake);
}
#[test]
fn rejects_unsupported_and_inconsistent_archives() {
let eocd = 7;
let mut bytes = archive(b"central", b"");
bytes[eocd + 4] = 1;
assert!(matches!(read_layout(&bytes), Err(ZipError::MultiDisk)));
let mut bytes = archive(b"central", b"");
bytes[eocd + 16] = 9; assert!(matches!(read_layout(&bytes), Err(ZipError::InconsistentCentralDirectory { .. })));
let mut bytes = archive(b"central", b"");
bytes[eocd + 10..eocd + 12].copy_from_slice(&u16::MAX.to_le_bytes());
assert!(matches!(read_layout(&bytes), Err(ZipError::Zip64)));
assert!(matches!(read_layout(&[0u8; 21]), Err(ZipError::MissingEndOfCentralDirectory)));
}
#[test]
fn validates_signing_block_size_fields() {
let block = |leading: u64, trailing: u64| {
let mut bytes = leading.to_le_bytes().to_vec();
bytes.extend([0; 8]);
bytes.extend(trailing.to_le_bytes());
bytes.extend(SIGNING_BLOCK_MAGIC);
bytes
};
let with_block = |block: Vec<u8>| {
let start = block.len();
let mut bytes = archive(&[block, b"central".to_vec()].concat(), b"");
let eocd = bytes.len() - EndOfCentralDirectory::FIXED_SIZE;
bytes[eocd + 12..eocd + 16].copy_from_slice(&7u32.to_le_bytes());
bytes[eocd + 16..eocd + 20].copy_from_slice(&(start as u32).to_le_bytes());
bytes
};
let size = 8 + 24;
let layout = read_layout(&with_block(block(size, size))).unwrap();
assert_eq!((layout.signing_block, layout.entries_end), (Some(0..40), 0));
assert_eq!(layout.central_directory, 40..47);
assert!(matches!(
read_layout(&with_block(block(size + 1, size))),
Err(ZipError::SigningBlockSizeMismatch { leading: 33, trailing: 32 })
));
assert!(matches!(
read_layout(&with_block(block(size, 1 << 40))),
Err(ZipError::SigningBlockOutOfBounds { .. })
));
}
#[test]
fn patches_the_central_directory_offset() {
let layout = read_layout(&archive(b"central", b"note")).unwrap();
let patched = layout.eocd_with_central_directory_at(0x1234).unwrap();
assert_eq!(patched[16..20], 0x1234u32.to_le_bytes());
assert_eq!(&patched[22..], b"note");
assert!(layout.eocd_with_central_directory_at(u64::from(u32::MAX)).is_err());
}
}