Skip to main content

apk/archive/
layout.rs

1//! Locating an APK's sections: ZIP entries, APK signing block, central
2//! directory, and end of central directory record.
3
4use std::io::{Cursor, Read, Seek, SeekFrom};
5use std::ops::Range;
6
7use binrw::{BinRead, BinWrite, binrw};
8
9use super::{ZipError, ZipLimit};
10
11/// The magic that ends an APK signing block.
12pub(crate) const SIGNING_BLOCK_MAGIC: [u8; 16] = *b"APK Sig Block 42";
13/// The block's trailing size field and magic.
14const SIGNING_BLOCK_FOOTER: u64 = 24;
15/// The ZIP64 end of central directory locator, which precedes the EOCD
16/// record of a ZIP64 archive.
17const ZIP64_LOCATOR: (u64, [u8; 4]) = (20, *b"PK\x06\x07");
18
19/// The end of central directory record (APPNOTE 4.3.16).
20#[binrw]
21#[brw(little, magic = b"PK\x05\x06")]
22#[derive(Clone, Debug, Default, Eq, PartialEq)]
23struct EndOfCentralDirectory {
24    disk: u16,
25    central_directory_disk: u16,
26    disk_entries: u16,
27    total_entries: u16,
28    central_directory_size: u32,
29    central_directory_offset: u32,
30    #[br(temp)]
31    #[bw(calc = comment.len() as u16)]
32    comment_len: u16,
33    #[br(count = comment_len)]
34    comment: Vec<u8>,
35}
36
37impl EndOfCentralDirectory {
38    const FIXED_SIZE: usize = 22;
39    const SIGNATURE: [u8; 4] = *b"PK\x05\x06";
40}
41
42/// The byte ranges of an APK's sections.
43///
44/// ```text
45/// [ ZIP entries | APK signing block? | central directory | EOCD ]
46///   0..entries_end                     central_directory   eocd_offset..
47/// ```
48#[derive(Clone, Debug, Eq, PartialEq)]
49pub struct ApkLayout {
50    /// Where ZIP entry data ends: the signing block's start, or the central
51    /// directory's when unsigned.
52    pub entries_end: u64,
53    /// The APK signing block, including its size fields and magic.
54    pub signing_block: Option<Range<u64>>,
55    pub central_directory: Range<u64>,
56    pub eocd_offset: u64,
57    eocd: EndOfCentralDirectory,
58}
59
60impl ApkLayout {
61    /// Reads the layout of the archive in `source`. ZIP64 and multi-disk
62    /// archives, which APK signature schemes do not support, are rejected.
63    pub fn read(source: &mut (impl Read + Seek)) -> Result<Self, ZipError> {
64        let (eocd_offset, eocd) = find_eocd(source)?;
65        let (locator_len, locator) = ZIP64_LOCATOR;
66        if eocd_offset >= locator_len {
67            let mut signature = [0; 4];
68            read_at(source, eocd_offset - locator_len, &mut signature)?;
69            if signature == locator {
70                return Err(ZipError::Zip64);
71            }
72        }
73        if eocd.total_entries == u16::MAX
74            || eocd.central_directory_offset == u32::MAX
75            || eocd.central_directory_size == u32::MAX
76        {
77            return Err(ZipError::Zip64);
78        }
79        if eocd.disk != 0 || eocd.central_directory_disk != 0 || eocd.disk_entries != eocd.total_entries {
80            return Err(ZipError::MultiDisk);
81        }
82        let cd_start = u64::from(eocd.central_directory_offset);
83        let signing_block = find_signing_block(source, cd_start)?;
84        let entries_end = signing_block.as_ref().map_or(cd_start, |block| block.start);
85        Ok(Self { entries_end, signing_block, central_directory: cd_start..eocd_offset, eocd_offset, eocd })
86    }
87
88    /// The number of entries the EOCD record declares.
89    pub fn entry_count(&self) -> u16 {
90        self.eocd.total_entries
91    }
92
93    /// The archive comment.
94    pub fn comment(&self) -> &[u8] {
95        &self.eocd.comment
96    }
97
98    /// The EOCD record as it would read with the central directory at
99    /// `offset`: how signature schemes digest it, and how a signing block is
100    /// inserted.
101    pub(crate) fn eocd_with_central_directory_at(&self, offset: u64) -> Result<Vec<u8>, ZipError> {
102        let offset = u32::try_from(offset)
103            .ok()
104            .filter(|&offset| offset != u32::MAX)
105            .ok_or(ZipError::LimitExceeded { limit: ZipLimit::Size })?;
106        let eocd = EndOfCentralDirectory { central_directory_offset: offset, ..self.eocd.clone() };
107        let mut bytes = Cursor::new(Vec::with_capacity(EndOfCentralDirectory::FIXED_SIZE + eocd.comment.len()));
108        eocd.write(&mut bytes).expect("an EOCD record read from an archive encodes into a Vec");
109        Ok(bytes.into_inner())
110    }
111}
112
113/// Finds the EOCD record: the last one whose comment ends exactly at the end
114/// of the file and whose central directory ends where the record starts.
115/// Comments that imitate a record are skipped.
116fn find_eocd(source: &mut (impl Read + Seek)) -> Result<(u64, EndOfCentralDirectory), ZipError> {
117    let len = source.seek(SeekFrom::End(0))?;
118    let fixed = EndOfCentralDirectory::FIXED_SIZE as u64;
119    if len < fixed {
120        return Err(ZipError::MissingEndOfCentralDirectory);
121    }
122    let tail_len = len.min(fixed + u64::from(u16::MAX));
123    let tail_start = len - tail_len;
124    let mut tail = vec![0; tail_len as usize];
125    read_at(source, tail_start, &mut tail)?;
126    let mut inconsistent = None;
127    for pos in (0..=tail.len() - EndOfCentralDirectory::FIXED_SIZE).rev() {
128        if tail[pos..pos + 4] != EndOfCentralDirectory::SIGNATURE {
129            continue;
130        }
131        let comment_len = usize::from(u16::from_le_bytes([tail[pos + 20], tail[pos + 21]]));
132        if pos + EndOfCentralDirectory::FIXED_SIZE + comment_len != tail.len() {
133            continue;
134        }
135        let eocd = EndOfCentralDirectory::read(&mut Cursor::new(&tail[pos..]))
136            .expect("a signature-prefixed record that spans the tail parses");
137        let offset = tail_start + pos as u64;
138        let (cd_offset, cd_size) = (u64::from(eocd.central_directory_offset), u64::from(eocd.central_directory_size));
139        if cd_offset + cd_size == offset {
140            return Ok((offset, eocd));
141        }
142        inconsistent.get_or_insert(ZipError::InconsistentCentralDirectory {
143            offset: cd_offset,
144            size: cd_size,
145            eocd: offset,
146        });
147    }
148    Err(inconsistent.unwrap_or(ZipError::MissingEndOfCentralDirectory))
149}
150
151/// Finds the APK signing block that immediately precedes the central directory.
152fn find_signing_block(source: &mut (impl Read + Seek), cd_start: u64) -> Result<Option<Range<u64>>, ZipError> {
153    if cd_start < SIGNING_BLOCK_FOOTER + 8 {
154        return Ok(None);
155    }
156    let mut footer = [0u8; SIGNING_BLOCK_FOOTER as usize];
157    read_at(source, cd_start - SIGNING_BLOCK_FOOTER, &mut footer)?;
158    if footer[8..] != SIGNING_BLOCK_MAGIC {
159        return Ok(None);
160    }
161    let size = u64::from_le_bytes(footer[..8].try_into().expect("eight bytes"));
162    let start = cd_start
163        .checked_sub(size)
164        .and_then(|start| start.checked_sub(8))
165        .filter(|_| size >= SIGNING_BLOCK_FOOTER)
166        .ok_or(ZipError::SigningBlockOutOfBounds { size })?;
167    let mut leading = [0u8; 8];
168    read_at(source, start, &mut leading)?;
169    let leading = u64::from_le_bytes(leading);
170    if leading != size {
171        return Err(ZipError::SigningBlockSizeMismatch { leading, trailing: size });
172    }
173    Ok(Some(start..cd_start))
174}
175
176fn read_at(source: &mut (impl Read + Seek), offset: u64, buf: &mut [u8]) -> Result<(), ZipError> {
177    source.seek(SeekFrom::Start(offset))?;
178    source.read_exact(buf)?;
179    Ok(())
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    fn read_layout(bytes: &[u8]) -> Result<ApkLayout, ZipError> {
187        ApkLayout::read(&mut Cursor::new(bytes))
188    }
189
190    /// An archive of `entries` (bytes standing in for entry data and a
191    /// central directory) whose EOCD has `comment`.
192    fn archive(entries: &[u8], comment: &[u8]) -> Vec<u8> {
193        let eocd = EndOfCentralDirectory {
194            central_directory_size: entries.len() as u32,
195            comment: comment.to_vec(),
196            ..Default::default()
197        };
198        let mut bytes = Cursor::new(entries.to_vec());
199        bytes.set_position(entries.len() as u64);
200        eocd.write(&mut bytes).unwrap();
201        bytes.into_inner()
202    }
203
204    #[test]
205    fn finds_the_eocd_behind_a_comment_that_imitates_one() {
206        let mut fake = EndOfCentralDirectory::SIGNATURE.to_vec();
207        fake.resize(EndOfCentralDirectory::FIXED_SIZE, 0);
208        let bytes = archive(b"central", &fake);
209        let layout = read_layout(&bytes).unwrap();
210        assert_eq!((layout.central_directory.clone(), layout.eocd_offset), (0..7, 7));
211        assert_eq!(layout.comment(), fake);
212    }
213
214    #[test]
215    fn rejects_unsupported_and_inconsistent_archives() {
216        let eocd = 7;
217        let mut bytes = archive(b"central", b"");
218        bytes[eocd + 4] = 1;
219        assert!(matches!(read_layout(&bytes), Err(ZipError::MultiDisk)));
220
221        let mut bytes = archive(b"central", b"");
222        bytes[eocd + 16] = 9; // central directory offset
223        assert!(matches!(read_layout(&bytes), Err(ZipError::InconsistentCentralDirectory { .. })));
224
225        let mut bytes = archive(b"central", b"");
226        bytes[eocd + 10..eocd + 12].copy_from_slice(&u16::MAX.to_le_bytes());
227        assert!(matches!(read_layout(&bytes), Err(ZipError::Zip64)));
228
229        assert!(matches!(read_layout(&[0u8; 21]), Err(ZipError::MissingEndOfCentralDirectory)));
230    }
231
232    #[test]
233    fn validates_signing_block_size_fields() {
234        let block = |leading: u64, trailing: u64| {
235            let mut bytes = leading.to_le_bytes().to_vec();
236            bytes.extend([0; 8]);
237            bytes.extend(trailing.to_le_bytes());
238            bytes.extend(SIGNING_BLOCK_MAGIC);
239            bytes
240        };
241        let with_block = |block: Vec<u8>| {
242            let start = block.len();
243            let mut bytes = archive(&[block, b"central".to_vec()].concat(), b"");
244            let eocd = bytes.len() - EndOfCentralDirectory::FIXED_SIZE;
245            bytes[eocd + 12..eocd + 16].copy_from_slice(&7u32.to_le_bytes());
246            bytes[eocd + 16..eocd + 20].copy_from_slice(&(start as u32).to_le_bytes());
247            bytes
248        };
249        let size = 8 + 24;
250        let layout = read_layout(&with_block(block(size, size))).unwrap();
251        assert_eq!((layout.signing_block, layout.entries_end), (Some(0..40), 0));
252        assert_eq!(layout.central_directory, 40..47);
253        assert!(matches!(
254            read_layout(&with_block(block(size + 1, size))),
255            Err(ZipError::SigningBlockSizeMismatch { leading: 33, trailing: 32 })
256        ));
257        assert!(matches!(
258            read_layout(&with_block(block(size, 1 << 40))),
259            Err(ZipError::SigningBlockOutOfBounds { .. })
260        ));
261    }
262
263    #[test]
264    fn patches_the_central_directory_offset() {
265        let layout = read_layout(&archive(b"central", b"note")).unwrap();
266        let patched = layout.eocd_with_central_directory_at(0x1234).unwrap();
267        assert_eq!(patched[16..20], 0x1234u32.to_le_bytes());
268        assert_eq!(&patched[22..], b"note");
269        assert!(layout.eocd_with_central_directory_at(u64::from(u32::MAX)).is_err());
270    }
271}