use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
use std::io::{BufWriter, Write};
use std::path::Path;
use crate::android::attr;
use crate::archive::{ApkWriter, Compression, Content, EntryOptions, ZipError};
use crate::error::{Error, FileOperation};
use crate::manifest::{self, Manifest, ManifestError};
use crate::native::{ElfInfo, LibraryStorage, NATIVE_ACTIVITY_ENTRY, NativeError, NativePolicy, PageSize};
use crate::res::{Config, Density, Locale, ResourceRef, ResourceType, Resources, Value};
use crate::sign::Signer;
use crate::types::{Abi, ApiLevel, ArchivePath, LibraryName, PackageName, PermissionName, VersionCode};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum BuildError {
#[error("minSdkVersion {min} is above targetSdkVersion {target}")]
MinSdkAboveTarget { min: ApiLevel, target: ApiLevel },
#[error("maxSdkVersion {max} is below minSdkVersion {min}")]
MaxSdkBelowMin { min: ApiLevel, max: u32 },
#[error("the NativeActivity library is named by {value}, which the builder cannot check; name it literally")]
NativeLibraryNotLiteral { value: Value },
#[error("NativeActivity requires minSdkVersion 9 or later")]
NativeActivityRequiresApi9,
#[error("the app has no native libraries for its NativeActivity to load")]
NoNativeLibraries,
#[error("hasCode is true but no DEX files were added")]
MissingCode,
#[error("DEX files were added but the manifest declares hasCode=\"false\"")]
CodeDeclaredAbsent,
#[error("{path} is written by the builder; use the dedicated method instead of `file`")]
ReservedPath { path: ArchivePath },
#[error("android:extractNativeLibs={manifest} conflicts with {storage:?} library storage")]
ExtractNativeLibsConflict { manifest: bool, storage: LibraryStorage },
#[error("{component} has intent filters, so targetSdkVersion 31+ requires android:exported")]
ExportedRequired { component: String },
}
#[derive(Clone, Debug)]
struct NativeInput {
abi: Abi,
name: LibraryName,
content: Content,
}
#[derive(Clone, Debug)]
pub struct ApkBuilder {
manifest: Manifest,
resources: Resources,
label: Option<String>,
files: BTreeMap<ArchivePath, (Content, Option<EntryOptions>)>,
dex: Vec<Content>,
native: Vec<NativeInput>,
native_policy: NativePolicy,
signer: Option<Signer>,
}
impl ApkBuilder {
pub fn new(manifest: Manifest) -> Self {
Self {
manifest,
resources: Resources::new(),
label: None,
files: BTreeMap::new(),
dex: Vec::new(),
native: Vec::new(),
native_policy: NativePolicy::default(),
signer: None,
}
}
pub fn native_activity(package: PackageName, library: LibraryName) -> Self {
let mut manifest = Manifest::new(&package);
manifest.application_mut().set_attr(attr::HAS_CODE, false).push_child(manifest::native_activity(&library));
Self::new(manifest)
}
pub fn manifest(&self) -> &Manifest {
&self.manifest
}
pub fn manifest_mut(&mut self) -> &mut Manifest {
&mut self.manifest
}
pub fn resources(&self) -> &Resources {
&self.resources
}
pub fn resources_mut(&mut self) -> &mut Resources {
&mut self.resources
}
pub fn label(&mut self, label: impl Into<String>) -> &mut Self {
self.label = Some(label.into());
self
}
pub fn version(&mut self, code: VersionCode, name: &str) -> &mut Self {
self.manifest.set_version(code, name);
self
}
pub fn min_sdk(&mut self, level: ApiLevel) -> &mut Self {
self.manifest.set_min_sdk(level);
self
}
pub fn target_sdk(&mut self, level: ApiLevel) -> &mut Self {
self.manifest.set_target_sdk(level);
self
}
pub fn permission(&mut self, name: PermissionName) -> &mut Self {
self.manifest.add_permission(&name);
self
}
pub fn debuggable(&mut self, debuggable: bool) -> &mut Self {
self.manifest.application_mut().set_attr(attr::DEBUGGABLE, debuggable);
self
}
pub fn string(&mut self, name: &str, value: impl Into<String>) -> crate::Result<&mut Self> {
self.resources.string(name, value)?;
Ok(self)
}
pub fn localized_string(
&mut self,
name: &str,
locale: Locale,
value: impl Into<String>,
) -> crate::Result<&mut Self> {
self.resources.localized_string(name, locale, value)?;
Ok(self)
}
pub fn icon(&mut self, density: Density, png: impl Into<Content>) -> crate::Result<&mut Self> {
self.resources.file(ResourceType::Mipmap, "ic_launcher", Config::for_density(density), "png", png)?;
Ok(self)
}
pub fn asset(&mut self, path: &str, content: impl Into<Content>) -> crate::Result<&mut Self> {
let path = ArchivePath::try_new(format!("assets/{path}"))?;
self.file(path, content, None)
}
pub fn file(
&mut self,
path: ArchivePath,
content: impl Into<Content>,
options: Option<EntryOptions>,
) -> crate::Result<&mut Self> {
if is_reserved(&path) {
return Err(BuildError::ReservedPath { path }.into());
}
if self.files.contains_key(&path) {
return Err(ZipError::DuplicateEntry { name: path.to_string() }.into());
}
self.files.insert(path, (content.into(), options));
Ok(self)
}
pub fn dex(&mut self, content: impl Into<Content>) -> &mut Self {
self.dex.push(content.into());
self
}
pub fn native_library(&mut self, abi: Abi, path: impl AsRef<Path>) -> crate::Result<&mut Self> {
let path = path.as_ref();
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or_default();
let name = LibraryName::from_file_name(file_name)?;
self.add_native(abi, name, Content::File(path.to_owned()))
}
pub fn native_library_bytes(
&mut self,
abi: Abi,
name: LibraryName,
bytes: impl Into<Content>,
) -> crate::Result<&mut Self> {
self.add_native(abi, name, bytes.into())
}
fn add_native(&mut self, abi: Abi, name: LibraryName, content: Content) -> crate::Result<&mut Self> {
if self.native.iter().any(|n| n.abi == abi && n.name == name) {
return Err(NativeError::DuplicateLibrary { library: name, abi }.into());
}
self.native.push(NativeInput { abi, name, content });
Ok(self)
}
pub fn native_policy(&mut self, policy: NativePolicy) -> &mut Self {
self.native_policy = policy;
self
}
pub fn sign_with(&mut self, signer: Signer) -> &mut Self {
self.signer = Some(signer);
self
}
pub fn write(mut self, output: impl AsRef<Path>) -> Result<(), Error> {
let output = output.as_ref();
self.complete_manifest()?;
let app = self.check()?;
self.check_native(&app)?;
let plan =
self.signer.as_ref().map(|signer| signer.plan(Some(app.min_sdk), Some(app.target_sdk))).transpose()?;
let compiled = self.resources.compile(&app.package, app.min_sdk)?;
let manifest = self.manifest.document().encode(&compiled)?;
if let Some(parent) = output.parent().filter(|p| !p.as_os_str().is_empty()) {
std::fs::create_dir_all(parent).map_err(Error::file(FileOperation::Create, parent))?;
}
let mut staged = crate::fs::Staged::beside(output)?;
let staged_path = staged.path().to_owned();
let mut writer = ApkWriter::new(BufWriter::new(staged.file()));
if let Some(digest) = plan.as_ref().and_then(|plan| plan.jar_digest()) {
writer = writer.digest_entries(digest);
}
writer.add(&ArchivePath::manifest(), &manifest, EntryOptions::DEFLATED)?;
for (index, dex) in self.dex.iter().enumerate() {
let name = if index == 0 { "classes.dex".to_owned() } else { format!("classes{}.dex", index + 1) };
writer.add_content(&ArchivePath::try_new(name)?, dex, EntryOptions::DEFLATED)?;
}
writer.add(&ArchivePath::resource_table(), &compiled.table, EntryOptions::STORED)?;
for (path, content) in &compiled.files {
writer.add_content(path, content, EntryOptions::for_path(path))?;
}
for (path, (content, options)) in &self.files {
writer.add_content(path, content, options.unwrap_or_else(|| EntryOptions::for_path(path)))?;
}
let library_options = match self.native_policy.storage {
LibraryStorage::Mapped => EntryOptions::page_aligned(PageSize::SixteenKiB.bytes() as u16),
LibraryStorage::Extracted => EntryOptions { compression: Compression::Deflated, alignment: 1 },
};
let mut libraries: Vec<&NativeInput> = self.native.iter().collect();
libraries.sort_by(|left, right| (left.abi, &left.name).cmp(&(right.abi, &right.name)));
for library in libraries {
let bytes = self.read_native(library, &app)?;
let path = ArchivePath::try_new(format!("lib/{}/{}", library.abi, library.name.file_name()))?;
writer.add(&path, &bytes, library_options)?;
}
if let Some(plan) = &plan {
for (path, data) in plan.jar_signature_files(writer.digests())? {
writer.add(&path, &data, EntryOptions::STORED)?;
}
}
writer.finish()?.flush().map_err(Error::file(FileOperation::Write, staged_path))?;
if let Some(plan) = &plan {
plan.apply_block(staged.file(), &[])?;
}
staged.persist(output)
}
fn complete_manifest(&mut self) -> Result<(), Error> {
let labeled = self.label.is_some();
if let Some(label) = self.label.take()
&& !self.resources.contains_variant(ResourceType::String, "app_name", Config::default())
{
self.resources.string("app_name", label)?;
}
let icon = self.resources.contains(ResourceType::Mipmap, "ic_launcher");
let defaults = [
(attr::LABEL, labeled.then(|| app_ref(ResourceType::String, "app_name"))),
(attr::ICON, icon.then(|| app_ref(ResourceType::Mipmap, "ic_launcher"))),
(attr::HAS_CODE, Some((!self.dex.is_empty()).into())),
(
attr::EXTRACT_NATIVE_LIBS,
(!self.native.is_empty()).then(|| (self.native_policy.storage == LibraryStorage::Extracted).into()),
),
];
let application = self.manifest.application_mut();
for (def, value) in defaults {
if let Some(value) = value
&& application.android_attr(def).is_none()
{
application.set_attr(def, value);
}
}
Ok(())
}
fn check(&self) -> Result<App, Error> {
let manifest = &self.manifest;
manifest.document().check()?;
manifest.validate()?;
let package = PackageName::try_new(manifest.package().ok_or(ManifestError::MissingPackage)?)?;
let (min_sdk, target_sdk) = (manifest.min_sdk().api_level()?, manifest.effective_target_sdk()?);
if min_sdk > target_sdk {
return Err(BuildError::MinSdkAboveTarget { min: min_sdk, target: target_sdk }.into());
}
if let Some(max) = manifest.max_sdk().filter(|&max| max < min_sdk.get()) {
return Err(BuildError::MaxSdkBelowMin { min: min_sdk, max }.into());
}
match (manifest.has_code(), self.dex.is_empty()) {
(true, true) => return Err(BuildError::MissingCode.into()),
(false, false) => return Err(BuildError::CodeDeclaredAbsent.into()),
_ => {}
}
if target_sdk >= ApiLevel::S
&& let Some(component) = manifest
.components()
.find(|c| c.element("intent-filter").is_some() && c.android_attr(attr::EXPORTED).is_none())
{
let name = component.android_attr(attr::NAME).map(ToString::to_string);
return Err(
BuildError::ExportedRequired { component: name.unwrap_or_else(|| component.name.clone()) }.into()
);
}
let storage = self.native_policy.storage;
if let Some(declared) = manifest.extract_native_libs().filter(|_| !self.native.is_empty())
&& declared != (storage == LibraryStorage::Extracted)
{
return Err(BuildError::ExtractNativeLibsConflict { manifest: declared, storage }.into());
}
let entry_library = match manifest.native_activity_library() {
None => None,
Some(Ok(library)) => Some(LibraryName::try_new(library)?),
Some(Err(value)) => return Err(BuildError::NativeLibraryNotLiteral { value: value.clone() }.into()),
};
Ok(App { package, min_sdk, target_sdk, entry_library })
}
fn check_native(&self, app: &App) -> Result<(), Error> {
if app.entry_library.is_some() {
if app.min_sdk < ApiLevel::GINGERBREAD {
return Err(BuildError::NativeActivityRequiresApi9.into());
}
if self.native.is_empty() {
return Err(BuildError::NoNativeLibraries.into());
}
}
let abis: BTreeSet<Abi> = self.native.iter().map(|n| n.abi).collect();
if self.native_policy.page_size == PageSize::FourKiB
&& app.target_sdk >= ApiLevel::VANILLA_ICE_CREAM
&& abis.iter().any(|a| a.is_64_bit())
{
return Err(NativeError::FourKibPages { target_sdk: app.target_sdk }.into());
}
if let Some(entry) = &app.entry_library
&& let Some(abi) =
abis.into_iter().find(|&abi| !self.native.iter().any(|n| n.abi == abi && &n.name == entry))
{
return Err(NativeError::MissingEntryLibrary { library: entry.clone(), abi }.into());
}
Ok(())
}
fn read_native<'a>(&self, input: &'a NativeInput, app: &App) -> Result<Cow<'a, [u8]>, Error> {
let policy = &self.native_policy;
let bytes = match &input.content {
Content::Bytes(bytes) => Cow::Borrowed(&bytes[..]),
Content::File(path) => Cow::Owned(std::fs::read(path).map_err(Error::file(FileOperation::Read, path))?),
};
let info = ElfInfo::parse(&bytes)?;
info.check(&input.name, input.abi, policy)?;
if app.entry_library.as_ref() == Some(&input.name) && !info.exports(NATIVE_ACTIVITY_ENTRY) {
return Err(
NativeError::MissingEntryPoint { library: input.name.clone(), symbol: NATIVE_ACTIVITY_ENTRY }.into()
);
}
if policy.check_dependencies {
let packaged =
|needed: &str| self.native.iter().any(|n| n.abi == input.abi && n.name.file_name() == needed);
for needed in info.needed.iter().filter(|needed| !packaged(needed)) {
policy.check_system_library(&input.name, input.abi, needed, app.min_sdk)?;
}
}
Ok(bytes)
}
}
struct App {
package: PackageName,
min_sdk: ApiLevel,
target_sdk: ApiLevel,
entry_library: Option<LibraryName>,
}
fn is_reserved(path: &ArchivePath) -> bool {
let name = path.as_str();
let dex = name.strip_prefix("classes").and_then(|rest| rest.strip_suffix(".dex"));
name == "AndroidManifest.xml"
|| name == "resources.arsc"
|| dex.is_some_and(|n| n.is_empty() || n.bytes().all(|b| b.is_ascii_digit()))
|| path.is_in("lib")
|| crate::archive::is_signature_file(name)
}
fn app_ref(ty: ResourceType, name: &str) -> crate::res::Value {
ResourceRef::app(ty, name).expect("built-in resource names are valid").into()
}