1mod program;
13
14pub use program::Version;
15
16use memmap2::Mmap;
17use std::borrow::Cow;
18use std::fs::File;
19use std::io::Read;
20use std::path::{Path, PathBuf};
21use std::sync::Arc;
22use std::sync::atomic::{AtomicBool, Ordering};
23use std::time::SystemTime;
24
25const CD_SIG: &[u8; 4] = b"PK\x01\x02";
26const LH_SIG: &[u8; 4] = b"PK\x03\x04";
27const EOCD_SIG: &[u8; 4] = b"PK\x05\x06";
28const ZIP64_EOCD_SIG: &[u8; 4] = b"PK\x06\x06";
29const ZIP64_LOCATOR_SIG: &[u8; 4] = b"PK\x06\x07";
30const DEFLATE_CHUNK: usize = 1 << 19;
31const MAX_DEFLATE_RATIO: u64 = 1032;
34const INITIAL_CAPACITY: usize = 64 << 20;
39const TYPICAL_RATIO: usize = 16;
41
42pub const METHOD_STORED: u16 = 0;
43pub const METHOD_DEFLATE: u16 = 8;
44
45#[derive(Debug, thiserror::Error)]
46pub enum ApkError {
47 #[error("io error on {path}: {source}")]
48 Io { path: PathBuf, source: std::io::Error },
49 #[error("end of central directory not found")]
50 NoEocd,
51 #[error("central directory out of range")]
52 BadCentralDirectory,
53 #[error("bad local header signature for entry {0}")]
54 BadLocalHeader(String),
55 #[error("unsupported compression method {0}")]
56 UnsupportedMethod(u16),
57 #[error("inflate failed for entry {name}: {source}")]
58 Inflate { name: String, source: std::io::Error },
59 #[error("size mismatch for entry {name}: expected {expected}, got {actual}")]
60 SizeMismatch { name: String, expected: usize, actual: usize },
61 #[error("entry {0} not found")]
62 NotFound(String),
63 #[error("entry {name} declares an implausible size (compressed {comp_size}, uncompressed {uncomp_size})")]
64 ImplausibleSize { name: String, comp_size: u64, uncomp_size: u64 },
65 #[error("entry {name} exceeds the inflate limit: {reason}")]
66 LimitExceeded { name: String, reason: String },
67 #[error("cancelled")]
68 Cancelled,
69 #[error("no archive of {} has classes*.dex ({})", path.display(), notes.join("; "))]
70 NoCode { path: PathBuf, notes: Vec<String> },
71}
72
73pub type Result<T> = std::result::Result<T, ApkError>;
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct Entry {
78 pub name: String,
79 pub method: u16,
80 pub comp_size: u64,
81 pub uncomp_size: u64,
82 pub local_header_off: u64,
83 pub crc32: u32,
85 pub archive: usize,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct Limits {
94 pub max_entry_bytes: u64,
96 pub max_ratio: u64,
98 pub max_dex_total_bytes: u64,
100}
101
102impl Default for Limits {
103 fn default() -> Limits {
104 Limits { max_entry_bytes: u64::MAX, max_ratio: MAX_DEFLATE_RATIO, max_dex_total_bytes: u64::MAX }
105 }
106}
107
108impl Limits {
109 pub fn guarded() -> Limits {
111 Limits { max_entry_bytes: 1 << 30, max_ratio: 100, max_dex_total_bytes: 4 << 30 }
112 }
113}
114
115#[derive(Clone)]
119pub struct Apk {
120 path: PathBuf,
121 archives: Arc<[Archive]>,
122 limits: Limits,
123 notes: Arc<[String]>,
125 version: Version,
126}
127
128#[derive(Debug, Clone, Copy)]
130pub struct ArchiveId<'a> {
131 pub path: &'a Path,
132 pub label: &'a str,
135 pub directory: &'a [u8],
137}
138
139struct Archive {
141 path: PathBuf,
142 map: Data,
143 label: String,
147 bare_dex: bool,
149}
150
151enum Data {
154 Mapped(Mmap),
155 Window {
157 outer: Arc<[Archive]>,
158 archive: usize,
159 range: std::ops::Range<usize>,
160 },
161 Owned(Vec<u8>),
163}
164
165impl std::ops::Deref for Data {
166 type Target = [u8];
167
168 fn deref(&self) -> &[u8] {
169 match self {
170 Data::Mapped(map) => map,
171 Data::Window { outer, archive, range } => &outer[*archive].map[range.clone()],
173 Data::Owned(bytes) => bytes,
174 }
175 }
176}
177
178#[inline]
181fn bytes_at(b: &[u8], o: usize, n: usize) -> Option<&[u8]> {
182 b.get(o..o.checked_add(n)?)
183}
184#[inline]
185fn u16_at(b: &[u8], o: usize) -> Option<u16> {
186 bytes_at(b, o, 2).map(|s| u16::from_le_bytes([s[0], s[1]]))
187}
188#[inline]
189fn u32_at(b: &[u8], o: usize) -> Option<u32> {
190 bytes_at(b, o, 4).map(|s| u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
191}
192#[inline]
193fn u64_at(b: &[u8], o: usize) -> Option<u64> {
194 bytes_at(b, o, 8).map(|s| u64::from_le_bytes(s.try_into().expect("8 bytes")))
195}
196#[inline]
199fn offset(v: u64) -> Option<usize> {
200 usize::try_from(v).ok()
201}
202
203pub fn archive_of(dex_name: &str) -> Option<&str> {
207 let entry = dex_name.split('!').next().unwrap_or(dex_name);
208 entry.rsplit_once('/').map(|(archive, _)| archive)
209}
210
211fn is_root_dex(name: &[u8]) -> bool {
214 name.ends_with(b".dex") && !name.contains(&b'/')
215}
216
217const BARE_DEX_ENTRY: &str = "classes.dex";
219const DEX_HEADER_SIZE: usize = 0x70;
221
222impl Archive {
223 fn open(path: PathBuf, label: String) -> Result<Archive> {
224 let file = File::open(&path).map_err(|source| ApkError::Io { path: path.clone(), source })?;
225 let map = unsafe { Mmap::map(&file) }.map_err(|source| ApkError::Io { path: path.clone(), source })?;
228 let bare_dex = map.starts_with(b"dex\n");
229 Ok(Archive { path, map: Data::Mapped(map), label, bare_dex })
230 }
231
232 fn entry_name(&self, inner: &[u8]) -> String {
235 let inner = String::from_utf8_lossy(inner);
236 if self.label.is_empty() { inner.into_owned() } else { format!("{}/{inner}", self.label) }
237 }
238
239 fn inner_name<'n>(&self, name: &'n str) -> Option<&'n str> {
242 if self.label.is_empty() {
243 return None;
244 }
245 name.strip_prefix(self.label.as_str())?.strip_prefix('/')
246 }
247
248 fn central_directory(&self) -> Result<(usize, usize)> {
250 let data = &self.map[..];
251 let search_start = data.len().saturating_sub(65536 + 22);
252 let eocd = memchr::memmem::rfind(&data[search_start..], EOCD_SIG).map(|p| p + search_start).ok_or(ApkError::NoEocd)?;
253 let mut cd_size = u32_at(data, eocd + 12).ok_or(ApkError::NoEocd)? as u64;
254 let mut cd_off = u32_at(data, eocd + 16).ok_or(ApkError::NoEocd)? as u64;
255 if cd_size == 0xFFFF_FFFF || cd_off == 0xFFFF_FFFF {
256 if eocd >= 20 && &data[eocd - 20..eocd - 16] == ZIP64_LOCATOR_SIG {
258 let rec = u64_at(data, eocd - 20 + 8).and_then(offset).ok_or(ApkError::NoEocd)?;
259 if bytes_at(data, rec, 4) == Some(ZIP64_EOCD_SIG) {
260 cd_size = rec.checked_add(40).and_then(|o| u64_at(data, o)).ok_or(ApkError::NoEocd)?;
261 cd_off = rec.checked_add(48).and_then(|o| u64_at(data, o)).ok_or(ApkError::NoEocd)?;
262 }
263 }
264 }
265 let (Some(off), Some(size)) = (offset(cd_off), offset(cd_size)) else {
266 return Err(ApkError::BadCentralDirectory);
267 };
268 if off.checked_add(size).map(|e| e > data.len()).unwrap_or(true) {
269 return Err(ApkError::BadCentralDirectory);
270 }
271 Ok((off, size))
272 }
273
274 fn directory(&self) -> Result<&[u8]> {
277 if self.bare_dex {
278 return Ok(&self.map[..self.map.len().min(DEX_HEADER_SIZE)]);
279 }
280 let (off, size) = self.central_directory()?;
281 Ok(&self.map[off..off + size])
282 }
283
284 fn entries_filtered(&self, index: usize, keep: &mut impl FnMut(&[u8]) -> bool) -> Result<Vec<Entry>> {
287 if self.bare_dex {
288 if !keep(BARE_DEX_ENTRY.as_bytes()) {
289 return Ok(Vec::new());
290 }
291 let len = self.map.len() as u64;
292 return Ok(vec![Entry {
293 name: self.entry_name(BARE_DEX_ENTRY.as_bytes()),
294 method: METHOD_STORED,
295 comp_size: len,
296 uncomp_size: len,
297 local_header_off: 0,
298 crc32: 0,
299 archive: index,
300 }]);
301 }
302 let data = &self.map[..];
303 let (cd_off, cd_size) = self.central_directory()?;
304 let cd_end = cd_off + cd_size;
305 let mut entries = Vec::new();
306 let mut p = cd_off;
307 while p + 46 <= cd_end {
308 if &data[p..p + 4] != CD_SIG {
309 break;
310 }
311 let name_len = u16_at(data, p + 28).unwrap_or(0) as usize;
312 let extra_len = u16_at(data, p + 30).unwrap_or(0) as usize;
313 let comment_len = u16_at(data, p + 32).unwrap_or(0) as usize;
314 let name_start = p + 46;
315 let name_end = name_start + name_len;
316 if name_end > cd_end {
317 break;
318 }
319 let name = &data[name_start..name_end];
320 if keep(name) {
321 let mut comp_size = u32_at(data, p + 20).unwrap_or(0) as u64;
322 let mut uncomp_size = u32_at(data, p + 24).unwrap_or(0) as u64;
323 let mut local_header_off = u32_at(data, p + 42).unwrap_or(0) as u64;
324 if comp_size == 0xFFFF_FFFF || uncomp_size == 0xFFFF_FFFF || local_header_off == 0xFFFF_FFFF {
325 let mut q = name_end;
327 let extra_end = name_end + extra_len;
328 while q + 4 <= extra_end {
329 let id = u16_at(data, q).unwrap_or(0);
330 let len = u16_at(data, q + 2).unwrap_or(0) as usize;
331 if id == 0x0001 {
332 let mut r = q + 4;
333 if uncomp_size == 0xFFFF_FFFF {
334 uncomp_size = u64_at(data, r).unwrap_or(0);
335 r += 8;
336 }
337 if comp_size == 0xFFFF_FFFF {
338 comp_size = u64_at(data, r).unwrap_or(0);
339 r += 8;
340 }
341 if local_header_off == 0xFFFF_FFFF {
342 local_header_off = u64_at(data, r).unwrap_or(0);
343 }
344 break;
345 }
346 q += 4 + len;
347 }
348 }
349 entries.push(Entry {
350 name: self.entry_name(name),
351 method: u16_at(data, p + 10).unwrap_or(0),
352 comp_size,
353 uncomp_size,
354 local_header_off,
355 crc32: u32_at(data, p + 16).unwrap_or(0),
356 archive: index,
357 });
358 }
359 p = name_end + extra_len + comment_len;
360 }
361 Ok(entries)
362 }
363
364 fn dex_entries_in_order(&self, index: usize) -> Result<Vec<Entry>> {
367 let mut entries = self.entries_filtered(index, &mut is_root_dex)?;
368 entries.reverse();
370 let mut seen = std::collections::HashSet::with_capacity(entries.len());
371 entries.retain(|e| seen.insert(e.name.clone()));
372 entries.reverse();
373 Ok(entries)
374 }
375}
376
377impl Apk {
378 pub fn open(path: impl AsRef<Path>) -> Result<Apk> {
392 let path = path.as_ref().to_path_buf();
393 let sources = program::sources(&path)?;
394 let version = Version::of_sources(&sources);
395 if let [lone] = &sources[..] {
396 let archives = Arc::new([Archive::open(lone.path.clone(), String::new())?]);
397 return Ok(Apk { path, archives, limits: Limits::default(), notes: Arc::new([]), version });
398 }
399 let mut archives = Vec::with_capacity(sources.len());
400 let mut notes = Vec::new();
401 for s in sources {
402 let archive = match Archive::open(s.path, s.label) {
403 Ok(a) => a,
404 Err(e) => {
405 notes.push(format!("skipped: {e}"));
406 continue;
407 }
408 };
409 match archive.dex_entries_in_order(archives.len()) {
410 Ok(dex) if dex.is_empty() => notes.push(format!("{}: no classes*.dex (odex/vdex only?): deodex first", archive.label)),
411 Ok(_) => archives.push(archive),
412 Err(e) => notes.push(format!("{}: skipped: {e}", archive.label)),
413 }
414 }
415 if archives.is_empty() {
416 return Err(ApkError::NoCode { path, notes });
417 }
418 if let [lone] = &mut archives[..] {
419 lone.label.clear();
420 }
421 Ok(Apk { path, archives: archives.into(), limits: Limits::default(), notes: notes.into(), version })
422 }
423
424 pub fn version_of(path: impl AsRef<Path>) -> Result<Version> {
429 Ok(Version::of_sources(&program::sources(path.as_ref())?))
430 }
431
432 pub fn version(&self) -> &Version {
434 &self.version
435 }
436
437 pub fn resolve(spec: &Path, base: &Path) -> PathBuf {
440 program::resolve(spec, base)
441 }
442
443 pub fn with_limits(mut self, limits: Limits) -> Apk {
445 self.limits = limits;
446 self
447 }
448
449 pub fn path(&self) -> &Path {
451 &self.path
452 }
453
454 pub fn limits(&self) -> Limits {
456 self.limits
457 }
458
459 pub fn archive_count(&self) -> usize {
462 self.archives.len()
463 }
464
465 pub fn notes(&self) -> &[String] {
467 &self.notes
468 }
469
470 pub fn archive_path(&self, entry: &Entry) -> &Path {
472 &self.archive(entry).path
473 }
474
475 pub fn archive_data(&self, entry: &Entry) -> &[u8] {
477 &self.archive(entry).map
478 }
479
480 fn archive(&self, entry: &Entry) -> &Archive {
481 self.archives.get(entry.archive).expect("entry of this APK")
482 }
483
484 pub fn directories(&self) -> Result<Vec<ArchiveId<'_>>> {
487 self.archives.iter().map(|a| Ok(ArchiveId { path: &a.path, label: &a.label, directory: a.directory()? })).collect()
488 }
489
490 pub fn stem(&self) -> String {
493 let first = self.archives[0].path.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default();
494 match self.archives.len() {
495 1 => first,
496 n => format!("{first}-and-{}", n - 1),
497 }
498 }
499
500 pub fn stamp(&self) -> Result<(u64, Option<SystemTime>)> {
504 let meta = |a: &Archive| std::fs::metadata(&a.path).map_err(|source| ApkError::Io { path: a.path.clone(), source });
505 if let [lone] = &self.archives[..] {
506 let m = meta(lone)?;
507 return Ok((m.len(), m.modified().ok()));
508 }
509 let mut size = 0u64;
510 let mut latest = None;
511 for a in self.archives.iter() {
512 let m = meta(a)?;
513 let since = m.modified().ok().and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok());
514 let nanos = since.map_or(0, |d| d.as_nanos() as u64);
516 size = size.rotate_left(13) ^ m.len().wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ nanos;
517 latest = latest.max(m.modified().ok());
518 }
519 Ok((size, latest))
520 }
521
522 pub fn entries_filtered(&self, mut keep: impl FnMut(&[u8]) -> bool) -> Result<Vec<Entry>> {
526 let mut entries = Vec::new();
527 for (i, a) in self.archives.iter().enumerate() {
528 entries.extend(a.entries_filtered(i, &mut keep)?);
529 }
530 Ok(entries)
531 }
532
533 pub fn entries(&self) -> Result<Vec<Entry>> {
535 self.entries_filtered(|_| true)
536 }
537
538 pub fn dex_entries(&self) -> Result<Vec<Entry>> {
542 let mut entries = self.dex_entries_in_order()?;
543 self.check_dex_total(&entries)?;
544 entries.sort_by_key(|e| e.comp_size);
545 Ok(entries)
546 }
547
548 pub fn dex_entries_in_order(&self) -> Result<Vec<Entry>> {
551 let mut entries = Vec::new();
552 for (i, a) in self.archives.iter().enumerate() {
553 entries.extend(a.dex_entries_in_order(i)?);
554 }
555 Ok(entries)
556 }
557
558 fn check_dex_total(&self, entries: &[Entry]) -> Result<()> {
559 let total = entries.iter().fold(0u64, |t, e| t.saturating_add(e.uncomp_size));
560 if total > self.limits.max_dex_total_bytes {
561 return Err(ApkError::LimitExceeded {
562 name: "classes*.dex".into(),
563 reason: format!("DEX entries declare {total} bytes in all, limit {}", self.limits.max_dex_total_bytes),
564 });
565 }
566 Ok(())
567 }
568
569 pub fn find_entry(&self, name: &str) -> Result<Option<Entry>> {
573 let owner = self.archives.iter().enumerate().find_map(|(i, a)| Some((i, a.inner_name(name)?)));
574 let candidates: Vec<(usize, &str)> = match owner {
575 Some(found) => vec![found],
576 None => (0..self.archives.len()).map(|i| (i, name)).collect(),
577 };
578 for (i, inner) in candidates {
579 let mut found = self.archives[i].entries_filtered(i, &mut |n: &[u8]| n == inner.as_bytes())?;
580 if let Some(e) = found.pop() {
581 return Ok(Some(e));
582 }
583 }
584 Ok(None)
585 }
586
587 pub fn raw_payload(&self, entry: &Entry) -> Result<&[u8]> {
589 let range = self.payload_range(entry)?;
590 Ok(&self.archive(entry).map[range])
591 }
592
593 fn payload_range(&self, entry: &Entry) -> Result<std::ops::Range<usize>> {
595 let archive = self.archive(entry);
596 let data = &archive.map[..];
597 let bad = || ApkError::BadLocalHeader(entry.name.clone());
598 if archive.bare_dex {
599 return offset(entry.comp_size).filter(|&n| n <= data.len()).map(|n| 0..n).ok_or_else(bad);
600 }
601 let lh = offset(entry.local_header_off).ok_or_else(bad)?;
602 if bytes_at(data, lh, 4) != Some(LH_SIG) {
603 return Err(bad());
604 }
605 let name_len = u16_at(data, lh + 26).ok_or_else(bad)? as usize;
607 let extra_len = u16_at(data, lh + 28).ok_or_else(bad)? as usize;
608 let start = lh + 30 + name_len + extra_len;
609 let end = offset(entry.comp_size).and_then(|n| start.checked_add(n));
610 end.filter(|&end| start <= end && end <= data.len()).map(|end| start..end).ok_or_else(bad)
611 }
612
613 pub fn lone_data(&self) -> Option<&[u8]> {
617 match &self.archives[..] {
618 [lone] => Some(&lone.map),
619 _ => None,
620 }
621 }
622
623 pub fn owned_bytes(&self) -> usize {
626 self.archives.iter().map(|archive| if let Data::Owned(bytes) = &archive.map { bytes.capacity() } else { 0 }).sum()
627 }
628
629 pub fn open_nested(&self, entry: &Entry) -> Result<Apk> {
635 let map = match entry.method {
636 METHOD_STORED => {
637 self.read_entry(entry)?;
639 Data::Window { outer: Arc::clone(&self.archives), archive: entry.archive, range: self.payload_range(entry)? }
640 }
641 _ => Data::Owned(self.read_entry(entry)?.into_owned()),
642 };
643 let bare_dex = map.starts_with(b"dex\n");
644 let nested = Archive { path: self.archive(entry).path.clone(), map, label: String::new(), bare_dex };
645 Ok(Apk {
646 path: self.path.clone(),
647 archives: Arc::new([nested]),
648 limits: self.limits,
649 notes: Arc::new([]),
650 version: self.version.clone(),
651 })
652 }
653
654 pub fn read_entry(&self, entry: &Entry) -> Result<Cow<'_, [u8]>> {
657 self.read_entry_cancellable(entry, None)
658 }
659
660 pub fn read_entry_cancellable(&self, entry: &Entry, cancel: Option<&AtomicBool>) -> Result<Cow<'_, [u8]>> {
663 if cancel.is_some_and(|c| c.load(Ordering::Relaxed)) {
664 return Err(ApkError::Cancelled);
665 }
666 let payload = self.raw_payload(entry)?;
667 let bound = self.check_declared_size(entry)?;
668 let out = match entry.method {
669 METHOD_STORED => {
670 if entry.uncomp_size != 0 && payload.len() as u64 != entry.uncomp_size {
671 return Err(ApkError::SizeMismatch {
672 name: entry.name.clone(),
673 expected: entry.uncomp_size as usize,
674 actual: payload.len(),
675 });
676 }
677 return Ok(Cow::Borrowed(payload));
678 }
679 #[cfg(feature = "libdeflate")]
680 METHOD_DEFLATE if entry.uncomp_size > 0 => {
681 if let Some(c) = cancel
682 && c.load(Ordering::Relaxed)
683 {
684 return Err(ApkError::Cancelled);
685 }
686 let mut out = vec![0u8; entry.uncomp_size as usize];
687 let n = libdeflater::Decompressor::new()
688 .deflate_decompress(payload, &mut out)
689 .map_err(|e| ApkError::Inflate { name: entry.name.clone(), source: std::io::Error::other(format!("{e:?}")) })?;
690 out.truncate(n);
691 out
692 }
693 METHOD_DEFLATE => {
694 let reader = CancelReader { data: payload, pos: 0, cancel };
695 let decoder = flate2::read::DeflateDecoder::new(reader);
696 let reserve = (entry.uncomp_size as usize).min(INITIAL_CAPACITY.max(payload.len().saturating_mul(TYPICAL_RATIO)));
697 let mut out = Vec::with_capacity(reserve);
698 match decoder.take(bound.saturating_add(1)).read_to_end(&mut out) {
702 Ok(_) => {}
703 Err(_) if cancel.is_some_and(|c| c.load(Ordering::Relaxed)) => return Err(ApkError::Cancelled),
704 Err(e) => return Err(ApkError::Inflate { name: entry.name.clone(), source: e }),
705 }
706 if out.len() as u64 > bound {
707 return Err(self.past_bound(entry, bound));
708 }
709 out
710 }
711 m => return Err(ApkError::UnsupportedMethod(m)),
712 };
713 if let Some(c) = cancel
714 && c.load(Ordering::Relaxed)
715 {
716 return Err(ApkError::Cancelled);
717 }
718 if entry.uncomp_size != 0 && out.len() as u64 != entry.uncomp_size {
719 return Err(ApkError::SizeMismatch { name: entry.name.clone(), expected: entry.uncomp_size as usize, actual: out.len() });
720 }
721 Ok(Cow::Owned(out))
722 }
723
724 fn check_declared_size(&self, entry: &Entry) -> Result<u64> {
729 let bound = entry.comp_size.saturating_mul(MAX_DEFLATE_RATIO).saturating_add(1024);
730 if entry.method == METHOD_DEFLATE && entry.uncomp_size > bound {
731 return Err(self.implausible(entry));
732 }
733 let Limits { max_entry_bytes, max_ratio, .. } = self.limits;
734 if entry.uncomp_size > max_entry_bytes {
735 return Err(self.over_limit(entry, format!("declares {} bytes, limit {max_entry_bytes}", entry.uncomp_size)));
736 }
737 let ratio_bound = entry.comp_size.saturating_mul(max_ratio).saturating_add(1024);
738 if entry.method == METHOD_DEFLATE && entry.uncomp_size > ratio_bound {
739 return Err(self.over_limit(
740 entry,
741 format!("declares {} bytes from {} compressed, ratio limit {max_ratio}", entry.uncomp_size, entry.comp_size),
742 ));
743 }
744 Ok(bound.min(ratio_bound).min(max_entry_bytes))
745 }
746
747 fn past_bound(&self, entry: &Entry, bound: u64) -> ApkError {
749 if bound < entry.comp_size.saturating_mul(MAX_DEFLATE_RATIO).saturating_add(1024) {
750 self.over_limit(entry, format!("inflates past {bound} bytes"))
751 } else {
752 self.implausible(entry)
753 }
754 }
755
756 fn over_limit(&self, entry: &Entry, reason: String) -> ApkError {
757 ApkError::LimitExceeded { name: entry.name.clone(), reason }
758 }
759
760 fn implausible(&self, entry: &Entry) -> ApkError {
761 ApkError::ImplausibleSize { name: entry.name.clone(), comp_size: entry.comp_size, uncomp_size: entry.uncomp_size }
762 }
763
764 pub fn read(&self, name: &str) -> Result<Vec<u8>> {
766 let entry = self.find_entry(name)?.ok_or_else(|| ApkError::NotFound(name.to_string()))?;
767 Ok(self.read_entry(&entry)?.into_owned())
768 }
769}
770
771#[derive(Debug, Clone, Copy, PartialEq, Eq)]
773pub enum PrefixStep {
774 Continue(usize),
777 Abort,
779}
780
781#[derive(Debug, Clone, PartialEq, Eq)]
783pub enum Prefix {
784 Complete(Vec<u8>),
786 Partial(Vec<u8>),
788 Aborted(Vec<u8>),
790}
791
792impl Apk {
793 pub fn inflate_until(&self, entry: &Entry, mut decide: impl FnMut(&[u8]) -> PrefixStep) -> Result<Prefix> {
797 const FIRST_CHUNK: usize = 1 << 14;
799 const CHUNK: usize = 1 << 18;
800 let mut chunk = FIRST_CHUNK;
801 if entry.method != METHOD_DEFLATE {
802 return Ok(Prefix::Complete(self.read_entry(entry)?.into_owned()));
803 }
804 let compressed = self.raw_payload(entry)?;
805 let bound = self.check_declared_size(entry)?;
806 let mut d = flate2::Decompress::new(false);
807 let mut out: Vec<u8> = Vec::with_capacity((entry.uncomp_size as usize).min(4 * CHUNK));
808 let mut target: Option<usize> = None;
809 loop {
810 if out.len() as u64 > bound {
811 return Err(self.past_bound(entry, bound));
812 }
813 let base = out.len();
814 out.resize(base + chunk, 0);
815 chunk = (chunk * 2).min(CHUNK);
816 let consumed = d.total_in() as usize;
817 let status = d
818 .decompress(&compressed[consumed.min(compressed.len())..], &mut out[base..], flate2::FlushDecompress::None)
819 .map_err(|e| ApkError::Inflate { name: entry.name.clone(), source: std::io::Error::other(e) })?;
820 let written = d.total_out() as usize - base;
821 out.truncate(base + written);
822 if target.is_none() {
823 match decide(&out) {
824 PrefixStep::Continue(0) => {}
825 PrefixStep::Continue(limit) => {
826 target = Some(if entry.uncomp_size == 0 { limit } else { limit.min(entry.uncomp_size as usize) })
828 }
829 PrefixStep::Abort => return Ok(Prefix::Aborted(out)),
830 }
831 }
832 if status == flate2::Status::StreamEnd {
833 return Ok(Prefix::Complete(out));
834 }
835 if let Some(limit) = target
836 && out.len() >= limit
837 {
838 return Ok(Prefix::Partial(out));
839 }
840 if written == 0 && d.total_in() as usize == consumed {
841 return Err(ApkError::Inflate { name: entry.name.clone(), source: std::io::Error::other("deflate made no progress") });
842 }
843 }
844 }
845}
846
847struct CancelReader<'a> {
849 data: &'a [u8],
850 pos: usize,
851 cancel: Option<&'a AtomicBool>,
852}
853
854impl Read for CancelReader<'_> {
855 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
856 if let Some(c) = self.cancel
857 && c.load(Ordering::Relaxed)
858 {
859 return Err(std::io::Error::other("cancelled"));
860 }
861 let remaining = &self.data[self.pos..];
862 let n = remaining.len().min(buf.len()).min(DEFLATE_CHUNK);
863 buf[..n].copy_from_slice(&remaining[..n]);
864 self.pos += n;
865 Ok(n)
866 }
867}
868
869#[cfg(test)]
870mod tests {
871 use super::*;
872 use std::io::Write;
873
874 fn write_temp(bytes: &[u8]) -> tempfile::NamedTempFile {
875 let mut f = tempfile::NamedTempFile::new().unwrap();
876 f.write_all(bytes).unwrap();
877 f.flush().unwrap();
878 f
879 }
880
881 #[test]
882 fn limits_reject_bombs_before_inflating() {
883 let bomb = vec![0u8; 4 << 20];
885 let f = write_temp(&asc_fixture::make_apk(&[("classes.dex", &bomb)], true));
886 let apk = Apk::open(f.path()).unwrap();
887 let entry = apk.dex_entries().unwrap().remove(0);
888 assert_eq!(apk.read_entry(&entry).unwrap().len(), bomb.len());
889 let apk = Apk::open(f.path()).unwrap().with_limits(Limits::guarded());
890 let err = apk.read_entry(&entry).unwrap_err();
891 assert!(matches!(&err, ApkError::LimitExceeded { reason, .. } if reason.contains("ratio limit 100")), "{err}");
892 let mut undeclared = entry.clone();
894 undeclared.uncomp_size = 0;
895 assert!(matches!(apk.read_entry(&undeclared), Err(ApkError::LimitExceeded { .. })));
896 let small = Limits { max_entry_bytes: 1 << 20, ..Limits::default() };
897 let apk = Apk::open(f.path()).unwrap().with_limits(small);
898 assert!(matches!(apk.read_entry(&entry), Err(ApkError::LimitExceeded { .. })));
899 let total = Limits { max_dex_total_bytes: 1 << 20, ..Limits::default() };
900 let apk = Apk::open(f.path()).unwrap().with_limits(total);
901 assert!(matches!(apk.dex_entries(), Err(ApkError::LimitExceeded { .. })));
902 }
903
904 #[test]
905 fn enumerates_and_inflates_dex_entries() {
906 let dex = asc_fixture::make_dex();
907 for deflate in [false, true] {
908 let apk_bytes = asc_fixture::make_apk(
909 &[("classes.dex", &dex), ("lib/x/classes.dex", &dex), ("classes2.dex", &dex), ("AndroidManifest.xml", b"xml")],
910 deflate,
911 );
912 let f = write_temp(&apk_bytes);
913 let apk = Apk::open(f.path()).unwrap();
914 let entries = apk.dex_entries().unwrap();
915 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
916 assert_eq!(names.len(), 2);
917 assert!(names.contains(&"classes.dex") && names.contains(&"classes2.dex"));
918 for e in &entries {
919 assert_eq!(apk.read_entry(e).unwrap().as_ref(), dex.as_slice());
920 }
921 assert_eq!(apk.read("AndroidManifest.xml").unwrap(), b"xml");
922 let cancel = AtomicBool::new(true);
923 let r = apk.read_entry_cancellable(&entries[0], Some(&cancel));
924 assert!(matches!(r, Err(ApkError::Cancelled)));
925 if deflate {
926 let mut lying = entries[0].clone();
928 lying.uncomp_size = 1 << 40;
929 assert!(matches!(apk.read_entry(&lying), Err(ApkError::ImplausibleSize { .. })));
930 assert!(matches!(apk.inflate_until(&lying, |_| PrefixStep::Continue(0)), Err(ApkError::ImplausibleSize { .. })));
931 let mut truncated = entries[0].clone();
932 truncated.comp_size = u64::MAX;
933 assert!(matches!(apk.read_entry(&truncated), Err(ApkError::BadLocalHeader(_))));
934 }
935 }
936 }
937
938 fn zeros_deflated(n: usize) -> Vec<u8> {
940 let mut e = flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::best());
941 e.write_all(&vec![0u8; n]).unwrap();
942 e.finish().unwrap()
943 }
944
945 fn raw_zip(entries: &[(&str, u16, &[u8], u32, u32)]) -> Vec<u8> {
948 let mut z = Vec::new();
949 let mut cd = Vec::new();
950 for (name, method, payload, comp_size, uncomp_size) in entries {
951 let lh = z.len() as u32;
952 z.extend_from_slice(LH_SIG);
953 z.extend_from_slice(&[20, 0, 0, 0]);
954 z.extend_from_slice(&method.to_le_bytes());
955 z.extend_from_slice(&[0; 8]);
956 z.extend_from_slice(&comp_size.to_le_bytes());
957 z.extend_from_slice(&uncomp_size.to_le_bytes());
958 z.extend_from_slice(&(name.len() as u16).to_le_bytes());
959 z.extend_from_slice(&0u16.to_le_bytes());
960 z.extend_from_slice(name.as_bytes());
961 z.extend_from_slice(payload);
962 cd.extend_from_slice(CD_SIG);
963 cd.extend_from_slice(&[20, 0, 20, 0, 0, 0]);
964 cd.extend_from_slice(&method.to_le_bytes());
965 cd.extend_from_slice(&[0; 8]);
966 cd.extend_from_slice(&comp_size.to_le_bytes());
967 cd.extend_from_slice(&uncomp_size.to_le_bytes());
968 cd.extend_from_slice(&(name.len() as u16).to_le_bytes());
969 cd.extend_from_slice(&[0; 12]);
970 cd.extend_from_slice(&lh.to_le_bytes());
971 cd.extend_from_slice(name.as_bytes());
972 }
973 let cd_off = z.len() as u32;
974 z.extend_from_slice(&cd);
975 z.extend_from_slice(EOCD_SIG);
976 z.extend_from_slice(&[0, 0, 0, 0]);
977 z.extend_from_slice(&(entries.len() as u16).to_le_bytes());
978 z.extend_from_slice(&(entries.len() as u16).to_le_bytes());
979 z.extend_from_slice(&(cd.len() as u32).to_le_bytes());
980 z.extend_from_slice(&cd_off.to_le_bytes());
981 z.extend_from_slice(&[0, 0]);
982 z
983 }
984
985 #[test]
986 fn undeclared_size_is_still_bounded() {
987 let ok = zeros_deflated(1 << 20);
989 let f = write_temp(&raw_zip(&[("classes.dex", METHOD_DEFLATE, &ok, ok.len() as u32, 0)]));
990 let apk = Apk::open(f.path()).unwrap();
991 let e = apk.dex_entries().unwrap().remove(0);
992 assert_eq!(e.uncomp_size, 0);
993 assert_eq!(apk.read_entry(&e).unwrap().len(), 1 << 20);
994 match apk.inflate_until(&e, |_| PrefixStep::Continue(4096)).unwrap() {
995 Prefix::Partial(v) => assert!(v.len() >= 4096),
996 Prefix::Complete(v) => assert_eq!(v.len(), 1 << 20),
997 Prefix::Aborted(_) => panic!("unexpected abort"),
998 }
999 let mut lying = e.clone();
1002 lying.comp_size = 200;
1003 assert!(apk.read_entry(&lying).is_err());
1004 assert!(apk.inflate_until(&lying, |_| PrefixStep::Continue(0)).is_err());
1005 }
1006
1007 #[test]
1008 fn hostile_offsets_do_not_overflow() {
1009 let dex = asc_fixture::make_dex();
1010 let f = write_temp(&asc_fixture::make_apk(&[("classes.dex", &dex)], true));
1011 let apk = Apk::open(f.path()).unwrap();
1012 let mut e = apk.dex_entries().unwrap().remove(0);
1013 for off in [u64::MAX, u64::MAX - 3, u64::MAX - 40, u32::MAX as u64] {
1014 e.local_header_off = off;
1015 assert!(matches!(apk.read_entry(&e), Err(ApkError::BadLocalHeader(_))));
1016 assert!(matches!(apk.inflate_until(&e, |_| PrefixStep::Continue(0)), Err(ApkError::BadLocalHeader(_))));
1017 }
1018 let mut bytes = asc_fixture::make_apk(&[("classes.dex", &dex)], false);
1020 let eocd = bytes.len() - 22;
1021 bytes[eocd + 16..eocd + 20].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
1022 let mut locator = Vec::new();
1023 locator.extend_from_slice(ZIP64_LOCATOR_SIG);
1024 locator.extend_from_slice(&0u32.to_le_bytes());
1025 locator.extend_from_slice(&(u64::MAX - 2).to_le_bytes());
1026 locator.extend_from_slice(&1u32.to_le_bytes());
1027 let tail = bytes.split_off(eocd);
1028 bytes.extend_from_slice(&locator);
1029 bytes.extend_from_slice(&tail);
1030 let f = write_temp(&bytes);
1031 let apk = Apk::open(f.path()).unwrap();
1032 assert!(matches!(apk.dex_entries(), Err(ApkError::NoEocd | ApkError::BadCentralDirectory)));
1033 }
1034
1035 #[test]
1036 fn duplicate_root_dex_names_keep_the_last() {
1037 let a = asc_fixture::make_dex();
1038 let mut b = a.clone();
1039 b.push(0); let (la, lb) = (a.len() as u32, b.len() as u32);
1041 let f = write_temp(&raw_zip(&[
1042 ("classes.dex", METHOD_STORED, &a, la, la),
1043 ("classes2.dex", METHOD_STORED, &a, la, la),
1044 ("classes.dex", METHOD_STORED, &b, lb, lb),
1045 ]));
1046 let apk = Apk::open(f.path()).unwrap();
1047 let entries = apk.dex_entries().unwrap();
1048 let mut names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
1049 names.sort();
1050 assert_eq!(names, ["classes.dex", "classes2.dex"]);
1051 let dup = entries.iter().find(|e| e.name == "classes.dex").unwrap();
1052 assert_eq!(apk.read_entry(dup).unwrap().as_ref(), b.as_slice());
1053 assert_eq!(apk.find_entry("classes.dex").unwrap().unwrap(), *dup);
1054 let ordered: Vec<String> = apk.dex_entries_in_order().unwrap().into_iter().map(|e| e.name).collect();
1056 assert_eq!(ordered, ["classes2.dex", "classes.dex"]);
1057 }
1058
1059 #[test]
1060 fn programs_name_entries_after_their_archive() {
1061 let dex = asc_fixture::make_dex();
1062 let dir = tempfile::tempdir().unwrap();
1063 let root = dir.path().join("system");
1064 std::fs::create_dir_all(root.join("app/Tele")).unwrap();
1065 let both = [("classes.dex", &dex[..]), ("classes2.dex", &dex[..]), ("AndroidManifest.xml", b"first")];
1066 std::fs::write(root.join("services.jar"), asc_fixture::make_apk(&both, true)).unwrap();
1067 let apk = [("classes.dex", &dex[..]), ("AndroidManifest.xml", b"second")];
1068 std::fs::write(root.join("app/Tele/Tele.apk"), asc_fixture::make_apk(&apk, false)).unwrap();
1069 std::fs::write(root.join("odexed.jar"), asc_fixture::make_apk(&[("META-INF/MANIFEST.MF", b"m")], false)).unwrap();
1070 std::fs::write(root.join("bare.dex"), &dex).unwrap();
1071 std::fs::write(root.join("broken.zip"), b"not a zip").unwrap();
1072
1073 let program = Apk::open(&root).unwrap();
1074 assert_eq!(program.archive_count(), 3);
1075 let names: Vec<String> = program.dex_entries_in_order().unwrap().into_iter().map(|e| e.name).collect();
1076 assert_eq!(
1077 names,
1078 ["app/Tele/Tele.apk/classes.dex", "bare.dex/classes.dex", "services.jar/classes.dex", "services.jar/classes2.dex"]
1079 );
1080 for e in program.dex_entries().unwrap() {
1081 assert_eq!(program.read_entry(&e).unwrap().as_ref(), dex.as_slice(), "{}", e.name);
1082 }
1083 let notes = program.notes().join("\n");
1084 assert!(notes.contains("broken.zip") && notes.contains("odexed.jar: no classes*.dex"), "{notes}");
1085 assert_eq!(program.read("AndroidManifest.xml").unwrap(), b"second");
1087 assert_eq!(program.read("services.jar/AndroidManifest.xml").unwrap(), b"first");
1088 let bare = program.find_entry("bare.dex/classes.dex").unwrap().unwrap();
1089 assert_eq!(program.archive_path(&bare), root.join("bare.dex"));
1090 assert_eq!(archive_of("app/Tele/Tele.apk/classes.dex"), Some("app/Tele/Tele.apk"));
1091 assert_eq!(archive_of("services.jar/classes5.dex!classes1.dex"), Some("services.jar"));
1092 assert_eq!(archive_of("classes5.dex!classes1.dex"), None);
1093 assert_eq!(program.stem(), "Tele-and-2");
1094 assert!(program.stamp().is_ok() && program.directories().unwrap().len() == 3);
1095
1096 let lone = Apk::open(root.join("bare.dex")).unwrap();
1098 assert_eq!(lone.dex_entries().unwrap()[0].name, "classes.dex");
1099 let spec = format!("{},{}", root.join("services.jar").display(), root.join("odexed.jar").display());
1100 let one = Apk::open(&spec).unwrap();
1101 assert_eq!(one.archive_count(), 1);
1102 assert_eq!(one.dex_entries_in_order().unwrap()[0].name, "classes.dex");
1103 assert_eq!(one.notes().len(), 1);
1104 let spec = format!("{},{}", root.join("odexed.jar").display(), root.join("broken.zip").display());
1105 assert!(matches!(Apk::open(&spec), Err(ApkError::NoCode { .. })));
1106 }
1107
1108 #[test]
1109 fn inflate_until_streams_prefixes() {
1110 let dex = asc_fixture::make_dex();
1111 let apk_bytes = asc_fixture::make_apk(&[("classes.dex", &dex)], true);
1112 let f = write_temp(&apk_bytes);
1113 let apk = Apk::open(f.path()).unwrap();
1114 let entry = apk.dex_entries().unwrap().remove(0);
1115 match apk.inflate_until(&entry, |_| PrefixStep::Continue(8)).unwrap() {
1116 Prefix::Complete(v) | Prefix::Partial(v) => assert!(v.starts_with(b"dex\n") && v.len() >= 8),
1117 Prefix::Aborted(_) => panic!("unexpected abort"),
1118 }
1119 assert!(matches!(apk.inflate_until(&entry, |_| PrefixStep::Abort).unwrap(), Prefix::Aborted(_)));
1120 }
1121
1122 #[test]
1123 fn nested_archives_open_in_place_or_inflated() {
1124 let inner = asc_fixture::make_apk(&[("ets/modules.abc", b"PANDA body"), ("module.json", b"{}")], false);
1125 for deflate in [false, true] {
1126 let file = write_temp(&asc_fixture::make_apk(&[("entry.hap", &inner), ("pack.info", b"{}")], deflate));
1127 let outer = Apk::open(file.path()).unwrap();
1128 assert!(outer.lone_data().is_some_and(|data| data.starts_with(LH_SIG)));
1129 let entry = outer.find_entry("entry.hap").unwrap().unwrap();
1130 assert_eq!(entry.method, if deflate { METHOD_DEFLATE } else { METHOD_STORED });
1131 let nested = outer.open_nested(&entry).unwrap();
1132 assert_eq!(nested.lone_data(), Some(&inner[..]));
1133 let outer_range = outer.lone_data().unwrap().as_ptr_range();
1135 assert_eq!(outer_range.contains(&nested.lone_data().unwrap().as_ptr()), !deflate);
1136 let names: Vec<String> = nested.entries().unwrap().into_iter().map(|e| e.name).collect();
1137 assert_eq!(names, ["ets/modules.abc", "module.json"]);
1138 let abc = nested.find_entry("ets/modules.abc").unwrap().unwrap();
1139 assert!(matches!(nested.read_entry(&abc).unwrap(), Cow::Borrowed(b"PANDA body")));
1140 drop(outer);
1142 assert_eq!(nested.read("module.json").unwrap(), b"{}");
1143 }
1144 let not_zip = write_temp(&asc_fixture::make_apk(&[("x.hap", b"not a zip")], false));
1145 let outer = Apk::open(not_zip.path()).unwrap();
1146 let nested = outer.open_nested(&outer.find_entry("x.hap").unwrap().unwrap()).unwrap();
1147 assert!(matches!(nested.entries(), Err(ApkError::NoEocd)));
1148 }
1149}