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};
pub const MANIFEST_LIMIT: u64 = 16 << 20;
pub const RESOURCE_TABLE_LIMIT: u64 = 256 << 20;
const MAX_ALIASES: usize = 8;
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 })
}
pub fn entries(&self) -> impl Iterator<Item = &str> {
self.names.iter().map(String::as_str)
}
pub fn contains(&self, name: &str) -> bool {
self.archive.index_for_name(name).is_some()
}
pub fn read(&mut self, name: &str) -> Result<Vec<u8>, Error> {
self.read_limited(name, u64::MAX)
}
pub fn read_limited(&mut self, name: &str, limit: u64) -> Result<Vec<u8>, Error> {
Ok(archive::read_entry(&mut self.archive, name, limit)?)
}
pub fn manifest(&mut self) -> Result<Manifest, Error> {
read_manifest(&mut self.archive)?.ok_or_else(|| ZipError::MissingEntry { name: MANIFEST.into() }.into())
}
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)
}
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";
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()));
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 { .. }))));
}
}