1mod layout;
9mod source;
10mod writer;
11
12use std::io::{self, Read, Seek, SeekFrom};
13
14use zip::ZipArchive;
15use zip::read::{ArchiveOffset, Config};
16
17pub use layout::ApkLayout;
18pub(crate) use layout::SIGNING_BLOCK_MAGIC;
19pub(crate) use source::ReadAt;
20pub(crate) use writer::ApkWriter;
21pub use writer::{Compression, EntryOptions};
22
23use crate::types::ArchivePathError;
24
25pub use crate::Content;
26pub use crate::digest::DigestAlgorithm;
27
28pub(crate) fn is_signed_entry(name: &str) -> bool {
30 !name.ends_with('/') && !is_signature_file(name)
31}
32
33pub(crate) fn is_signature_file(name: &str) -> bool {
35 let Some(file) = name.strip_prefix("META-INF/").filter(|f| !f.contains('/')) else { return false };
36 let file = file.to_ascii_lowercase();
37 file == "manifest.mf"
38 || file.starts_with("sig-")
39 || [".sf", ".rsa", ".dsa", ".ec"].iter().any(|ext| file.ends_with(ext))
40}
41
42#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
44pub enum ZipLimit {
45 Size,
47 EntryCount,
49}
50
51#[derive(Debug, thiserror::Error)]
53#[non_exhaustive]
54pub enum ZipError {
55 #[error(transparent)]
56 Io(#[from] io::Error),
57 #[error(transparent)]
58 Zip(#[from] zip::result::ZipError),
59 #[error("no end of central directory record found")]
60 MissingEndOfCentralDirectory,
61 #[error("ZIP64 archives are not supported by APK signature schemes")]
62 Zip64,
63 #[error("multi-disk archives are not supported")]
64 MultiDisk,
65 #[error("central directory at {offset:#x} with size {size} does not end at the EOCD record at {eocd:#x}")]
66 InconsistentCentralDirectory { offset: u64, size: u64, eocd: u64 },
67 #[error("the central directory reads differently to different ZIP parsers")]
68 AmbiguousCentralDirectory,
69 #[error("the first entry starts at offset {offset:#x}, not at the start of the file")]
70 DataBeforeEntries { offset: u64 },
71 #[error("entry {index}'s local header is malformed")]
72 BadLocalHeader { index: usize },
73 #[error("entry {index}'s data runs past the end of the entries, into the signing block or central directory")]
74 EntryOutOfBounds { index: usize },
75 #[error("APK signing block size fields disagree ({leading} vs {trailing})")]
76 SigningBlockSizeMismatch { leading: u64, trailing: u64 },
77 #[error("APK signing block of {size} bytes does not fit before the central directory")]
78 SigningBlockOutOfBounds { size: u64 },
79 #[error("entry {index} has a name that is not valid UTF-8")]
80 NonUtf8Name { index: usize },
81 #[error("entry name {name:?} is unsafe: {source}")]
82 UnsafeName { name: String, source: ArchivePathError },
83 #[error("the archive has no entry {name:?}")]
84 MissingEntry { name: String },
85 #[error("entry {name:?} decompresses to more than {limit} bytes")]
86 EntryTooLarge { name: String, limit: u64 },
87 #[error("entry {name:?} appears more than once")]
88 DuplicateEntry { name: String },
89 #[error("the central directory lists {listed} entries but only {distinct} distinct names")]
90 DuplicateNames { listed: u16, distinct: usize },
91 #[error("entry {name:?} is encrypted")]
92 EncryptedEntry { name: String },
93 #[error("archive exceeds the classic ZIP {limit:?} limit")]
94 LimitExceeded { limit: ZipLimit },
95 #[error("input {name:?} changed size while it was archived (expected {expected} bytes, read {actual})")]
96 SourceChanged { name: String, expected: u64, actual: u64 },
97}
98
99pub(crate) fn open<R: Read + Seek>(mut reader: R) -> Result<(ZipArchive<R>, ApkLayout), ZipError> {
107 let layout = ApkLayout::read(&mut reader)?;
108 let cd = &layout.central_directory;
109 let mut central_directory = vec![0; (cd.end - cd.start) as usize];
110 read_at(&mut reader, cd.start, &mut central_directory)?;
111 let records = central_records(¢ral_directory)?;
112 if records.len() != usize::from(layout.entry_count()) {
113 return Err(ZipError::AmbiguousCentralDirectory);
114 }
115 check_placement(&mut reader, &records, layout.entries_end)?;
116
117 let config = Config { archive_offset: ArchiveOffset::Known(0) };
118 let mut archive = ZipArchive::with_config(config, reader)?;
119 if archive.central_directory_start() != layout.central_directory.start {
120 return Err(ZipError::AmbiguousCentralDirectory);
121 }
122 if archive.len() != records.len() {
124 return Err(ZipError::DuplicateNames { listed: layout.entry_count(), distinct: archive.len() });
125 }
126 archive.validate_header_consistency()?;
127 for (index, record) in records.iter().enumerate() {
128 let entry = archive.by_index_data(index)?;
129 let agrees = entry.name_raw() == record.name
130 && entry.header_start() == u64::from(record.offset)
131 && entry.compressed_size() == u64::from(record.compressed_size)
132 && entry.size() == u64::from(record.size);
133 if !agrees {
134 return Err(ZipError::AmbiguousCentralDirectory);
135 }
136 }
137 Ok((archive, layout))
138}
139
140struct CentralRecord<'a> {
142 name: &'a [u8],
143 flags: u16,
144 compressed_size: u32,
145 size: u32,
146 offset: u32,
147}
148
149fn central_records(mut central_directory: &[u8]) -> Result<Vec<CentralRecord<'_>>, ZipError> {
151 let mut records = Vec::new();
152 while !central_directory.is_empty() {
153 let fixed = central_directory.get(..46).filter(|fixed| fixed[..4] == *b"PK\x01\x02");
154 let fixed = fixed.ok_or(ZipError::AmbiguousCentralDirectory)?;
155 let u16_at = |at: usize| u16::from_le_bytes([fixed[at], fixed[at + 1]]);
156 let u32_at = |at: usize| u32::from_le_bytes([fixed[at], fixed[at + 1], fixed[at + 2], fixed[at + 3]]);
157 let [name_len, extra_len, comment_len] = [28, 30, 32].map(|at| usize::from(u16_at(at)));
158 let (compressed_size, size, offset) = (u32_at(20), u32_at(24), u32_at(42));
159 if [compressed_size, size, offset].contains(&u32::MAX) {
160 return Err(ZipError::Zip64);
161 }
162 let record_len = 46 + name_len + extra_len + comment_len;
163 let name = central_directory.get(46..46 + name_len).ok_or(ZipError::AmbiguousCentralDirectory)?;
164 records.push(CentralRecord { name, flags: u16_at(8), compressed_size, size, offset });
165 central_directory = central_directory.get(record_len..).ok_or(ZipError::AmbiguousCentralDirectory)?;
166 }
167 Ok(records)
168}
169
170fn check_placement(
173 reader: &mut (impl Read + Seek),
174 records: &[CentralRecord<'_>],
175 entries_end: u64,
176) -> Result<(), ZipError> {
177 if let Some(first) = records.iter().map(|record| record.offset).min()
178 && first != 0
179 {
180 return Err(ZipError::DataBeforeEntries { offset: first.into() });
181 }
182 const DATA_DESCRIPTOR: u16 = 1 << 3;
183 for (index, record) in records.iter().enumerate() {
184 let start = u64::from(record.offset);
185 let mut local = [0; 30];
186 read_at(reader, start, &mut local).map_err(|_| ZipError::BadLocalHeader { index })?;
187 if local[..4] != *b"PK\x03\x04" {
188 return Err(ZipError::BadLocalHeader { index });
189 }
190 let variable = u64::from(u16::from_le_bytes([local[26], local[27]]))
191 + u64::from(u16::from_le_bytes([local[28], local[29]]));
192 let mut end = start + 30 + variable + u64::from(record.compressed_size);
193 if record.flags & DATA_DESCRIPTOR != 0 {
194 let mut signature = [0; 4];
196 let signed =
197 end + 4 <= entries_end && read_at(reader, end, &mut signature).is_ok() && signature == *b"PK\x07\x08";
198 end += if signed { 16 } else { 12 };
199 }
200 if end > entries_end {
201 return Err(ZipError::EntryOutOfBounds { index });
202 }
203 }
204 Ok(())
205}
206
207fn read_at(reader: &mut (impl Read + Seek), offset: u64, buf: &mut [u8]) -> Result<(), ZipError> {
208 reader.seek(SeekFrom::Start(offset))?;
209 reader.read_exact(buf)?;
210 Ok(())
211}
212
213pub(crate) fn entry_name<R: Read + Seek>(archive: &ZipArchive<R>, index: usize) -> Result<String, ZipError> {
215 let file = archive.by_index_data(index)?;
216 String::from_utf8(file.name_raw().to_vec()).map_err(|_| ZipError::NonUtf8Name { index })
217}
218
219pub(crate) fn read_entry<R: Read + Seek>(
222 archive: &mut ZipArchive<R>,
223 name: &str,
224 limit: u64,
225) -> Result<Vec<u8>, ZipError> {
226 let entry = match archive.by_name(name) {
227 Ok(entry) => entry,
228 Err(zip::result::ZipError::FileNotFound) => return Err(ZipError::MissingEntry { name: name.to_owned() }),
229 Err(error) => return Err(error.into()),
230 };
231 read_capped(entry, name, limit)
232}
233
234pub(crate) fn read_capped(reader: impl Read, name: &str, limit: u64) -> Result<Vec<u8>, ZipError> {
236 const INITIAL_CAPACITY: u64 = 1 << 20;
237 let mut bytes = Vec::with_capacity(limit.min(INITIAL_CAPACITY) as usize);
238 reader.take(limit.saturating_add(1)).read_to_end(&mut bytes)?;
239 if bytes.len() as u64 > limit {
240 return Err(ZipError::EntryTooLarge { name: name.to_owned(), limit });
241 }
242 Ok(bytes)
243}
244
245#[cfg(test)]
246mod tests {
247 use std::io::Cursor;
248
249 use zip::write::SimpleFileOptions;
250
251 use super::*;
252 use crate::types::ArchivePath;
253
254 fn archive() -> Vec<u8> {
255 let mut writer = ApkWriter::new(Cursor::new(Vec::new()));
256 writer.add(&ArchivePath::known("a.txt"), b"alpha", EntryOptions::STORED).unwrap();
257 writer.add(&ArchivePath::known("b.txt"), b"beta", EntryOptions::DEFLATED).unwrap();
258 writer.finish().unwrap().into_inner()
259 }
260
261 fn record_offsets(bytes: &[u8]) -> Vec<usize> {
263 let layout = ApkLayout::read(&mut Cursor::new(bytes)).unwrap();
264 let mut offsets = Vec::new();
265 let mut at = layout.central_directory.start as usize;
266 while bytes[at..at + 4] == *b"PK\x01\x02" {
267 offsets.push(at);
268 let len = |field: usize| usize::from(u16::from_le_bytes([bytes[at + field], bytes[at + field + 1]]));
269 at += 46 + len(28) + len(30) + len(32);
270 }
271 offsets
272 }
273
274 fn add_u32(bytes: &mut [u8], at: usize, delta: u32) {
275 let value = u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()) + delta;
276 bytes[at..at + 4].copy_from_slice(&value.to_le_bytes());
277 }
278
279 #[test]
280 fn opens_well_formed_archives() {
281 let (archive, layout) = open(Cursor::new(archive())).unwrap();
282 assert_eq!((archive.len(), layout.entry_count()), (2, 2));
283 }
284
285 #[test]
286 fn data_before_the_first_entry_is_rejected() {
287 let original = archive();
289 let prefix = b"dex\n035\0";
290 let mut bytes = [prefix.as_slice(), &original].concat();
291 let shift = prefix.len() as u32;
292 for record in record_offsets(&original) {
293 add_u32(&mut bytes, record + prefix.len() + 42, shift);
294 }
295 let eocd = bytes.len() - 22;
296 add_u32(&mut bytes, eocd + 16, shift);
297 assert!(matches!(open(Cursor::new(&bytes)), Err(ZipError::DataBeforeEntries { offset: 8 })));
298 }
299
300 #[test]
301 fn entry_data_must_end_before_the_central_directory() {
302 let mut bytes = archive();
303 let last = *record_offsets(&bytes).last().unwrap();
304 add_u32(&mut bytes, last + 20, 1000);
305 assert!(matches!(open(Cursor::new(&bytes)), Err(ZipError::EntryOutOfBounds { index: 1 })));
306 }
307
308 #[test]
309 fn unicode_path_fields_cannot_rename_entries() {
310 let crc32 = |bytes: &[u8]| {
311 let mut crc = !0u32;
312 for &byte in bytes {
313 crc ^= u32::from(byte);
314 for _ in 0..8 {
315 crc = if crc & 1 != 0 { (crc >> 1) ^ 0xedb8_8320 } else { crc >> 1 };
316 }
317 }
318 !crc
319 };
320 let field = [&[1][..], &crc32(b"a.txt").to_le_bytes(), b"evil.txt"].concat();
322 let mut options = SimpleFileOptions::default().into_full_options();
323 options.add_extra_field(0x7075, &field, false).unwrap();
324 let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
325 zip.start_file("a.txt", options).unwrap();
326 io::Write::write_all(&mut zip, b"alpha").unwrap();
327 let bytes = zip.finish().unwrap().into_inner();
328 assert!(matches!(open(Cursor::new(&bytes)), Err(ZipError::AmbiguousCentralDirectory)));
329 }
330}