rust-apk 1.0.0

Build, align, sign, verify, and inspect Android APKs in pure Rust
Documentation
//! Reading existing APKs.

use std::collections::BTreeMap;
use std::fs::File;
use std::io::{Cursor, Read, Seek};
use std::path::Path;

use zip::ZipArchive;

use crate::archive::{self, ZipError};
use crate::error::{Error, FileOperation};
use crate::manifest::Manifest;
use crate::res::{EntryValue, ResourceRef, ResourceTable, Value};
use crate::types::{Abi, LibraryName};

/// The largest `AndroidManifest.xml` read from an APK.
pub const MANIFEST_LIMIT: u64 = 16 << 20;
/// The largest `resources.arsc` read from an APK.
pub const RESOURCE_TABLE_LIMIT: u64 = 256 << 20;
/// How many resource aliases [`Apk::label`] follows.
const MAX_ALIASES: usize = 8;

/// An APK opened for reading.
///
/// Opening checks UTF-8 names, duplicate entries, ZIP headers, and entry
/// placement (see [`ZipError`] for the rejected layouts).
///
/// ```no_run
/// use apk::Apk;
///
/// let mut apk = Apk::open("app.apk")?;
/// let manifest = apk.manifest()?;
/// println!("{} (API {}+)", manifest.package().unwrap_or_default(), manifest.min_sdk());
/// println!("{manifest}");
/// # Ok::<(), apk::Error>(())
/// ```
pub struct Apk<R: Read + Seek = File> {
    archive: ZipArchive<R>,
    names: Vec<String>,
}

impl Apk<File> {
    pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
        let path = path.as_ref();
        Self::new(File::open(path).map_err(Error::file(FileOperation::Open, path))?)
    }
}

impl<'a> Apk<Cursor<&'a [u8]>> {
    pub fn from_bytes(bytes: &'a [u8]) -> Result<Self, Error> {
        Self::new(Cursor::new(bytes))
    }
}

impl<R: Read + Seek> Apk<R> {
    pub fn new(reader: R) -> Result<Self, Error> {
        let (archive, _) = archive::open(reader)?;
        let names = (0..archive.len()).map(|index| archive::entry_name(&archive, index)).collect::<Result<_, _>>()?;
        Ok(Self { archive, names })
    }

    /// The entry names, in central directory order.
    pub fn entries(&self) -> impl Iterator<Item = &str> {
        self.names.iter().map(String::as_str)
    }

    /// Whether the APK has an entry `name`.
    pub fn contains(&self, name: &str) -> bool {
        self.archive.index_for_name(name).is_some()
    }

    /// Reads and decompresses the entry `name`.
    pub fn read(&mut self, name: &str) -> Result<Vec<u8>, Error> {
        self.read_limited(name, u64::MAX)
    }

    /// Reads the entry `name`, failing if it decompresses to more than `limit`
    /// bytes. Declared sizes are not trusted.
    pub fn read_limited(&mut self, name: &str, limit: u64) -> Result<Vec<u8>, Error> {
        Ok(archive::read_entry(&mut self.archive, name, limit)?)
    }

    /// The decoded `AndroidManifest.xml`.
    pub fn manifest(&mut self) -> Result<Manifest, Error> {
        read_manifest(&mut self.archive)?.ok_or_else(|| ZipError::MissingEntry { name: MANIFEST.into() }.into())
    }

    /// The application label in the default configuration, following string
    /// resource references.
    pub fn label(&mut self) -> Result<Option<String>, Error> {
        let mut id = match self.manifest()?.label() {
            Some(Value::String(label)) => return Ok(Some(label.clone())),
            Some(Value::Reference(ResourceRef::Id(id))) => *id,
            _ => return Ok(None),
        };
        if !self.contains(RESOURCE_TABLE) {
            return Ok(None);
        }
        let bytes = self.read_limited(RESOURCE_TABLE, RESOURCE_TABLE_LIMIT)?;
        let table = ResourceTable::parse(&bytes)?;
        for _ in 0..MAX_ALIASES {
            match table.get(id).and_then(|resource| resource.default_value()) {
                Some(EntryValue::Simple(Value::String(label))) => return Ok(Some(label)),
                Some(EntryValue::Simple(Value::Reference(ResourceRef::Id(next)))) => id = next,
                _ => break,
            }
        }
        Ok(None)
    }

    /// The native libraries (`lib/<abi>/lib<name>.so`), by ABI.
    pub fn native_libraries(&self) -> BTreeMap<Abi, Vec<LibraryName>> {
        let mut libraries: BTreeMap<Abi, Vec<LibraryName>> = BTreeMap::new();
        for name in &self.names {
            let Some((abi, file)) = name.strip_prefix("lib/").and_then(|rest| rest.split_once('/')) else { continue };
            if let (Ok(abi), Ok(library)) = (abi.parse(), LibraryName::from_file_name(file)) {
                libraries.entry(abi).or_default().push(library);
            }
        }
        libraries
    }
}

const MANIFEST: &str = "AndroidManifest.xml";
const RESOURCE_TABLE: &str = "resources.arsc";

/// The decoded manifest of `archive`, if it has one.
pub(crate) fn read_manifest<R: Read + Seek>(archive: &mut ZipArchive<R>) -> Result<Option<Manifest>, Error> {
    match archive::read_entry(archive, MANIFEST, MANIFEST_LIMIT) {
        Ok(bytes) => Ok(Some(Manifest::decode(&bytes)?)),
        Err(ZipError::MissingEntry { .. }) => Ok(None),
        Err(error) => Err(error.into()),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::archive::{ApkWriter, EntryOptions};
    use crate::types::ArchivePath;

    #[test]
    fn reads_are_bounded_by_decompressed_size() {
        let mut writer = ApkWriter::new(Cursor::new(Vec::new()));
        // A tiny compressed entry that inflates to 1 MiB.
        writer.add(&ArchivePath::known("bomb.bin"), &vec![0u8; 1 << 20], EntryOptions::DEFLATED).unwrap();
        let bytes = writer.finish().unwrap().into_inner();
        let mut apk = Apk::from_bytes(&bytes).unwrap();
        assert!(matches!(
            apk.read_limited("bomb.bin", 1024),
            Err(Error::Zip(ZipError::EntryTooLarge { limit: 1024, .. }))
        ));
        assert_eq!(apk.read_limited("bomb.bin", 1 << 20).unwrap().len(), 1 << 20);
        assert!(matches!(apk.read("missing"), Err(Error::Zip(ZipError::MissingEntry { .. }))));
        assert!(matches!(apk.manifest(), Err(Error::Zip(ZipError::MissingEntry { .. }))));
    }
}