rust-apk 1.0.0

Build, align, sign, verify, and inspect Android APKs in pure Rust
Documentation
//! Shared fixtures and Android SDK oracles for integration tests.
//!
//! Oracle checks run when the tools are available: set `AAPT2` and
//! `APKSIGNER` to the build-tools executables and `ANDROID_JAR` to a
//! platform's `android.jar`, or `ANDROID_HOME` to an SDK (the newest
//! build-tools and platform are used). `apksigner` also needs Java.
//!
//! Without the tools, oracle checks are skipped. Set
//! `APK_RS_REQUIRE_ORACLES=1` (as CI does) to make a missing tool fail the
//! test instead.

#![allow(dead_code)]

use std::path::{Path, PathBuf};
use std::process::Command;

use apk::sign::SigningIdentity;

/// A NativeActivity library for arm64-v8a that exports `ANativeActivity_onCreate`.
pub const NATIVE_LIBRARY: &[u8] = include_bytes!("../fixtures/native/libpostcard.so");

/// A valid 1×1 RGBA PNG.
pub const PNG: &[u8] = &[
    0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00,
    0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0d, 0x49,
    0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x60, 0x60, 0x60, 0xf8, 0x0f, 0x00, 0x01, 0x04, 0x01, 0x00, 0x5f, 0xe5, 0xc3,
    0x4b, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
];

/// A test-only identity from `tests/fixtures/keys`.
pub fn identity(name: &str) -> SigningIdentity {
    SigningIdentity::from_pem_file(fixture(&format!("keys/{name}.pem"))).expect("test identity")
}

pub fn fixture(relative: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures").join(relative)
}

/// `found`, or a test failure if oracles are required.
fn oracle(found: Option<PathBuf>, variable: &str) -> Option<PathBuf> {
    let required = std::env::var_os("APK_RS_REQUIRE_ORACLES").is_some_and(|value| value != "0");
    assert!(
        found.is_some() || !required,
        "APK_RS_REQUIRE_ORACLES is set, but neither {variable} nor ANDROID_HOME locates it"
    );
    found
}

/// The newest entry of `<sdk>/<directory>` containing `file`.
fn from_sdk(directory: &str, file: &str) -> Option<PathBuf> {
    let sdk = std::env::var_os("ANDROID_HOME").or_else(|| std::env::var_os("ANDROID_SDK_ROOT"))?;
    newest_sdk_entry(&Path::new(&sdk).join(directory), file)
}

pub fn newest_sdk_entry(directory: &Path, file: &str) -> Option<PathBuf> {
    let mut versions: Vec<_> = std::fs::read_dir(directory).ok()?.flatten().map(|e| e.path()).collect();
    versions.sort_by_cached_key(|path| {
        let name = path.file_name().and_then(|s| s.to_str()).unwrap_or_default();
        let name = name.strip_prefix("android-").unwrap_or(name);
        let (version, preview) = name.split_once('-').unwrap_or((name, ""));
        let numbers = |text: &str| {
            text.split(|c: char| !c.is_ascii_digit()).filter_map(|s| s.parse::<u32>().ok()).collect::<Vec<_>>()
        };
        (numbers(version), preview.is_empty(), numbers(preview))
    });
    versions.into_iter().rev().map(|dir| dir.join(file)).find(|path| path.is_file())
}

/// An Android SDK build tool, if one is configured.
pub fn tool(name: &str) -> Option<PathBuf> {
    let variable = name.to_ascii_uppercase();
    let file = if cfg!(windows) {
        format!("{name}.{}", if name == "apksigner" { "bat" } else { "exe" })
    } else {
        name.to_owned()
    };
    let found = std::env::var_os(&variable).map(PathBuf::from).or_else(|| from_sdk("build-tools", &file));
    oracle(found, &variable)
}

/// The output of a tool run.
pub struct Run {
    pub success: bool,
    pub stdout: String,
    pub stderr: String,
}

impl Run {
    pub fn expect_success(self, what: &str) -> Self {
        assert!(self.success, "{what} failed:\n{}\n{}", self.stdout, self.stderr);
        self
    }
}

fn run(tool: &Path, args: &[&str], apk: &Path) -> Run {
    let output = Command::new(tool).args(args).arg(apk).output().expect("run SDK tool");
    Run {
        success: output.status.success(),
        stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
        stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
    }
}

/// `aapt2 <args> <apk>`, or `None` without aapt2.
pub fn aapt2(args: &[&str], apk: &Path) -> Option<Run> {
    Some(run(&tool("aapt2")?, args, apk))
}

/// `apksigner verify --verbose --min-sdk-version <min> <apk>`, or `None` without apksigner.
pub fn apksigner_verify(apk: &Path, min_sdk: u32) -> Option<Run> {
    let min_sdk = min_sdk.to_string();
    Some(run(&tool("apksigner")?, &["verify", "--verbose", "--min-sdk-version", &min_sdk], apk))
}

/// `apksigner sign` with `identity`, in place, or `None` without apksigner.
pub fn apksigner_sign(apk: &Path, identity: &SigningIdentity) -> Option<Run> {
    let apksigner = tool("apksigner")?;
    // apksigner reads keys as PKCS#8 and certificates as X.509, both DER.
    let dir = tempfile::tempdir().expect("temporary directory");
    let (key, certificate) = (dir.path().join("key.pk8"), dir.path().join("certificate.der"));
    std::fs::write(&key, identity.key().to_pkcs8_der().expect("encodable key")).expect("write key");
    std::fs::write(&certificate, identity.certificate().der()).expect("write certificate");
    let (key, certificate) = (key.to_str().expect("UTF-8 path"), certificate.to_str().expect("UTF-8 path"));
    let args = ["sign", "--key", key, "--cert", certificate, "--v4-signing-enabled", "false"];
    Some(run(&apksigner, &args, apk))
}

/// The newest platform `android.jar`: `ANDROID_JAR`, or from `ANDROID_HOME`.
pub fn android_jar() -> Option<PathBuf> {
    let found = std::env::var_os("ANDROID_JAR").map(PathBuf::from).or_else(|| from_sdk("platforms", "android.jar"));
    oracle(found, "ANDROID_JAR")
}