Skip to main content

asc_apk/
lib.rs

1//! Read-only APK (zip) access.
2//!
3//! The archive is memory-mapped and only its central directory is walked.
4//! Entries are extracted on demand: stored entries are borrowed straight from
5//! the mapping, Deflate entries are inflated (optionally with a cancellation
6//! flag; the streaming decoder polls it on every input read, the libdeflate
7//! path once per entry, so a parallel class search can abandon the remaining
8//! DEX files as soon as one worker hits).
9//!
10//! Several archives can also be read as one program ([`Apk::open`]).
11
12mod 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;
31/// Deflate cannot expand data by more than about 1032:1; a central directory
32/// claiming more is corrupt or hostile and must not drive an allocation.
33const MAX_DEFLATE_RATIO: u64 = 1032;
34/// A declared size is reserved up front only up to the larger of this and
35/// [`TYPICAL_RATIO`] times the compressed payload; anything bigger grows on
36/// demand, so a lying central directory cannot force a large allocation
37/// while real DEX entries still get a single exact one.
38const INITIAL_CAPACITY: usize = 64 << 20;
39/// Real DEX files inflate well under this ratio.
40const TYPICAL_RATIO: usize = 16;
41
42/// Zip compression method 0: the entry is stored uncompressed.
43pub const METHOD_STORED: u16 = 0;
44/// Zip compression method 8: the entry is Deflate-compressed.
45pub const METHOD_DEFLATE: u16 = 8;
46
47/// Why an archive could not be opened or an entry read.
48#[derive(Debug, thiserror::Error)]
49#[non_exhaustive]
50pub enum ApkError {
51    /// Opening, mapping or reading the metadata of a file failed.
52    #[error("io error on {path}: {source}")]
53    Io {
54        /// The file or directory.
55        path: PathBuf,
56        /// The underlying error.
57        source: std::io::Error,
58    },
59    /// No end-of-central-directory record: not a zip, or truncated.
60    #[error("end of central directory not found")]
61    NoEocd,
62    /// The central directory lies outside the file.
63    #[error("central directory out of range")]
64    BadCentralDirectory,
65    /// The local header of the named entry is missing or its payload runs
66    /// past the end of the file.
67    #[error("bad local header signature for entry {0}")]
68    BadLocalHeader(String),
69    /// A compression method other than stored or Deflate.
70    #[error("unsupported compression method {0}")]
71    UnsupportedMethod(u16),
72    /// The Deflate stream of an entry is corrupt.
73    #[error("inflate failed for entry {name}: {source}")]
74    Inflate {
75        /// The entry name.
76        name: String,
77        /// The decoder's error.
78        source: std::io::Error,
79    },
80    /// An entry's data is not as long as the central directory says.
81    #[error("size mismatch for entry {name}: expected {expected}, got {actual}")]
82    SizeMismatch {
83        /// The entry name.
84        name: String,
85        /// Uncompressed size in bytes from the central directory.
86        expected: usize,
87        /// Bytes actually read or inflated.
88        actual: usize,
89    },
90    /// No entry of that name.
91    #[error("entry {0} not found")]
92    NotFound(String),
93    /// An entry declares (or inflates to) more than Deflate can produce from
94    /// its compressed size: a corrupt or hostile central directory.
95    #[error("entry {name} declares an implausible size (compressed {comp_size}, uncompressed {uncomp_size})")]
96    ImplausibleSize {
97        /// The entry name.
98        name: String,
99        /// Compressed size in bytes.
100        comp_size: u64,
101        /// Declared uncompressed size in bytes.
102        uncomp_size: u64,
103    },
104    /// An entry, or the DEX entries together, exceed the configured
105    /// [`Limits`].
106    #[error("entry {name} exceeds the inflate limit: {reason}")]
107    LimitExceeded {
108        /// The entry name (`classes*.dex` for the DEX total).
109        name: String,
110        /// Which limit, and by how much.
111        reason: String,
112    },
113    /// The caller's cancellation flag was raised.
114    #[error("cancelled")]
115    Cancelled,
116    /// A program path where no archive has root DEX entries.
117    #[error("no archive of {} has classes*.dex ({})", path.display(), notes.join("; "))]
118    NoCode {
119        /// The path as given to [`Apk::open`].
120        path: PathBuf,
121        /// Why each archive was left out, one line each.
122        notes: Vec<String>,
123    },
124}
125
126/// `Result` with [`ApkError`].
127pub type Result<T> = std::result::Result<T, ApkError>;
128
129/// One central-directory entry.
130#[derive(Debug, Clone, PartialEq, Eq)]
131#[non_exhaustive]
132pub struct Entry {
133    /// Name inside the archive; in a program, prefixed with the archive's
134    /// label (`services.jar/classes.dex`).
135    pub name: String,
136    /// Compression method ([`METHOD_STORED`], [`METHOD_DEFLATE`], ...).
137    pub method: u16,
138    /// Compressed size in bytes (zip64 sizes resolved).
139    pub comp_size: u64,
140    /// Uncompressed size in bytes as declared; 0 may mean unknown.
141    pub uncomp_size: u64,
142    /// Offset of the entry's local file header in its archive.
143    pub local_header_off: u64,
144    /// CRC-32 of the uncompressed data, as the central directory records it.
145    pub crc32: u32,
146    /// The archive of a program it belongs to ([`Apk::archive_path`]).
147    pub archive: usize,
148}
149
150/// Caps on what reading an APK may inflate, against zip bombs. The default
151/// only rejects what Deflate cannot produce; [`Limits::guarded`] is for
152/// sweeping untrusted APKs in bulk.
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154#[non_exhaustive]
155pub struct Limits {
156    /// Most bytes one entry may inflate to.
157    pub max_entry_bytes: u64,
158    /// Most an entry may expand over its compressed payload.
159    pub max_ratio: u64,
160    /// Most bytes all root DEX entries together may declare.
161    pub max_dex_total_bytes: u64,
162}
163
164impl Default for Limits {
165    fn default() -> Limits {
166        Limits::new()
167    }
168}
169
170impl Limits {
171    /// The default: only what Deflate cannot produce is rejected. A
172    /// `const fn`, so that with the setters below limits can be constants.
173    pub const fn new() -> Limits {
174        Limits { max_entry_bytes: u64::MAX, max_ratio: MAX_DEFLATE_RATIO, max_dex_total_bytes: u64::MAX }
175    }
176
177    /// Real DEX files are at most a few hundred MiB and compress 2-5:1.
178    pub const fn guarded() -> Limits {
179        Limits { max_entry_bytes: 1 << 30, max_ratio: 100, max_dex_total_bytes: 4 << 30 }
180    }
181
182    /// These limits with at most `bytes` inflated per entry.
183    #[must_use]
184    pub const fn max_entry_bytes(self, bytes: u64) -> Limits {
185        Limits { max_entry_bytes: bytes, ..self }
186    }
187
188    /// These limits with an entry expanding at most `ratio` times its
189    /// compressed payload.
190    #[must_use]
191    pub const fn max_ratio(self, ratio: u64) -> Limits {
192        Limits { max_ratio: ratio, ..self }
193    }
194
195    /// These limits with at most `bytes` declared by all root DEX entries
196    /// together.
197    #[must_use]
198    pub const fn max_dex_total_bytes(self, bytes: u64) -> Limits {
199        Limits { max_dex_total_bytes: bytes, ..self }
200    }
201}
202
203/// A memory-mapped APK, or several archives (APKs, jars, bare DEX files)
204/// read as one program: see [`Apk::open`]. Clones share the mappings, so a
205/// host can keep one open and hand out copies while [`Apk::version`] holds.
206#[derive(Clone)]
207pub struct Apk {
208    path: PathBuf,
209    archives: Arc<[Archive]>,
210    limits: Limits,
211    /// Archives of a program left out of its code, one line each.
212    notes: Arc<[String]>,
213    version: Version,
214}
215
216/// One archive of an [`Apk`] as a cache key sees it ([`Apk::directories`]).
217#[derive(Debug, Clone, Copy)]
218#[non_exhaustive]
219pub struct ArchiveId<'a> {
220    /// The archive's file.
221    pub path: &'a Path,
222    /// What its entry names start with in a program ([`archive_of`]); empty
223    /// for a lone archive.
224    pub label: &'a str,
225    /// Its central directory, or a bare DEX file's header.
226    pub directory: &'a [u8],
227}
228
229/// One file of an [`Apk`].
230struct Archive {
231    path: PathBuf,
232    map: Data,
233    /// Its name in a program of several archives (`services.jar`,
234    /// `app/TeleService/TeleService.apk`), which prefixes its entry names;
235    /// empty for a lone archive.
236    label: String,
237    /// A bare DEX file rather than a zip: one stored entry, the whole file.
238    bare_dex: bool,
239}
240
241/// The bytes of one archive: a mapped file, or an entry of another archive
242/// that is itself an archive ([`Apk::open_nested`]).
243enum Data {
244    Mapped(Mmap),
245    /// A stored entry, read in place: `range` of archive `archive` of `outer`.
246    Window {
247        outer: Arc<[Archive]>,
248        archive: usize,
249        range: std::ops::Range<usize>,
250    },
251    /// An inflated entry.
252    Owned(Vec<u8>),
253}
254
255impl std::ops::Deref for Data {
256    type Target = [u8];
257
258    fn deref(&self) -> &[u8] {
259        match self {
260            Data::Mapped(map) => map,
261            // The range was taken from this very archive when the window was made.
262            Data::Window { outer, archive, range } => &outer[*archive].map[range.clone()],
263            Data::Owned(bytes) => bytes,
264        }
265    }
266}
267
268/// `n` bytes at `o`, or `None` if they are not all inside `b` (offsets come
269/// from the archive, so the addition itself must not be trusted either).
270#[inline]
271fn bytes_at(b: &[u8], o: usize, n: usize) -> Option<&[u8]> {
272    b.get(o..o.checked_add(n)?)
273}
274#[inline]
275fn u16_at(b: &[u8], o: usize) -> Option<u16> {
276    bytes_at(b, o, 2).map(|s| u16::from_le_bytes([s[0], s[1]]))
277}
278#[inline]
279fn u32_at(b: &[u8], o: usize) -> Option<u32> {
280    bytes_at(b, o, 4).map(|s| u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
281}
282#[inline]
283fn u64_at(b: &[u8], o: usize) -> Option<u64> {
284    bytes_at(b, o, 8).map(|s| u64::from_le_bytes(s.try_into().expect("8 bytes")))
285}
286/// A `u64` archive offset as `usize`, or `None` when it cannot even be
287/// represented (32-bit hosts).
288#[inline]
289fn offset(v: u64) -> Option<usize> {
290    usize::try_from(v).ok()
291}
292
293/// The archive of a program a DEX of it comes from, by the DEX's name
294/// (`app/Tele.apk` for `app/Tele.apk/classes2.dex`, also as a logical DEX
295/// `...classes2.dex!classes1.dex`); `None` for a lone archive's.
296pub fn archive_of(dex_name: &str) -> Option<&str> {
297    let entry = dex_name.split('!').next().unwrap_or(dex_name);
298    entry.rsplit_once('/').map(|(archive, _)| archive)
299}
300
301/// Whether `name` (inside one archive) is a root-level `*.dex`
302/// (`classes.dex`, `classes2.dex`, ...).
303fn is_root_dex(name: &[u8]) -> bool {
304    name.ends_with(b".dex") && !name.contains(&b'/')
305}
306
307/// Whether `name` (inside one archive) is a `*.dex` at any depth
308/// (`classes.dex`, `assets/payload.dex`, ...).
309fn is_any_dex(name: &[u8]) -> bool {
310    name.ends_with(b".dex")
311}
312
313/// Where a root DEX entry falls in the runtime's multidex order:
314/// `classes.dex` is 1, `classesN.dex` is N, anything else comes last. `name`
315/// may carry a program's archive prefix.
316fn multidex_rank(name: &str) -> u32 {
317    let file = name.rsplit('/').next().unwrap_or(name);
318    match file.strip_prefix("classes").and_then(|rest| rest.strip_suffix(".dex")) {
319        Some("") => 1,
320        Some(n) if n.bytes().all(|b| b.is_ascii_digit()) && !n.starts_with('0') => n.parse().unwrap_or(u32::MAX),
321        _ => u32::MAX,
322    }
323}
324
325/// The one entry of a bare DEX file, by its name inside the archive.
326const BARE_DEX_ENTRY: &str = "classes.dex";
327/// Size of the DEX header, which carries the file's checksum and SHA-1.
328const DEX_HEADER_SIZE: usize = 0x70;
329
330impl Archive {
331    fn open(path: PathBuf, label: String) -> Result<Archive> {
332        let file = File::open(&path).map_err(|source| ApkError::Io { path: path.clone(), source })?;
333        // SAFETY: the mapping is read-only and the file is not expected to be
334        // modified while it is mapped (same contract as the Python mmap use).
335        let map = unsafe { Mmap::map(&file) }.map_err(|source| ApkError::Io { path: path.clone(), source })?;
336        let bare_dex = map.starts_with(b"dex\n");
337        Ok(Archive { path, map: Data::Mapped(map), label, bare_dex })
338    }
339
340    /// `inner` (a name inside this archive) as the program names it:
341    /// `label/inner`, so the archive is all before the last `/`.
342    fn entry_name(&self, inner: &[u8]) -> String {
343        let inner = String::from_utf8_lossy(inner);
344        if self.label.is_empty() { inner.into_owned() } else { format!("{}/{inner}", self.label) }
345    }
346
347    /// The name inside this archive of the program's entry `name`, if it
348    /// names one of this archive's entries.
349    fn inner_name<'n>(&self, name: &'n str) -> Option<&'n str> {
350        if self.label.is_empty() {
351            return None;
352        }
353        name.strip_prefix(self.label.as_str())?.strip_prefix('/')
354    }
355
356    /// Locate the central directory as `(offset, size)`, honouring zip64.
357    fn central_directory(&self) -> Result<(usize, usize)> {
358        let data = &self.map[..];
359        let search_start = data.len().saturating_sub(65536 + 22);
360        let eocd = memchr::memmem::rfind(&data[search_start..], EOCD_SIG).map(|p| p + search_start).ok_or(ApkError::NoEocd)?;
361        let mut cd_size = u32_at(data, eocd + 12).ok_or(ApkError::NoEocd)? as u64;
362        let mut cd_off = u32_at(data, eocd + 16).ok_or(ApkError::NoEocd)? as u64;
363        if cd_size == 0xFFFF_FFFF || cd_off == 0xFFFF_FFFF {
364            // zip64: locator sits right before the EOCD.
365            if eocd >= 20 && &data[eocd - 20..eocd - 16] == ZIP64_LOCATOR_SIG {
366                let rec = u64_at(data, eocd - 20 + 8).and_then(offset).ok_or(ApkError::NoEocd)?;
367                if bytes_at(data, rec, 4) == Some(ZIP64_EOCD_SIG) {
368                    cd_size = rec.checked_add(40).and_then(|o| u64_at(data, o)).ok_or(ApkError::NoEocd)?;
369                    cd_off = rec.checked_add(48).and_then(|o| u64_at(data, o)).ok_or(ApkError::NoEocd)?;
370                }
371            }
372        }
373        let (Some(off), Some(size)) = (offset(cd_off), offset(cd_size)) else {
374            return Err(ApkError::BadCentralDirectory);
375        };
376        if off.checked_add(size).map(|e| e > data.len()).unwrap_or(true) {
377            return Err(ApkError::BadCentralDirectory);
378        }
379        Ok((off, size))
380    }
381
382    /// What identifies this version of the archive: its central directory,
383    /// or a bare DEX file's header.
384    fn directory(&self) -> Result<&[u8]> {
385        if self.bare_dex {
386            return Ok(&self.map[..self.map.len().min(DEX_HEADER_SIZE)]);
387        }
388        let (off, size) = self.central_directory()?;
389        Ok(&self.map[off..off + size])
390    }
391
392    /// Walk the central directory, `keep` filtering by the name inside this
393    /// archive; the entries are of archive `index` of the program.
394    fn entries_filtered(&self, index: usize, keep: &mut impl FnMut(&[u8]) -> bool) -> Result<Vec<Entry>> {
395        if self.bare_dex {
396            if !keep(BARE_DEX_ENTRY.as_bytes()) {
397                return Ok(Vec::new());
398            }
399            let len = self.map.len() as u64;
400            return Ok(vec![Entry {
401                name: self.entry_name(BARE_DEX_ENTRY.as_bytes()),
402                method: METHOD_STORED,
403                comp_size: len,
404                uncomp_size: len,
405                local_header_off: 0,
406                crc32: 0,
407                archive: index,
408            }]);
409        }
410        let data = &self.map[..];
411        let (cd_off, cd_size) = self.central_directory()?;
412        let cd_end = cd_off + cd_size;
413        let mut entries = Vec::new();
414        let mut p = cd_off;
415        while p + 46 <= cd_end {
416            if &data[p..p + 4] != CD_SIG {
417                break;
418            }
419            let name_len = u16_at(data, p + 28).unwrap_or(0) as usize;
420            let extra_len = u16_at(data, p + 30).unwrap_or(0) as usize;
421            let comment_len = u16_at(data, p + 32).unwrap_or(0) as usize;
422            let name_start = p + 46;
423            let name_end = name_start + name_len;
424            if name_end > cd_end {
425                break;
426            }
427            let name = &data[name_start..name_end];
428            if keep(name) {
429                let mut comp_size = u32_at(data, p + 20).unwrap_or(0) as u64;
430                let mut uncomp_size = u32_at(data, p + 24).unwrap_or(0) as u64;
431                let mut local_header_off = u32_at(data, p + 42).unwrap_or(0) as u64;
432                if comp_size == 0xFFFF_FFFF || uncomp_size == 0xFFFF_FFFF || local_header_off == 0xFFFF_FFFF {
433                    // zip64 extra field (id 0x0001).
434                    let mut q = name_end;
435                    let extra_end = name_end + extra_len;
436                    while q + 4 <= extra_end {
437                        let id = u16_at(data, q).unwrap_or(0);
438                        let len = u16_at(data, q + 2).unwrap_or(0) as usize;
439                        if id == 0x0001 {
440                            let mut r = q + 4;
441                            if uncomp_size == 0xFFFF_FFFF {
442                                uncomp_size = u64_at(data, r).unwrap_or(0);
443                                r += 8;
444                            }
445                            if comp_size == 0xFFFF_FFFF {
446                                comp_size = u64_at(data, r).unwrap_or(0);
447                                r += 8;
448                            }
449                            if local_header_off == 0xFFFF_FFFF {
450                                local_header_off = u64_at(data, r).unwrap_or(0);
451                            }
452                            break;
453                        }
454                        q += 4 + len;
455                    }
456                }
457                entries.push(Entry {
458                    name: self.entry_name(name),
459                    method: u16_at(data, p + 10).unwrap_or(0),
460                    comp_size,
461                    uncomp_size,
462                    local_header_off,
463                    crc32: u32_at(data, p + 16).unwrap_or(0),
464                    archive: index,
465                });
466            }
467            p = name_end + extra_len + comment_len;
468        }
469        Ok(entries)
470    }
471
472    /// Root-level DEX entries in central-directory order. Duplicate names
473    /// keep the last occurrence, like [`Apk::find_entry`] (and `zipfile`).
474    fn dex_entries_in_order(&self, index: usize) -> Result<Vec<Entry>> {
475        self.dex_entries_matching(index, is_root_dex)
476    }
477
478    /// The entries `is_dex` accepts by name, in central-directory order,
479    /// duplicate names keeping the last occurrence.
480    fn dex_entries_matching(&self, index: usize, mut is_dex: fn(&[u8]) -> bool) -> Result<Vec<Entry>> {
481        let mut entries = self.entries_filtered(index, &mut is_dex)?;
482        // Last occurrence wins: walk from the back keeping first-seen names.
483        entries.reverse();
484        let mut seen = std::collections::HashSet::with_capacity(entries.len());
485        entries.retain(|e| seen.insert(e.name.clone()));
486        entries.reverse();
487        Ok(entries)
488    }
489}
490
491impl Apk {
492    /// Open `path` read-only: an APK (any zip, e.g. a jar), a bare DEX file,
493    /// or a program of several archives read as one:
494    ///
495    /// - a directory: every `*.apk`, `*.jar`, `*.zip` and `*.dex` below it;
496    /// - several paths (files or directories) joined with `,`, when no file
497    ///   of that whole name exists.
498    ///
499    /// A program's entries are named after their archive
500    /// (`services.jar/classes2.dex`, `x.dex/classes.dex` for a bare DEX
501    /// file; see [`archive_of`]). Archives without root DEX entries, or
502    /// that do not open, are left out and named in [`Apk::notes`]; a program
503    /// none of whose archives has code is an error. A program that comes
504    /// down to one archive is that archive alone.
505    ///
506    /// # Errors
507    ///
508    /// [`ApkError::Io`] when a path cannot be read or a lone archive cannot
509    /// be mapped, [`ApkError::NoCode`] for a directory without archives or a
510    /// program none of whose archives has code.
511    pub fn open(path: impl AsRef<Path>) -> Result<Apk> {
512        let path = path.as_ref().to_path_buf();
513        let sources = program::sources(&path)?;
514        let version = Version::of_sources(&sources);
515        if let [lone] = &sources[..] {
516            let archives = Arc::new([Archive::open(lone.path.clone(), String::new())?]);
517            return Ok(Apk { path, archives, limits: Limits::default(), notes: Arc::new([]), version });
518        }
519        let mut archives = Vec::with_capacity(sources.len());
520        let mut notes = Vec::new();
521        for s in sources {
522            let archive = match Archive::open(s.path, s.label) {
523                Ok(a) => a,
524                Err(e) => {
525                    notes.push(format!("skipped: {e}"));
526                    continue;
527                }
528            };
529            match archive.dex_entries_in_order(archives.len()) {
530                Ok(dex) if dex.is_empty() => notes.push(format!("{}: no classes*.dex (odex/vdex only?): deodex first", archive.label)),
531                Ok(_) => archives.push(archive),
532                Err(e) => notes.push(format!("{}: skipped: {e}", archive.label)),
533            }
534        }
535        if archives.is_empty() {
536            return Err(ApkError::NoCode { path, notes });
537        }
538        if let [lone] = &mut archives[..] {
539            lone.label.clear();
540        }
541        Ok(Apk { path, archives: archives.into(), limits: Limits::default(), notes: notes.into(), version })
542    }
543
544    /// Read `bytes` as a lone archive (or bare DEX file) already in memory,
545    /// as [`Apk::open`] reads a file; `path` only names it. Its
546    /// [`Apk::version`] lists no file.
547    pub fn from_bytes(path: impl Into<PathBuf>, bytes: Vec<u8>) -> Apk {
548        let path = path.into();
549        let bare_dex = bytes.starts_with(b"dex\n");
550        let archive = Archive { path: path.clone(), map: Data::Owned(bytes), label: String::new(), bare_dex };
551        Apk { path, archives: Arc::new([archive]), limits: Limits::default(), notes: Arc::new([]), version: Version::in_memory() }
552    }
553
554    /// The files `path` (as [`Apk::open`] takes it) names now, with their
555    /// sizes and modification times: equal to an open [`Apk::version`]
556    /// when opening again would read the same files. Costs a directory walk
557    /// and a `stat` per archive, not a mapping.
558    ///
559    /// # Errors
560    ///
561    /// [`ApkError::Io`] when a path cannot be read, [`ApkError::NoCode`] for
562    /// a directory without archives.
563    pub fn version_of(path: impl AsRef<Path>) -> Result<Version> {
564        Ok(Version::of_sources(&program::sources(path.as_ref())?))
565    }
566
567    /// The files read and their sizes and times when this was opened.
568    pub fn version(&self) -> &Version {
569        &self.version
570    }
571
572    /// `spec` (a path [`Apk::open`] takes) with its relative paths resolved
573    /// against `base`: each of several paths joined with `,` on its own.
574    pub fn resolve(spec: &Path, base: &Path) -> PathBuf {
575        program::resolve(spec, base)
576    }
577
578    /// Enforce `limits` on every later read.
579    pub fn with_limits(mut self, limits: Limits) -> Apk {
580        self.limits = limits;
581        self
582    }
583
584    /// The path the APK was opened from (a program's directory or list).
585    pub fn path(&self) -> &Path {
586        &self.path
587    }
588
589    /// The limits every read enforces ([`Apk::with_limits`]).
590    pub fn limits(&self) -> Limits {
591        self.limits
592    }
593
594    /// How many archives make up the program: 1 for a lone archive, whose
595    /// entry names are the names inside it.
596    pub fn archive_count(&self) -> usize {
597        self.archives.len()
598    }
599
600    /// One line per archive of a program left out of its code.
601    pub fn notes(&self) -> &[String] {
602        &self.notes
603    }
604
605    /// The file `entry` lives in.
606    pub fn archive_path(&self, entry: &Entry) -> &Path {
607        &self.archive(entry).path
608    }
609
610    /// The raw bytes of the file `entry` lives in.
611    pub fn archive_data(&self, entry: &Entry) -> &[u8] {
612        &self.archive(entry).map
613    }
614
615    fn archive(&self, entry: &Entry) -> &Archive {
616        self.archives.get(entry.archive).expect("entry of this APK")
617    }
618
619    /// What identifies each archive's version, in program order. A cache key
620    /// hashes them.
621    ///
622    /// # Errors
623    ///
624    /// [`ApkError::NoEocd`] or [`ApkError::BadCentralDirectory`] when an
625    /// archive's central directory cannot be located.
626    pub fn directories(&self) -> Result<Vec<ArchiveId<'_>>> {
627        self.archives.iter().map(|a| Ok(ArchiveId { path: &a.path, label: &a.label, directory: a.directory()? })).collect()
628    }
629
630    /// A short name for the APK: the file stem, or for a program the first
631    /// archive's and how many others follow.
632    pub fn stem(&self) -> String {
633        let first = self.archives[0].path.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default();
634        match self.archives.len() {
635            1 => first,
636            n => format!("{first}-and-{}", n - 1),
637        }
638    }
639
640    /// `(size, modification time)` of the file; for a program, its sizes and
641    /// times hashed into one number and the latest time, so rewriting any
642    /// archive changes it.
643    ///
644    /// # Errors
645    ///
646    /// [`ApkError::Io`] when a file's metadata cannot be read.
647    pub fn stamp(&self) -> Result<(u64, Option<SystemTime>)> {
648        let meta = |a: &Archive| std::fs::metadata(&a.path).map_err(|source| ApkError::Io { path: a.path.clone(), source });
649        if let [lone] = &self.archives[..] {
650            let m = meta(lone)?;
651            return Ok((m.len(), m.modified().ok()));
652        }
653        let mut size = 0u64;
654        let mut latest = None;
655        for a in self.archives.iter() {
656            let m = meta(a)?;
657            let since = m.modified().ok().and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok());
658            // Only a hash input: the low bits of the nanoseconds are what differ.
659            let nanos = since.map_or(0, |d| d.as_nanos() as u64);
660            size = size.rotate_left(13) ^ m.len().wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ nanos;
661            latest = latest.max(m.modified().ok());
662        }
663        Ok((size, latest))
664    }
665
666    /// Walk the central directories. `keep` filters entries by their name
667    /// inside their archive early, so the full name list is never
668    /// materialised for large archives.
669    ///
670    /// # Errors
671    ///
672    /// As [`Apk::directories`].
673    pub fn entries_filtered(&self, mut keep: impl FnMut(&[u8]) -> bool) -> Result<Vec<Entry>> {
674        let mut entries = Vec::new();
675        for (i, a) in self.archives.iter().enumerate() {
676            entries.extend(a.entries_filtered(i, &mut keep)?);
677        }
678        Ok(entries)
679    }
680
681    /// Every entry of the archive.
682    ///
683    /// # Errors
684    ///
685    /// As [`Apk::directories`].
686    pub fn entries(&self) -> Result<Vec<Entry>> {
687        self.entries_filtered(|_| true)
688    }
689
690    /// Root-level DEX entries sorted by compressed size (smallest first, so a
691    /// parallel scan finishes the cheap ones early). Duplicate names keep the
692    /// last occurrence, like [`Apk::find_entry`] (and `zipfile`).
693    ///
694    /// # Errors
695    ///
696    /// As [`Apk::directories`], and [`ApkError::LimitExceeded`] when the
697    /// entries together declare more than [`Limits::max_dex_total_bytes`].
698    pub fn dex_entries(&self) -> Result<Vec<Entry>> {
699        let mut entries = self.dex_entries_in_order()?;
700        self.check_dex_total(&entries)?;
701        entries.sort_by_key(|e| e.comp_size);
702        Ok(entries)
703    }
704
705    /// The entries of [`Apk::dex_entries`] in central-directory order, archive
706    /// by archive. Unlike [`Apk::dex_entries`], the total size is not checked.
707    ///
708    /// # Errors
709    ///
710    /// As [`Apk::directories`].
711    pub fn dex_entries_in_order(&self) -> Result<Vec<Entry>> {
712        let mut entries = Vec::new();
713        for (i, a) in self.archives.iter().enumerate() {
714            entries.extend(a.dex_entries_in_order(i)?);
715        }
716        Ok(entries)
717    }
718
719    /// The entries of [`Apk::dex_entries`] in the order the runtime loads
720    /// them, archive by archive: `classes.dex`, `classes2.dex`,
721    /// `classes3.dex`, ... by number, then any other root `*.dex` in
722    /// central-directory order. Duplicate names keep the last occurrence.
723    ///
724    /// # Errors
725    ///
726    /// As [`Apk::dex_entries`].
727    pub fn dex_entries_multidex(&self) -> Result<Vec<Entry>> {
728        let mut entries = self.dex_entries_in_order()?;
729        self.check_dex_total(&entries)?;
730        // Stable: entries outside the classesN.dex scheme keep their order.
731        entries.sort_by_key(|e| (e.archive, multidex_rank(&e.name)));
732        Ok(entries)
733    }
734
735    /// Every `*.dex` entry at any depth (`assets/payload.dex`,
736    /// `lib/x/classes.dex` as well as the root ones), in central-directory
737    /// order, archive by archive. Duplicate names keep the last occurrence.
738    /// The runtime loads only the root entries ([`Apk::dex_entries`]); this
739    /// is for callers that look at embedded code too. The total size is not
740    /// checked. A program of several archives has already left out those
741    /// without root DEX entries ([`Apk::open`]).
742    ///
743    /// # Errors
744    ///
745    /// As [`Apk::directories`].
746    pub fn all_dex_entries(&self) -> Result<Vec<Entry>> {
747        let mut entries = Vec::new();
748        for (i, a) in self.archives.iter().enumerate() {
749            entries.extend(a.dex_entries_matching(i, is_any_dex)?);
750        }
751        Ok(entries)
752    }
753
754    fn check_dex_total(&self, entries: &[Entry]) -> Result<()> {
755        let total = entries.iter().fold(0u64, |t, e| t.saturating_add(e.uncomp_size));
756        if total > self.limits.max_dex_total_bytes {
757            return Err(ApkError::LimitExceeded {
758                name: "classes*.dex".into(),
759                reason: format!("DEX entries declare {total} bytes in all, limit {}", self.limits.max_dex_total_bytes),
760            });
761        }
762        Ok(())
763    }
764
765    /// Find an entry by exact name. In a program, a name without an
766    /// archive's prefix (`AndroidManifest.xml`) is looked up archive by
767    /// archive, the first that has it winning.
768    ///
769    /// # Errors
770    ///
771    /// As [`Apk::directories`].
772    pub fn find_entry(&self, name: &str) -> Result<Option<Entry>> {
773        let owner = self.archives.iter().enumerate().find_map(|(i, a)| Some((i, a.inner_name(name)?)));
774        let candidates: Vec<(usize, &str)> = match owner {
775            Some(found) => vec![found],
776            None => (0..self.archives.len()).map(|i| (i, name)).collect(),
777        };
778        for (i, inner) in candidates {
779            let mut found = self.archives[i].entries_filtered(i, &mut |n: &[u8]| n == inner.as_bytes())?;
780            if let Some(e) = found.pop() {
781                return Ok(Some(e));
782            }
783        }
784        Ok(None)
785    }
786
787    /// Compressed payload of `entry` inside the mapping.
788    ///
789    /// # Errors
790    ///
791    /// [`ApkError::BadLocalHeader`] when the local header is missing or the
792    /// payload runs past the end of the archive.
793    pub fn raw_payload(&self, entry: &Entry) -> Result<&[u8]> {
794        let range = self.payload_range(entry)?;
795        Ok(&self.archive(entry).map[range])
796    }
797
798    /// Where [`Apk::raw_payload`] of `entry` lies in its archive's bytes.
799    fn payload_range(&self, entry: &Entry) -> Result<std::ops::Range<usize>> {
800        let archive = self.archive(entry);
801        let data = &archive.map[..];
802        let bad = || ApkError::BadLocalHeader(entry.name.clone());
803        if archive.bare_dex {
804            return offset(entry.comp_size).filter(|&n| n <= data.len()).map(|n| 0..n).ok_or_else(bad);
805        }
806        let lh = offset(entry.local_header_off).ok_or_else(bad)?;
807        if bytes_at(data, lh, 4) != Some(LH_SIG) {
808            return Err(bad());
809        }
810        // The signature check bounds `lh`, so these small additions cannot overflow.
811        let name_len = u16_at(data, lh + 26).ok_or_else(bad)? as usize;
812        let extra_len = u16_at(data, lh + 28).ok_or_else(bad)? as usize;
813        let start = lh + 30 + name_len + extra_len;
814        let end = offset(entry.comp_size).and_then(|n| start.checked_add(n));
815        end.filter(|&end| start <= end && end <= data.len()).map(|end| start..end).ok_or_else(bad)
816    }
817
818    /// The raw bytes of a lone archive's file (a zip, or a file that is not
819    /// one, which a caller may know how to read); `None` for a program of
820    /// several archives.
821    pub fn lone_data(&self) -> Option<&[u8]> {
822        match &self.archives[..] {
823            [lone] => Some(&lone.map),
824            _ => None,
825        }
826    }
827
828    /// Bytes of the archives this holds on the heap: the inflated ones of
829    /// [`Apk::open_nested`]. Clones share them.
830    pub fn owned_bytes(&self) -> usize {
831        self.archives.iter().map(|archive| if let Data::Owned(bytes) = &archive.map { bytes.capacity() } else { 0 }).sum()
832    }
833
834    /// Open `entry`, an archive inside this one (a `.hap` of an `.app`, a jar
835    /// of a zip), as an archive of its own with this one's limits. A stored
836    /// entry is read in place; a Deflate entry is inflated into memory and
837    /// kept there for as long as the returned [`Apk`] lives. Its entry names
838    /// are the names inside it.
839    ///
840    /// # Errors
841    ///
842    /// As [`Apk::read_entry`].
843    pub fn open_nested(&self, entry: &Entry) -> Result<Apk> {
844        let EntryBytes(map) = self.read_entry_owned(entry)?;
845        let bare_dex = map.starts_with(b"dex\n");
846        let nested = Archive { path: self.archive(entry).path.clone(), map, label: String::new(), bare_dex };
847        Ok(Apk {
848            path: self.path.clone(),
849            archives: Arc::new([nested]),
850            limits: self.limits,
851            notes: Arc::new([]),
852            version: self.version.clone(),
853        })
854    }
855
856    /// Where the bytes of `entry` lie in [`Apk::archive_data`] when it is
857    /// stored: `archive_data(entry)[range]` is what [`Apk::read_entry`]
858    /// borrows. `None` for a compressed entry, which has to be inflated.
859    /// With the range, a caller holding a clone of this [`Apk`] can keep a
860    /// zero-copy view of the entry for as long as it likes (or see
861    /// [`Apk::read_entry_owned`]).
862    ///
863    /// # Errors
864    ///
865    /// As [`Apk::read_entry`] for a stored entry: [`ApkError::BadLocalHeader`],
866    /// [`ApkError::LimitExceeded`] and [`ApkError::SizeMismatch`].
867    pub fn entry_range(&self, entry: &Entry) -> Result<Option<std::ops::Range<usize>>> {
868        if entry.method != METHOD_STORED {
869            return Ok(None);
870        }
871        let range = self.payload_range(entry)?;
872        self.check_declared_size(entry)?;
873        self.check_stored_size(entry, range.len())?;
874        Ok(Some(range))
875    }
876
877    /// Extract `entry` into a handle that keeps its bytes alive on its own:
878    /// a stored entry stays in the mapping (the handle shares it, like a
879    /// clone of this [`Apk`]), a Deflate entry is inflated into the handle.
880    ///
881    /// # Errors
882    ///
883    /// As [`Apk::read_entry`].
884    pub fn read_entry_owned(&self, entry: &Entry) -> Result<EntryBytes> {
885        let data = match self.entry_range(entry)? {
886            Some(range) => Data::Window { outer: Arc::clone(&self.archives), archive: entry.archive, range },
887            None => Data::Owned(self.read_entry(entry)?.into_owned()),
888        };
889        Ok(EntryBytes(data))
890    }
891
892    /// Extract `entry`: stored entries are borrowed straight from the mapping,
893    /// Deflate entries are inflated into a fresh buffer.
894    ///
895    /// # Errors
896    ///
897    /// As [`Apk::read_entry_cancellable`], except [`ApkError::Cancelled`].
898    pub fn read_entry(&self, entry: &Entry) -> Result<Cow<'_, [u8]>> {
899        self.read_entry_cancellable(entry, None)
900    }
901
902    /// Extract `entry`, polling `cancel` between input chunks. Returns
903    /// `Err(ApkError::Cancelled)` when the flag is raised mid-way.
904    ///
905    /// # Errors
906    ///
907    /// [`ApkError::Cancelled`]; [`ApkError::BadLocalHeader`] when the payload
908    /// cannot be located; [`ApkError::ImplausibleSize`] or
909    /// [`ApkError::LimitExceeded`] when the declared or inflated size is out of
910    /// bounds; [`ApkError::Inflate`] for a corrupt stream;
911    /// [`ApkError::UnsupportedMethod`]; [`ApkError::SizeMismatch`] when the
912    /// data is not the declared size.
913    pub fn read_entry_cancellable(&self, entry: &Entry, cancel: Option<&AtomicBool>) -> Result<Cow<'_, [u8]>> {
914        if cancel.is_some_and(|c| c.load(Ordering::Relaxed)) {
915            return Err(ApkError::Cancelled);
916        }
917        let payload = self.raw_payload(entry)?;
918        let bound = self.check_declared_size(entry)?;
919        let out = match entry.method {
920            METHOD_STORED => {
921                self.check_stored_size(entry, payload.len())?;
922                return Ok(Cow::Borrowed(payload));
923            }
924            #[cfg(feature = "libdeflate")]
925            METHOD_DEFLATE if entry.uncomp_size > 0 => {
926                if let Some(c) = cancel
927                    && c.load(Ordering::Relaxed)
928                {
929                    return Err(ApkError::Cancelled);
930                }
931                let mut out = vec![0u8; entry.uncomp_size as usize];
932                let n = libdeflater::Decompressor::new()
933                    .deflate_decompress(payload, &mut out)
934                    .map_err(|e| ApkError::Inflate { name: entry.name.clone(), source: std::io::Error::other(format!("{e:?}")) })?;
935                out.truncate(n);
936                out
937            }
938            METHOD_DEFLATE => {
939                let reader = CancelReader { data: payload, pos: 0, cancel };
940                let decoder = flate2::read::DeflateDecoder::new(reader);
941                let reserve = (entry.uncomp_size as usize).min(INITIAL_CAPACITY.max(payload.len().saturating_mul(TYPICAL_RATIO)));
942                let mut out = Vec::with_capacity(reserve);
943                // Stop one byte past the bound so an over-long stream is detected
944                // instead of inflated without limit (an entry that declares no
945                // uncompressed size has no other cap).
946                match decoder.take(bound.saturating_add(1)).read_to_end(&mut out) {
947                    Ok(_) => {}
948                    Err(_) if cancel.is_some_and(|c| c.load(Ordering::Relaxed)) => return Err(ApkError::Cancelled),
949                    Err(e) => return Err(ApkError::Inflate { name: entry.name.clone(), source: e }),
950                }
951                if out.len() as u64 > bound {
952                    return Err(self.past_bound(entry, bound));
953                }
954                out
955            }
956            m => return Err(ApkError::UnsupportedMethod(m)),
957        };
958        if let Some(c) = cancel
959            && c.load(Ordering::Relaxed)
960        {
961            return Err(ApkError::Cancelled);
962        }
963        if entry.uncomp_size != 0 && out.len() as u64 != entry.uncomp_size {
964            return Err(ApkError::SizeMismatch { name: entry.name.clone(), expected: entry.uncomp_size as usize, actual: out.len() });
965        }
966        Ok(Cow::Owned(out))
967    }
968
969    /// A stored entry's payload must be as long as it declares (when it
970    /// declares a size at all).
971    fn check_stored_size(&self, entry: &Entry, len: usize) -> Result<()> {
972        if entry.uncomp_size != 0 && len as u64 != entry.uncomp_size {
973            return Err(ApkError::SizeMismatch { name: entry.name.clone(), expected: entry.uncomp_size as usize, actual: len });
974        }
975        Ok(())
976    }
977
978    /// Reject Deflate entries whose declared uncompressed size cannot come out
979    /// of their compressed payload, before anything is allocated for them.
980    /// Returns the most an inflated entry may legitimately grow to; it also
981    /// bounds entries that declare no uncompressed size at all.
982    fn check_declared_size(&self, entry: &Entry) -> Result<u64> {
983        let bound = entry.comp_size.saturating_mul(MAX_DEFLATE_RATIO).saturating_add(1024);
984        if entry.method == METHOD_DEFLATE && entry.uncomp_size > bound {
985            return Err(self.implausible(entry));
986        }
987        let Limits { max_entry_bytes, max_ratio, .. } = self.limits;
988        if entry.uncomp_size > max_entry_bytes {
989            return Err(self.over_limit(entry, format!("declares {} bytes, limit {max_entry_bytes}", entry.uncomp_size)));
990        }
991        let ratio_bound = entry.comp_size.saturating_mul(max_ratio).saturating_add(1024);
992        if entry.method == METHOD_DEFLATE && entry.uncomp_size > ratio_bound {
993            return Err(self.over_limit(
994                entry,
995                format!("declares {} bytes from {} compressed, ratio limit {max_ratio}", entry.uncomp_size, entry.comp_size),
996            ));
997        }
998        Ok(bound.min(ratio_bound).min(max_entry_bytes))
999    }
1000
1001    /// An entry whose stream ran past `bound` (from [`Apk::check_declared_size`]).
1002    fn past_bound(&self, entry: &Entry, bound: u64) -> ApkError {
1003        if bound < entry.comp_size.saturating_mul(MAX_DEFLATE_RATIO).saturating_add(1024) {
1004            self.over_limit(entry, format!("inflates past {bound} bytes"))
1005        } else {
1006            self.implausible(entry)
1007        }
1008    }
1009
1010    fn over_limit(&self, entry: &Entry, reason: String) -> ApkError {
1011        ApkError::LimitExceeded { name: entry.name.clone(), reason }
1012    }
1013
1014    fn implausible(&self, entry: &Entry) -> ApkError {
1015        ApkError::ImplausibleSize { name: entry.name.clone(), comp_size: entry.comp_size, uncomp_size: entry.uncomp_size }
1016    }
1017
1018    /// Convenience: extract a named entry.
1019    ///
1020    /// # Errors
1021    ///
1022    /// [`ApkError::NotFound`] when there is no such entry, otherwise as
1023    /// [`Apk::find_entry`] and [`Apk::read_entry`].
1024    pub fn read(&self, name: &str) -> Result<Vec<u8>> {
1025        let entry = self.find_entry(name)?.ok_or_else(|| ApkError::NotFound(name.to_string()))?;
1026        Ok(self.read_entry(&entry)?.into_owned())
1027    }
1028}
1029
1030/// The bytes of one entry, kept alive on their own ([`Apk::read_entry_owned`]):
1031/// a window of the shared mapping for a stored entry, a buffer for an
1032/// inflated one. Dereferences to the bytes.
1033pub struct EntryBytes(Data);
1034
1035impl EntryBytes {
1036    /// Whether the bytes are read in place from an archive rather than held
1037    /// in a buffer of their own.
1038    pub fn is_mapped(&self) -> bool {
1039        matches!(self.0, Data::Window { .. })
1040    }
1041}
1042
1043impl std::ops::Deref for EntryBytes {
1044    type Target = [u8];
1045
1046    fn deref(&self) -> &[u8] {
1047        &self.0
1048    }
1049}
1050
1051impl AsRef<[u8]> for EntryBytes {
1052    fn as_ref(&self) -> &[u8] {
1053        &self.0
1054    }
1055}
1056
1057impl std::fmt::Debug for EntryBytes {
1058    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1059        f.debug_struct("EntryBytes").field("len", &self.len()).field("mapped", &self.is_mapped()).finish()
1060    }
1061}
1062
1063/// Decision returned by the callback of [`Apk::inflate_until`].
1064#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1065pub enum PrefixStep {
1066    /// Keep inflating; stop once at least this many bytes are available
1067    /// (`0` = undecided, ask again after the next chunk).
1068    Continue(usize),
1069    /// A prefix is not useful for this entry; stop now.
1070    Abort,
1071}
1072
1073/// Outcome of [`Apk::inflate_until`].
1074#[derive(Debug, Clone, PartialEq, Eq)]
1075pub enum Prefix {
1076    /// The whole entry (the stream ended before or at the requested length).
1077    Complete(Vec<u8>),
1078    /// At least the requested number of leading bytes.
1079    Partial(Vec<u8>),
1080    /// The callback aborted; holds whatever was inflated so far.
1081    Aborted(Vec<u8>),
1082}
1083
1084impl Apk {
1085    /// Inflate a Deflate entry incrementally, letting `decide` inspect the
1086    /// leading bytes after every chunk to say how much of the entry is
1087    /// actually needed. Stored entries are returned whole.
1088    ///
1089    /// # Errors
1090    ///
1091    /// As [`Apk::read_entry`]: a payload that cannot be located, a size out
1092    /// of bounds, or a corrupt stream.
1093    pub fn inflate_until(&self, entry: &Entry, mut decide: impl FnMut(&[u8]) -> PrefixStep) -> Result<Prefix> {
1094        // Small first chunks so a quick "not useful" costs little; then up to CHUNK.
1095        const FIRST_CHUNK: usize = 1 << 14;
1096        const CHUNK: usize = 1 << 18;
1097        let mut chunk = FIRST_CHUNK;
1098        if entry.method != METHOD_DEFLATE {
1099            return Ok(Prefix::Complete(self.read_entry(entry)?.into_owned()));
1100        }
1101        let compressed = self.raw_payload(entry)?;
1102        let bound = self.check_declared_size(entry)?;
1103        let mut d = flate2::Decompress::new(false);
1104        let mut out: Vec<u8> = Vec::with_capacity((entry.uncomp_size as usize).min(4 * CHUNK));
1105        let mut target: Option<usize> = None;
1106        loop {
1107            if out.len() as u64 > bound {
1108                return Err(self.past_bound(entry, bound));
1109            }
1110            let base = out.len();
1111            out.resize(base + chunk, 0);
1112            chunk = (chunk * 2).min(CHUNK);
1113            let consumed = d.total_in() as usize;
1114            let status = d
1115                .decompress(&compressed[consumed.min(compressed.len())..], &mut out[base..], flate2::FlushDecompress::None)
1116                .map_err(|e| ApkError::Inflate { name: entry.name.clone(), source: std::io::Error::other(e) })?;
1117            let written = d.total_out() as usize - base;
1118            out.truncate(base + written);
1119            if target.is_none() {
1120                match decide(&out) {
1121                    PrefixStep::Continue(0) => {}
1122                    PrefixStep::Continue(limit) => {
1123                        // An entry that declares no size cannot cap the request.
1124                        target = Some(if entry.uncomp_size == 0 { limit } else { limit.min(entry.uncomp_size as usize) })
1125                    }
1126                    PrefixStep::Abort => return Ok(Prefix::Aborted(out)),
1127                }
1128            }
1129            if status == flate2::Status::StreamEnd {
1130                return Ok(Prefix::Complete(out));
1131            }
1132            if let Some(limit) = target
1133                && out.len() >= limit
1134            {
1135                return Ok(Prefix::Partial(out));
1136            }
1137            if written == 0 && d.total_in() as usize == consumed {
1138                return Err(ApkError::Inflate { name: entry.name.clone(), source: std::io::Error::other("deflate made no progress") });
1139            }
1140        }
1141    }
1142}
1143
1144/// Slice reader that serves bounded chunks and honours a cancellation flag.
1145struct CancelReader<'a> {
1146    data: &'a [u8],
1147    pos: usize,
1148    cancel: Option<&'a AtomicBool>,
1149}
1150
1151impl Read for CancelReader<'_> {
1152    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1153        if let Some(c) = self.cancel
1154            && c.load(Ordering::Relaxed)
1155        {
1156            return Err(std::io::Error::other("cancelled"));
1157        }
1158        let remaining = &self.data[self.pos..];
1159        let n = remaining.len().min(buf.len()).min(DEFLATE_CHUNK);
1160        buf[..n].copy_from_slice(&remaining[..n]);
1161        self.pos += n;
1162        Ok(n)
1163    }
1164}
1165
1166#[cfg(test)]
1167mod tests {
1168    use super::*;
1169    use std::io::Write;
1170
1171    fn write_temp(bytes: &[u8]) -> tempfile::NamedTempFile {
1172        let mut f = tempfile::NamedTempFile::new().unwrap();
1173        f.write_all(bytes).unwrap();
1174        f.flush().unwrap();
1175        f
1176    }
1177
1178    #[test]
1179    fn limits_reject_bombs_before_inflating() {
1180        // Zeros compress far beyond any real DEX.
1181        let bomb = vec![0u8; 4 << 20];
1182        let f = write_temp(&asc_fixture::make_apk(&[("classes.dex", &bomb)], true));
1183        let apk = Apk::open(f.path()).unwrap();
1184        let entry = apk.dex_entries().unwrap().remove(0);
1185        assert_eq!(apk.read_entry(&entry).unwrap().len(), bomb.len());
1186        let apk = Apk::open(f.path()).unwrap().with_limits(Limits::guarded());
1187        let err = apk.read_entry(&entry).unwrap_err();
1188        assert!(matches!(&err, ApkError::LimitExceeded { reason, .. } if reason.contains("ratio limit 100")), "{err}");
1189        // An entry with no declared size is cut off by the same bound while streaming.
1190        let mut undeclared = entry.clone();
1191        undeclared.uncomp_size = 0;
1192        assert!(matches!(apk.read_entry(&undeclared), Err(ApkError::LimitExceeded { .. })));
1193        let small = Limits { max_entry_bytes: 1 << 20, ..Limits::default() };
1194        let apk = Apk::open(f.path()).unwrap().with_limits(small);
1195        assert!(matches!(apk.read_entry(&entry), Err(ApkError::LimitExceeded { .. })));
1196        let total = Limits { max_dex_total_bytes: 1 << 20, ..Limits::default() };
1197        let apk = Apk::open(f.path()).unwrap().with_limits(total);
1198        assert!(matches!(apk.dex_entries(), Err(ApkError::LimitExceeded { .. })));
1199    }
1200
1201    #[test]
1202    fn enumerates_and_inflates_dex_entries() {
1203        let dex = asc_fixture::make_dex();
1204        for deflate in [false, true] {
1205            let apk_bytes = asc_fixture::make_apk(
1206                &[("classes.dex", &dex), ("lib/x/classes.dex", &dex), ("classes2.dex", &dex), ("AndroidManifest.xml", b"xml")],
1207                deflate,
1208            );
1209            let f = write_temp(&apk_bytes);
1210            let apk = Apk::open(f.path()).unwrap();
1211            let entries = apk.dex_entries().unwrap();
1212            let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
1213            assert_eq!(names.len(), 2);
1214            assert!(names.contains(&"classes.dex") && names.contains(&"classes2.dex"));
1215            for e in &entries {
1216                assert_eq!(apk.read_entry(e).unwrap().as_ref(), dex.as_slice());
1217            }
1218            assert_eq!(apk.read("AndroidManifest.xml").unwrap(), b"xml");
1219            let cancel = AtomicBool::new(true);
1220            let r = apk.read_entry_cancellable(&entries[0], Some(&cancel));
1221            assert!(matches!(r, Err(ApkError::Cancelled)));
1222            if deflate {
1223                // A lying central directory must not drive a huge allocation.
1224                let mut lying = entries[0].clone();
1225                lying.uncomp_size = 1 << 40;
1226                assert!(matches!(apk.read_entry(&lying), Err(ApkError::ImplausibleSize { .. })));
1227                assert!(matches!(apk.inflate_until(&lying, |_| PrefixStep::Continue(0)), Err(ApkError::ImplausibleSize { .. })));
1228                let mut truncated = entries[0].clone();
1229                truncated.comp_size = u64::MAX;
1230                assert!(matches!(apk.read_entry(&truncated), Err(ApkError::BadLocalHeader(_))));
1231            }
1232        }
1233    }
1234
1235    #[test]
1236    fn entries_outlive_their_apk_in_place_or_inflated() {
1237        fn assert_send_sync<T: Send + Sync>() {}
1238        assert_send_sync::<EntryBytes>();
1239        let dex = asc_fixture::make_dex();
1240        for deflate in [false, true] {
1241            let f = write_temp(&asc_fixture::make_apk(&[("classes.dex", &dex), ("a.txt", b"text")], deflate));
1242            let apk = Apk::open(f.path()).unwrap();
1243            let entry = apk.find_entry("classes.dex").unwrap().unwrap();
1244            let range = apk.entry_range(&entry).unwrap();
1245            assert_eq!(range.is_some(), !deflate);
1246            if let Some(range) = &range {
1247                let Cow::Borrowed(read) = apk.read_entry(&entry).unwrap() else { panic!("a stored entry is borrowed") };
1248                assert_eq!(&apk.archive_data(&entry)[range.clone()], read);
1249                assert_eq!(apk.archive_data(&entry)[range.clone()].as_ptr(), read.as_ptr());
1250            }
1251            let owned = apk.read_entry_owned(&entry).unwrap();
1252            assert_eq!(owned.is_mapped(), !deflate);
1253            if let Some(range) = range {
1254                assert_eq!(owned.as_ptr(), apk.archive_data(&entry)[range].as_ptr());
1255            }
1256            drop(apk);
1257            assert_eq!(&*owned, dex.as_slice());
1258            assert_eq!(owned.as_ref(), dex.as_slice());
1259            assert!(format!("{owned:?}").contains(&format!("len: {}", dex.len())));
1260        }
1261        // A stored entry is checked like any read.
1262        let f = write_temp(&asc_fixture::make_apk(&[("classes.dex", &dex)], false));
1263        let apk = Apk::open(f.path()).unwrap();
1264        let mut lying = apk.find_entry("classes.dex").unwrap().unwrap();
1265        lying.uncomp_size += 1;
1266        assert!(matches!(apk.entry_range(&lying), Err(ApkError::SizeMismatch { .. })));
1267        assert!(matches!(apk.read_entry_owned(&lying), Err(ApkError::SizeMismatch { .. })));
1268        lying.local_header_off += 1;
1269        assert!(matches!(apk.entry_range(&lying), Err(ApkError::BadLocalHeader(_))));
1270        let small = apk.clone().with_limits(Limits::default().max_entry_bytes(16));
1271        let entry = small.find_entry("classes.dex").unwrap().unwrap();
1272        assert!(matches!(small.entry_range(&entry), Err(ApkError::LimitExceeded { .. })));
1273    }
1274
1275    #[test]
1276    fn all_dex_entries_reach_any_depth() {
1277        let dex = asc_fixture::make_dex();
1278        let apk_bytes = asc_fixture::make_apk(
1279            &[
1280                ("classes2.dex", &dex),
1281                ("assets/payload.dex", b"x"),
1282                ("classes.dex", &dex),
1283                ("lib/x/classes.dex", &dex),
1284                ("assets/notdex.dex.bak", b"y"),
1285                ("AndroidManifest.xml", b"xml"),
1286            ],
1287            true,
1288        );
1289        let f = write_temp(&apk_bytes);
1290        let apk = Apk::open(f.path()).unwrap();
1291        let names = |entries: Vec<Entry>| entries.into_iter().map(|e| e.name).collect::<Vec<_>>();
1292        assert_eq!(names(apk.all_dex_entries().unwrap()), ["classes2.dex", "assets/payload.dex", "classes.dex", "lib/x/classes.dex"]);
1293        // The root enumeration is unchanged.
1294        assert_eq!(names(apk.dex_entries_in_order().unwrap()), ["classes2.dex", "classes.dex"]);
1295        let payload = apk.all_dex_entries().unwrap().remove(1);
1296        assert_eq!(apk.read_entry(&payload).unwrap().as_ref(), b"x");
1297        // A bare DEX file is its one entry.
1298        let f = write_temp(&dex);
1299        assert_eq!(names(Apk::open(f.path()).unwrap().all_dex_entries().unwrap()), ["classes.dex"]);
1300    }
1301
1302    #[test]
1303    fn multidex_entries_follow_the_runtime_load_order() {
1304        let dex = asc_fixture::make_dex();
1305        let apk_bytes = asc_fixture::make_apk(
1306            &[
1307                ("classes10.dex", &dex),
1308                ("extra.dex", &dex),
1309                ("classes2.dex", &dex),
1310                ("classes.dex", &dex),
1311                ("classes02.dex", &dex),
1312                ("classes3.dex", &dex),
1313                ("assets/classes4.dex", &dex),
1314            ],
1315            true,
1316        );
1317        let f = write_temp(&apk_bytes);
1318        let apk = Apk::open(f.path()).unwrap();
1319        let names: Vec<String> = apk.dex_entries_multidex().unwrap().into_iter().map(|e| e.name).collect();
1320        assert_eq!(names, ["classes.dex", "classes2.dex", "classes3.dex", "classes10.dex", "extra.dex", "classes02.dex"]);
1321        assert_eq!(multidex_rank("services.jar/classes2.dex"), 2);
1322    }
1323
1324    /// Deflate stream of `n` zero bytes (compresses about 1000:1).
1325    fn zeros_deflated(n: usize) -> Vec<u8> {
1326        let mut e = flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::best());
1327        e.write_all(&vec![0u8; n]).unwrap();
1328        e.finish().unwrap()
1329    }
1330
1331    /// Hand-rolled zip (the `zip` crate refuses duplicate names and always
1332    /// writes true sizes): `(name, method, payload, comp_size, uncomp_size)`.
1333    fn raw_zip(entries: &[(&str, u16, &[u8], u32, u32)]) -> Vec<u8> {
1334        let mut z = Vec::new();
1335        let mut cd = Vec::new();
1336        for (name, method, payload, comp_size, uncomp_size) in entries {
1337            let lh = z.len() as u32;
1338            z.extend_from_slice(LH_SIG);
1339            z.extend_from_slice(&[20, 0, 0, 0]);
1340            z.extend_from_slice(&method.to_le_bytes());
1341            z.extend_from_slice(&[0; 8]);
1342            z.extend_from_slice(&comp_size.to_le_bytes());
1343            z.extend_from_slice(&uncomp_size.to_le_bytes());
1344            z.extend_from_slice(&(name.len() as u16).to_le_bytes());
1345            z.extend_from_slice(&0u16.to_le_bytes());
1346            z.extend_from_slice(name.as_bytes());
1347            z.extend_from_slice(payload);
1348            cd.extend_from_slice(CD_SIG);
1349            cd.extend_from_slice(&[20, 0, 20, 0, 0, 0]);
1350            cd.extend_from_slice(&method.to_le_bytes());
1351            cd.extend_from_slice(&[0; 8]);
1352            cd.extend_from_slice(&comp_size.to_le_bytes());
1353            cd.extend_from_slice(&uncomp_size.to_le_bytes());
1354            cd.extend_from_slice(&(name.len() as u16).to_le_bytes());
1355            cd.extend_from_slice(&[0; 12]);
1356            cd.extend_from_slice(&lh.to_le_bytes());
1357            cd.extend_from_slice(name.as_bytes());
1358        }
1359        let cd_off = z.len() as u32;
1360        z.extend_from_slice(&cd);
1361        z.extend_from_slice(EOCD_SIG);
1362        z.extend_from_slice(&[0, 0, 0, 0]);
1363        z.extend_from_slice(&(entries.len() as u16).to_le_bytes());
1364        z.extend_from_slice(&(entries.len() as u16).to_le_bytes());
1365        z.extend_from_slice(&(cd.len() as u32).to_le_bytes());
1366        z.extend_from_slice(&cd_off.to_le_bytes());
1367        z.extend_from_slice(&[0, 0]);
1368        z
1369    }
1370
1371    #[test]
1372    fn undeclared_size_is_still_bounded() {
1373        // 1 MiB of zeros deflates to about 1 KiB; the 1032:1 bound allows it.
1374        let ok = zeros_deflated(1 << 20);
1375        let f = write_temp(&raw_zip(&[("classes.dex", METHOD_DEFLATE, &ok, ok.len() as u32, 0)]));
1376        let apk = Apk::open(f.path()).unwrap();
1377        let e = apk.dex_entries().unwrap().remove(0);
1378        assert_eq!(e.uncomp_size, 0);
1379        assert_eq!(apk.read_entry(&e).unwrap().len(), 1 << 20);
1380        match apk.inflate_until(&e, |_| PrefixStep::Continue(4096)).unwrap() {
1381            Prefix::Partial(v) => assert!(v.len() >= 4096),
1382            Prefix::Complete(v) => assert_eq!(v.len(), 1 << 20),
1383            Prefix::Aborted(_) => panic!("unexpected abort"),
1384        }
1385        // A shorter compressed size lowers the bound and truncates the stream:
1386        // either way the entry is rejected instead of inflated open-endedly.
1387        let mut lying = e.clone();
1388        lying.comp_size = 200;
1389        assert!(apk.read_entry(&lying).is_err());
1390        assert!(apk.inflate_until(&lying, |_| PrefixStep::Continue(0)).is_err());
1391    }
1392
1393    #[test]
1394    fn hostile_offsets_do_not_overflow() {
1395        let dex = asc_fixture::make_dex();
1396        let f = write_temp(&asc_fixture::make_apk(&[("classes.dex", &dex)], true));
1397        let apk = Apk::open(f.path()).unwrap();
1398        let mut e = apk.dex_entries().unwrap().remove(0);
1399        for off in [u64::MAX, u64::MAX - 3, u64::MAX - 40, u32::MAX as u64] {
1400            e.local_header_off = off;
1401            assert!(matches!(apk.read_entry(&e), Err(ApkError::BadLocalHeader(_))));
1402            assert!(matches!(apk.inflate_until(&e, |_| PrefixStep::Continue(0)), Err(ApkError::BadLocalHeader(_))));
1403        }
1404        // zip64 locator pointing at an offset near usize::MAX.
1405        let mut bytes = asc_fixture::make_apk(&[("classes.dex", &dex)], false);
1406        let eocd = bytes.len() - 22;
1407        bytes[eocd + 16..eocd + 20].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
1408        let mut locator = Vec::new();
1409        locator.extend_from_slice(ZIP64_LOCATOR_SIG);
1410        locator.extend_from_slice(&0u32.to_le_bytes());
1411        locator.extend_from_slice(&(u64::MAX - 2).to_le_bytes());
1412        locator.extend_from_slice(&1u32.to_le_bytes());
1413        let tail = bytes.split_off(eocd);
1414        bytes.extend_from_slice(&locator);
1415        bytes.extend_from_slice(&tail);
1416        let f = write_temp(&bytes);
1417        let apk = Apk::open(f.path()).unwrap();
1418        assert!(matches!(apk.dex_entries(), Err(ApkError::NoEocd | ApkError::BadCentralDirectory)));
1419    }
1420
1421    #[test]
1422    fn duplicate_root_dex_names_keep_the_last() {
1423        let a = asc_fixture::make_dex();
1424        let mut b = a.clone();
1425        b.push(0); // different content, same name
1426        let (la, lb) = (a.len() as u32, b.len() as u32);
1427        let f = write_temp(&raw_zip(&[
1428            ("classes.dex", METHOD_STORED, &a, la, la),
1429            ("classes2.dex", METHOD_STORED, &a, la, la),
1430            ("classes.dex", METHOD_STORED, &b, lb, lb),
1431        ]));
1432        let apk = Apk::open(f.path()).unwrap();
1433        let entries = apk.dex_entries().unwrap();
1434        let mut names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
1435        names.sort();
1436        assert_eq!(names, ["classes.dex", "classes2.dex"]);
1437        let dup = entries.iter().find(|e| e.name == "classes.dex").unwrap();
1438        assert_eq!(apk.read_entry(dup).unwrap().as_ref(), b.as_slice());
1439        assert_eq!(apk.find_entry("classes.dex").unwrap().unwrap(), *dup);
1440        // Central-directory order, the duplicate at its last position.
1441        let ordered: Vec<String> = apk.dex_entries_in_order().unwrap().into_iter().map(|e| e.name).collect();
1442        assert_eq!(ordered, ["classes2.dex", "classes.dex"]);
1443    }
1444
1445    #[test]
1446    fn programs_name_entries_after_their_archive() {
1447        let dex = asc_fixture::make_dex();
1448        let dir = tempfile::tempdir().unwrap();
1449        let root = dir.path().join("system");
1450        std::fs::create_dir_all(root.join("app/Tele")).unwrap();
1451        let both = [("classes.dex", &dex[..]), ("classes2.dex", &dex[..]), ("AndroidManifest.xml", b"first")];
1452        std::fs::write(root.join("services.jar"), asc_fixture::make_apk(&both, true)).unwrap();
1453        let apk = [("classes.dex", &dex[..]), ("AndroidManifest.xml", b"second")];
1454        std::fs::write(root.join("app/Tele/Tele.apk"), asc_fixture::make_apk(&apk, false)).unwrap();
1455        std::fs::write(root.join("odexed.jar"), asc_fixture::make_apk(&[("META-INF/MANIFEST.MF", b"m")], false)).unwrap();
1456        std::fs::write(root.join("bare.dex"), &dex).unwrap();
1457        std::fs::write(root.join("broken.zip"), b"not a zip").unwrap();
1458
1459        let program = Apk::open(&root).unwrap();
1460        assert_eq!(program.archive_count(), 3);
1461        let names: Vec<String> = program.dex_entries_in_order().unwrap().into_iter().map(|e| e.name).collect();
1462        assert_eq!(
1463            names,
1464            ["app/Tele/Tele.apk/classes.dex", "bare.dex/classes.dex", "services.jar/classes.dex", "services.jar/classes2.dex"]
1465        );
1466        for e in program.dex_entries().unwrap() {
1467            assert_eq!(program.read_entry(&e).unwrap().as_ref(), dex.as_slice(), "{}", e.name);
1468        }
1469        let notes = program.notes().join("\n");
1470        assert!(notes.contains("broken.zip") && notes.contains("odexed.jar: no classes*.dex"), "{notes}");
1471        // Unprefixed names: the first archive that has one; prefixed: that archive's.
1472        assert_eq!(program.read("AndroidManifest.xml").unwrap(), b"second");
1473        assert_eq!(program.read("services.jar/AndroidManifest.xml").unwrap(), b"first");
1474        let bare = program.find_entry("bare.dex/classes.dex").unwrap().unwrap();
1475        assert_eq!(program.archive_path(&bare), root.join("bare.dex"));
1476        assert_eq!(archive_of("app/Tele/Tele.apk/classes.dex"), Some("app/Tele/Tele.apk"));
1477        assert_eq!(archive_of("services.jar/classes5.dex!classes1.dex"), Some("services.jar"));
1478        assert_eq!(archive_of("classes5.dex!classes1.dex"), None);
1479        assert_eq!(program.stem(), "Tele-and-2");
1480        assert!(program.stamp().is_ok() && program.directories().unwrap().len() == 3);
1481
1482        // A lone bare DEX, and a list that comes down to one archive, read as before.
1483        let lone = Apk::open(root.join("bare.dex")).unwrap();
1484        assert_eq!(lone.dex_entries().unwrap()[0].name, "classes.dex");
1485        let spec = format!("{},{}", root.join("services.jar").display(), root.join("odexed.jar").display());
1486        let one = Apk::open(&spec).unwrap();
1487        assert_eq!(one.archive_count(), 1);
1488        assert_eq!(one.dex_entries_in_order().unwrap()[0].name, "classes.dex");
1489        assert_eq!(one.notes().len(), 1);
1490        let spec = format!("{},{}", root.join("odexed.jar").display(), root.join("broken.zip").display());
1491        assert!(matches!(Apk::open(&spec), Err(ApkError::NoCode { .. })));
1492    }
1493
1494    #[test]
1495    fn inflate_until_streams_prefixes() {
1496        let dex = asc_fixture::make_dex();
1497        let apk_bytes = asc_fixture::make_apk(&[("classes.dex", &dex)], true);
1498        let f = write_temp(&apk_bytes);
1499        let apk = Apk::open(f.path()).unwrap();
1500        let entry = apk.dex_entries().unwrap().remove(0);
1501        match apk.inflate_until(&entry, |_| PrefixStep::Continue(8)).unwrap() {
1502            Prefix::Complete(v) | Prefix::Partial(v) => assert!(v.starts_with(b"dex\n") && v.len() >= 8),
1503            Prefix::Aborted(_) => panic!("unexpected abort"),
1504        }
1505        assert!(matches!(apk.inflate_until(&entry, |_| PrefixStep::Abort).unwrap(), Prefix::Aborted(_)));
1506    }
1507
1508    #[test]
1509    fn nested_archives_open_in_place_or_inflated() {
1510        let inner = asc_fixture::make_apk(&[("ets/modules.abc", b"PANDA body"), ("module.json", b"{}")], false);
1511        for deflate in [false, true] {
1512            let file = write_temp(&asc_fixture::make_apk(&[("entry.hap", &inner), ("pack.info", b"{}")], deflate));
1513            let outer = Apk::open(file.path()).unwrap();
1514            assert!(outer.lone_data().is_some_and(|data| data.starts_with(LH_SIG)));
1515            let entry = outer.find_entry("entry.hap").unwrap().unwrap();
1516            assert_eq!(entry.method, if deflate { METHOD_DEFLATE } else { METHOD_STORED });
1517            let nested = outer.open_nested(&entry).unwrap();
1518            assert_eq!(nested.lone_data(), Some(&inner[..]));
1519            // A stored inner archive is a window on the outer mapping, not a copy.
1520            let outer_range = outer.lone_data().unwrap().as_ptr_range();
1521            assert_eq!(outer_range.contains(&nested.lone_data().unwrap().as_ptr()), !deflate);
1522            let names: Vec<String> = nested.entries().unwrap().into_iter().map(|e| e.name).collect();
1523            assert_eq!(names, ["ets/modules.abc", "module.json"]);
1524            let abc = nested.find_entry("ets/modules.abc").unwrap().unwrap();
1525            assert!(matches!(nested.read_entry(&abc).unwrap(), Cow::Borrowed(b"PANDA body")));
1526            // The nested archive keeps the outer bytes alive on its own.
1527            drop(outer);
1528            assert_eq!(nested.read("module.json").unwrap(), b"{}");
1529        }
1530        let not_zip = write_temp(&asc_fixture::make_apk(&[("x.hap", b"not a zip")], false));
1531        let outer = Apk::open(not_zip.path()).unwrap();
1532        let nested = outer.open_nested(&outer.find_entry("x.hap").unwrap().unwrap()).unwrap();
1533        assert!(matches!(nested.entries(), Err(ApkError::NoEocd)));
1534    }
1535}