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
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/// One central-directory entry.
76#[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    /// CRC-32 of the uncompressed data, as the central directory records it.
84    pub crc32: u32,
85    /// The archive of a program it belongs to ([`Apk::archive_path`]).
86    pub archive: usize,
87}
88
89/// Caps on what reading an APK may inflate, against zip bombs. The default
90/// only rejects what Deflate cannot produce; [`Limits::guarded`] is for
91/// sweeping untrusted APKs in bulk.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct Limits {
94    /// Most bytes one entry may inflate to.
95    pub max_entry_bytes: u64,
96    /// Most an entry may expand over its compressed payload.
97    pub max_ratio: u64,
98    /// Most bytes all root DEX entries together may declare.
99    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    /// Real DEX files are at most a few hundred MiB and compress 2-5:1.
110    pub fn guarded() -> Limits {
111        Limits { max_entry_bytes: 1 << 30, max_ratio: 100, max_dex_total_bytes: 4 << 30 }
112    }
113}
114
115/// A memory-mapped APK, or several archives (APKs, jars, bare DEX files)
116/// read as one program: see [`Apk::open`]. Clones share the mappings, so a
117/// host can keep one open and hand out copies while [`Apk::version`] holds.
118#[derive(Clone)]
119pub struct Apk {
120    path: PathBuf,
121    archives: Arc<[Archive]>,
122    limits: Limits,
123    /// Archives of a program left out of its code, one line each.
124    notes: Arc<[String]>,
125    version: Version,
126}
127
128/// One archive of an [`Apk`] as a cache key sees it ([`Apk::directories`]).
129#[derive(Debug, Clone, Copy)]
130pub struct ArchiveId<'a> {
131    pub path: &'a Path,
132    /// What its entry names start with in a program ([`archive_of`]); empty
133    /// for a lone archive.
134    pub label: &'a str,
135    /// Its central directory, or a bare DEX file's header.
136    pub directory: &'a [u8],
137}
138
139/// One file of an [`Apk`].
140struct Archive {
141    path: PathBuf,
142    map: Data,
143    /// Its name in a program of several archives (`services.jar`,
144    /// `app/TeleService/TeleService.apk`), which prefixes its entry names;
145    /// empty for a lone archive.
146    label: String,
147    /// A bare DEX file rather than a zip: one stored entry, the whole file.
148    bare_dex: bool,
149}
150
151/// The bytes of one archive: a mapped file, or an entry of another archive
152/// that is itself an archive ([`Apk::open_nested`]).
153enum Data {
154    Mapped(Mmap),
155    /// A stored entry, read in place: `range` of archive `archive` of `outer`.
156    Window {
157        outer: Arc<[Archive]>,
158        archive: usize,
159        range: std::ops::Range<usize>,
160    },
161    /// An inflated entry.
162    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            // The range was taken from this very archive when the window was made.
172            Data::Window { outer, archive, range } => &outer[*archive].map[range.clone()],
173            Data::Owned(bytes) => bytes,
174        }
175    }
176}
177
178/// `n` bytes at `o`, or `None` if they are not all inside `b` (offsets come
179/// from the archive, so the addition itself must not be trusted either).
180#[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/// A `u64` archive offset as `usize`, or `None` when it cannot even be
197/// represented (32-bit hosts).
198#[inline]
199fn offset(v: u64) -> Option<usize> {
200    usize::try_from(v).ok()
201}
202
203/// The archive of a program a DEX of it comes from, by the DEX's name
204/// (`app/Tele.apk` for `app/Tele.apk/classes2.dex`, also as a logical DEX
205/// `...classes2.dex!classes1.dex`); `None` for a lone archive's.
206pub 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
211/// Whether `name` (inside one archive) is a root-level `*.dex`
212/// (`classes.dex`, `classes2.dex`, ...).
213fn is_root_dex(name: &[u8]) -> bool {
214    name.ends_with(b".dex") && !name.contains(&b'/')
215}
216
217/// The one entry of a bare DEX file, by its name inside the archive.
218const BARE_DEX_ENTRY: &str = "classes.dex";
219/// Size of the DEX header, which carries the file's checksum and SHA-1.
220const 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        // SAFETY: the mapping is read-only and the file is not expected to be
226        // modified while it is mapped (same contract as the Python mmap use).
227        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    /// `inner` (a name inside this archive) as the program names it:
233    /// `label/inner`, so the archive is all before the last `/`.
234    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    /// The name inside this archive of the program's entry `name`, if it
240    /// names one of this archive's entries.
241    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    /// Locate the central directory as `(offset, size)`, honouring zip64.
249    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            // zip64: locator sits right before the EOCD.
257            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    /// What identifies this version of the archive: its central directory,
275    /// or a bare DEX file's header.
276    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    /// Walk the central directory, `keep` filtering by the name inside this
285    /// archive; the entries are of archive `index` of the program.
286    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                    // zip64 extra field (id 0x0001).
326                    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    /// Root-level DEX entries in central-directory order. Duplicate names
365    /// keep the last occurrence, like [`Apk::find_entry`] (and `zipfile`).
366    fn dex_entries_in_order(&self, index: usize) -> Result<Vec<Entry>> {
367        let mut entries = self.entries_filtered(index, &mut is_root_dex)?;
368        // Last occurrence wins: walk from the back keeping first-seen names.
369        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    /// Open `path` read-only: an APK (any zip, e.g. a jar), a bare DEX file,
379    /// or a program of several archives read as one:
380    ///
381    /// - a directory: every `*.apk`, `*.jar`, `*.zip` and `*.dex` below it;
382    /// - several paths (files or directories) joined with `,`, when no file
383    ///   of that whole name exists.
384    ///
385    /// A program's entries are named after their archive
386    /// (`services.jar/classes2.dex`, `x.dex/classes.dex` for a bare DEX
387    /// file; see [`archive_of`]). Archives without root DEX entries, or
388    /// that do not open, are left out and named in [`Apk::notes`]; a program
389    /// none of whose archives has code is an error. A program that comes
390    /// down to one archive is that archive alone.
391    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    /// The files `path` (as [`Apk::open`] takes it) names now, with their
425    /// sizes and modification times: equal to an open [`Apk::version`]
426    /// when opening again would read the same files. Costs a directory walk
427    /// and a `stat` per archive, not a mapping.
428    pub fn version_of(path: impl AsRef<Path>) -> Result<Version> {
429        Ok(Version::of_sources(&program::sources(path.as_ref())?))
430    }
431
432    /// The files read and their sizes and times when this was opened.
433    pub fn version(&self) -> &Version {
434        &self.version
435    }
436
437    /// `spec` (a path [`Apk::open`] takes) with its relative paths resolved
438    /// against `base`: each of several paths joined with `,` on its own.
439    pub fn resolve(spec: &Path, base: &Path) -> PathBuf {
440        program::resolve(spec, base)
441    }
442
443    /// Enforce `limits` on every later read.
444    pub fn with_limits(mut self, limits: Limits) -> Apk {
445        self.limits = limits;
446        self
447    }
448
449    /// The path the APK was opened from (a program's directory or list).
450    pub fn path(&self) -> &Path {
451        &self.path
452    }
453
454    /// The limits every read enforces ([`Apk::with_limits`]).
455    pub fn limits(&self) -> Limits {
456        self.limits
457    }
458
459    /// How many archives make up the program: 1 for a lone archive, whose
460    /// entry names are the names inside it.
461    pub fn archive_count(&self) -> usize {
462        self.archives.len()
463    }
464
465    /// One line per archive of a program left out of its code.
466    pub fn notes(&self) -> &[String] {
467        &self.notes
468    }
469
470    /// The file `entry` lives in.
471    pub fn archive_path(&self, entry: &Entry) -> &Path {
472        &self.archive(entry).path
473    }
474
475    /// The raw bytes of the file `entry` lives in.
476    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    /// What identifies each archive's version, in program order. A cache key
485    /// hashes them.
486    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    /// A short name for the APK: the file stem, or for a program the first
491    /// archive's and how many others follow.
492    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    /// `(size, modification time)` of the file; for a program, its sizes and
501    /// times hashed into one number and the latest time, so rewriting any
502    /// archive changes it.
503    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            // Only a hash input: the low bits of the nanoseconds are what differ.
515            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    /// Walk the central directories. `keep` filters entries by their name
523    /// inside their archive early, so the full name list is never
524    /// materialised for large archives.
525    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    /// Every entry of the archive.
534    pub fn entries(&self) -> Result<Vec<Entry>> {
535        self.entries_filtered(|_| true)
536    }
537
538    /// Root-level DEX entries sorted by compressed size (smallest first, so a
539    /// parallel scan finishes the cheap ones early). Duplicate names keep the
540    /// last occurrence, like [`Apk::find_entry`] (and `zipfile`).
541    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    /// The entries of [`Apk::dex_entries`] in central-directory order, archive
549    /// by archive.
550    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    /// Find an entry by exact name. In a program, a name without an
570    /// archive's prefix (`AndroidManifest.xml`) is looked up archive by
571    /// archive, the first that has it winning.
572    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    /// Compressed payload of `entry` inside the mapping.
588    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    /// Where [`Apk::raw_payload`] of `entry` lies in its archive's bytes.
594    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        // The signature check bounds `lh`, so these small additions cannot overflow.
606        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    /// The raw bytes of a lone archive's file (a zip, or a file that is not
614    /// one, which a caller may know how to read); `None` for a program of
615    /// several archives.
616    pub fn lone_data(&self) -> Option<&[u8]> {
617        match &self.archives[..] {
618            [lone] => Some(&lone.map),
619            _ => None,
620        }
621    }
622
623    /// Bytes of the archives this holds on the heap: the inflated ones of
624    /// [`Apk::open_nested`]. Clones share them.
625    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    /// Open `entry`, an archive inside this one (a `.hap` of an `.app`, a jar
630    /// of a zip), as an archive of its own with this one's limits. A stored
631    /// entry is read in place; a Deflate entry is inflated into memory and
632    /// kept there for as long as the returned [`Apk`] lives. Its entry names
633    /// are the names inside it.
634    pub fn open_nested(&self, entry: &Entry) -> Result<Apk> {
635        let map = match entry.method {
636            METHOD_STORED => {
637                // Checks the declared size against the payload like any other read.
638                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    /// Extract `entry`: stored entries are borrowed straight from the mapping,
655    /// Deflate entries are inflated into a fresh buffer.
656    pub fn read_entry(&self, entry: &Entry) -> Result<Cow<'_, [u8]>> {
657        self.read_entry_cancellable(entry, None)
658    }
659
660    /// Extract `entry`, polling `cancel` between input chunks. Returns
661    /// `Err(ApkError::Cancelled)` when the flag is raised mid-way.
662    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                // Stop one byte past the bound so an over-long stream is detected
699                // instead of inflated without limit (an entry that declares no
700                // uncompressed size has no other cap).
701                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    /// Reject Deflate entries whose declared uncompressed size cannot come out
725    /// of their compressed payload, before anything is allocated for them.
726    /// Returns the most an inflated entry may legitimately grow to; it also
727    /// bounds entries that declare no uncompressed size at all.
728    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    /// An entry whose stream ran past `bound` (from [`Apk::check_declared_size`]).
748    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    /// Convenience: extract a named entry.
765    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/// Decision returned by the callback of [`Apk::inflate_until`].
772#[derive(Debug, Clone, Copy, PartialEq, Eq)]
773pub enum PrefixStep {
774    /// Keep inflating; stop once at least this many bytes are available
775    /// (`0` = undecided, ask again after the next chunk).
776    Continue(usize),
777    /// A prefix is not useful for this entry; stop now.
778    Abort,
779}
780
781/// Outcome of [`Apk::inflate_until`].
782#[derive(Debug, Clone, PartialEq, Eq)]
783pub enum Prefix {
784    /// The whole entry (the stream ended before or at the requested length).
785    Complete(Vec<u8>),
786    /// At least the requested number of leading bytes.
787    Partial(Vec<u8>),
788    /// The callback aborted; holds whatever was inflated so far.
789    Aborted(Vec<u8>),
790}
791
792impl Apk {
793    /// Inflate a Deflate entry incrementally, letting `decide` inspect the
794    /// leading bytes after every chunk to say how much of the entry is
795    /// actually needed. Stored entries are returned whole.
796    pub fn inflate_until(&self, entry: &Entry, mut decide: impl FnMut(&[u8]) -> PrefixStep) -> Result<Prefix> {
797        // Small first chunks so a quick "not useful" costs little; then up to CHUNK.
798        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                        // An entry that declares no size cannot cap the request.
827                        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
847/// Slice reader that serves bounded chunks and honours a cancellation flag.
848struct 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        // Zeros compress far beyond any real DEX.
884        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        // An entry with no declared size is cut off by the same bound while streaming.
893        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                // A lying central directory must not drive a huge allocation.
927                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    /// Deflate stream of `n` zero bytes (compresses about 1000:1).
939    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    /// Hand-rolled zip (the `zip` crate refuses duplicate names and always
946    /// writes true sizes): `(name, method, payload, comp_size, uncomp_size)`.
947    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        // 1 MiB of zeros deflates to about 1 KiB; the 1032:1 bound allows it.
988        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        // A shorter compressed size lowers the bound and truncates the stream:
1000        // either way the entry is rejected instead of inflated open-endedly.
1001        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        // zip64 locator pointing at an offset near usize::MAX.
1019        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); // different content, same name
1040        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        // Central-directory order, the duplicate at its last position.
1055        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        // Unprefixed names: the first archive that has one; prefixed: that archive's.
1086        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        // A lone bare DEX, and a list that comes down to one archive, read as before.
1097        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            // A stored inner archive is a window on the outer mapping, not a copy.
1134            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            // The nested archive keeps the outer bytes alive on its own.
1141            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}