1use std::io::{Cursor, Read, Seek, SeekFrom};
5use std::ops::Range;
6
7use binrw::{BinRead, BinWrite, binrw};
8
9use super::{ZipError, ZipLimit};
10
11pub(crate) const SIGNING_BLOCK_MAGIC: [u8; 16] = *b"APK Sig Block 42";
13const SIGNING_BLOCK_FOOTER: u64 = 24;
15const ZIP64_LOCATOR: (u64, [u8; 4]) = (20, *b"PK\x06\x07");
18
19#[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#[derive(Clone, Debug, Eq, PartialEq)]
49pub struct ApkLayout {
50 pub entries_end: u64,
53 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 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 pub fn entry_count(&self) -> u16 {
90 self.eocd.total_entries
91 }
92
93 pub fn comment(&self) -> &[u8] {
95 &self.eocd.comment
96 }
97
98 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
113fn 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
151fn 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 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; 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}