Skip to main content

apk/
apk.rs

1//! Reading existing APKs.
2
3use std::collections::BTreeMap;
4use std::fs::File;
5use std::io::{Cursor, Read, Seek};
6use std::path::Path;
7
8use zip::ZipArchive;
9
10use crate::archive::{self, ZipError};
11use crate::error::{Error, FileOperation};
12use crate::manifest::Manifest;
13use crate::res::{EntryValue, ResourceRef, ResourceTable, Value};
14use crate::types::{Abi, LibraryName};
15
16/// The largest `AndroidManifest.xml` read from an APK.
17pub const MANIFEST_LIMIT: u64 = 16 << 20;
18/// The largest `resources.arsc` read from an APK.
19pub const RESOURCE_TABLE_LIMIT: u64 = 256 << 20;
20/// How many resource aliases [`Apk::label`] follows.
21const MAX_ALIASES: usize = 8;
22
23/// An APK opened for reading.
24///
25/// Opening checks UTF-8 names, duplicate entries, ZIP headers, and entry
26/// placement (see [`ZipError`] for the rejected layouts).
27///
28/// ```no_run
29/// use apk::Apk;
30///
31/// let mut apk = Apk::open("app.apk")?;
32/// let manifest = apk.manifest()?;
33/// println!("{} (API {}+)", manifest.package().unwrap_or_default(), manifest.min_sdk());
34/// println!("{manifest}");
35/// # Ok::<(), apk::Error>(())
36/// ```
37pub struct Apk<R: Read + Seek = File> {
38    archive: ZipArchive<R>,
39    names: Vec<String>,
40}
41
42impl Apk<File> {
43    pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
44        let path = path.as_ref();
45        Self::new(File::open(path).map_err(Error::file(FileOperation::Open, path))?)
46    }
47}
48
49impl<'a> Apk<Cursor<&'a [u8]>> {
50    pub fn from_bytes(bytes: &'a [u8]) -> Result<Self, Error> {
51        Self::new(Cursor::new(bytes))
52    }
53}
54
55impl<R: Read + Seek> Apk<R> {
56    pub fn new(reader: R) -> Result<Self, Error> {
57        let (archive, _) = archive::open(reader)?;
58        let names = (0..archive.len()).map(|index| archive::entry_name(&archive, index)).collect::<Result<_, _>>()?;
59        Ok(Self { archive, names })
60    }
61
62    /// The entry names, in central directory order.
63    pub fn entries(&self) -> impl Iterator<Item = &str> {
64        self.names.iter().map(String::as_str)
65    }
66
67    /// Whether the APK has an entry `name`.
68    pub fn contains(&self, name: &str) -> bool {
69        self.archive.index_for_name(name).is_some()
70    }
71
72    /// Reads and decompresses the entry `name`.
73    pub fn read(&mut self, name: &str) -> Result<Vec<u8>, Error> {
74        self.read_limited(name, u64::MAX)
75    }
76
77    /// Reads the entry `name`, failing if it decompresses to more than `limit`
78    /// bytes. Declared sizes are not trusted.
79    pub fn read_limited(&mut self, name: &str, limit: u64) -> Result<Vec<u8>, Error> {
80        Ok(archive::read_entry(&mut self.archive, name, limit)?)
81    }
82
83    /// The decoded `AndroidManifest.xml`.
84    pub fn manifest(&mut self) -> Result<Manifest, Error> {
85        read_manifest(&mut self.archive)?.ok_or_else(|| ZipError::MissingEntry { name: MANIFEST.into() }.into())
86    }
87
88    /// The application label in the default configuration, following string
89    /// resource references.
90    pub fn label(&mut self) -> Result<Option<String>, Error> {
91        let mut id = match self.manifest()?.label() {
92            Some(Value::String(label)) => return Ok(Some(label.clone())),
93            Some(Value::Reference(ResourceRef::Id(id))) => *id,
94            _ => return Ok(None),
95        };
96        if !self.contains(RESOURCE_TABLE) {
97            return Ok(None);
98        }
99        let bytes = self.read_limited(RESOURCE_TABLE, RESOURCE_TABLE_LIMIT)?;
100        let table = ResourceTable::parse(&bytes)?;
101        for _ in 0..MAX_ALIASES {
102            match table.get(id).and_then(|resource| resource.default_value()) {
103                Some(EntryValue::Simple(Value::String(label))) => return Ok(Some(label)),
104                Some(EntryValue::Simple(Value::Reference(ResourceRef::Id(next)))) => id = next,
105                _ => break,
106            }
107        }
108        Ok(None)
109    }
110
111    /// The native libraries (`lib/<abi>/lib<name>.so`), by ABI.
112    pub fn native_libraries(&self) -> BTreeMap<Abi, Vec<LibraryName>> {
113        let mut libraries: BTreeMap<Abi, Vec<LibraryName>> = BTreeMap::new();
114        for name in &self.names {
115            let Some((abi, file)) = name.strip_prefix("lib/").and_then(|rest| rest.split_once('/')) else { continue };
116            if let (Ok(abi), Ok(library)) = (abi.parse(), LibraryName::from_file_name(file)) {
117                libraries.entry(abi).or_default().push(library);
118            }
119        }
120        libraries
121    }
122}
123
124const MANIFEST: &str = "AndroidManifest.xml";
125const RESOURCE_TABLE: &str = "resources.arsc";
126
127/// The decoded manifest of `archive`, if it has one.
128pub(crate) fn read_manifest<R: Read + Seek>(archive: &mut ZipArchive<R>) -> Result<Option<Manifest>, Error> {
129    match archive::read_entry(archive, MANIFEST, MANIFEST_LIMIT) {
130        Ok(bytes) => Ok(Some(Manifest::decode(&bytes)?)),
131        Err(ZipError::MissingEntry { .. }) => Ok(None),
132        Err(error) => Err(error.into()),
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::archive::{ApkWriter, EntryOptions};
140    use crate::types::ArchivePath;
141
142    #[test]
143    fn reads_are_bounded_by_decompressed_size() {
144        let mut writer = ApkWriter::new(Cursor::new(Vec::new()));
145        // A tiny compressed entry that inflates to 1 MiB.
146        writer.add(&ArchivePath::known("bomb.bin"), &vec![0u8; 1 << 20], EntryOptions::DEFLATED).unwrap();
147        let bytes = writer.finish().unwrap().into_inner();
148        let mut apk = Apk::from_bytes(&bytes).unwrap();
149        assert!(matches!(
150            apk.read_limited("bomb.bin", 1024),
151            Err(Error::Zip(ZipError::EntryTooLarge { limit: 1024, .. }))
152        ));
153        assert_eq!(apk.read_limited("bomb.bin", 1 << 20).unwrap().len(), 1 << 20);
154        assert!(matches!(apk.read("missing"), Err(Error::Zip(ZipError::MissingEntry { .. }))));
155        assert!(matches!(apk.manifest(), Err(Error::Zip(ZipError::MissingEntry { .. }))));
156    }
157}