rust-apk 1.0.0

Build, align, sign, verify, and inspect Android APKs in pure Rust
Documentation
//! Reading APKs produced by Android's own tools.

mod common;

use std::path::Path;
use std::process::Command;

use apk::prelude::*;
use apk::res::{EntryValue, ResourceTable, Value};
use apk::xml::Document;

#[test]
fn sdk_discovery_orders_versions_numerically_and_prefers_releases() {
    let dir = tempfile::tempdir().unwrap();
    for version in ["9.0.0", "35.0.0", "36.0.0-rc1", "36.0.0"] {
        let version = dir.path().join(version);
        std::fs::create_dir(&version).unwrap();
        std::fs::write(version.join("oracle"), b"").unwrap();
    }
    std::fs::create_dir(dir.path().join("37.0.0")).unwrap();
    assert_eq!(common::newest_sdk_entry(dir.path(), "oracle"), Some(dir.path().join("36.0.0/oracle")));
    std::fs::remove_file(dir.path().join("36.0.0/oracle")).unwrap();
    assert_eq!(common::newest_sdk_entry(dir.path(), "oracle"), Some(dir.path().join("36.0.0-rc1/oracle")));
}

/// Links a small app with `aapt2 compile` + `aapt2 link`.
fn link_with_aapt2(dir: &Path) -> Option<std::path::PathBuf> {
    let (aapt2, android_jar) = (common::tool("aapt2")?, common::android_jar()?);
    for values in ["res/values", "res/values-fr"] {
        std::fs::create_dir_all(dir.join(values)).unwrap();
    }
    std::fs::write(
        dir.join("res/values/strings.xml"),
        r#"<resources>
        <string name="app_name">Linked</string>
        <string name="greeting">Hello, %1$s!</string>
        <color name="accent">#ff336699</color>
        <dimen name="margin">16dp</dimen>
        <bool name="enabled">true</bool>
    </resources>"#,
    )
    .unwrap();
    std::fs::write(
        dir.join("res/values-fr/strings.xml"),
        r#"<resources><string name="app_name">Lié</string></resources>"#,
    )
    .unwrap();
    std::fs::write(dir.join("AndroidManifest.xml"), r#"<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.example.linked" android:versionCode="5" android:versionName="5.0">
        <uses-sdk android:minSdkVersion="24" android:targetSdkVersion="35"/>
        <uses-permission android:name="android.permission.VIBRATE"/>
        <application android:label="@string/app_name" android:hasCode="false">
            <activity android:name="android.app.NativeActivity" android:exported="true" android:screenOrientation="sensorLandscape" android:configChanges="orientation|screenSize">
                <meta-data android:name="android.app.lib_name" android:value="main"/>
                <intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
            </activity>
        </application>
    </manifest>"#).unwrap();
    let run = |args: &[&str]| {
        let output = Command::new(&aapt2).args(args).current_dir(dir).output().unwrap();
        assert!(output.status.success(), "aapt2 {args:?}: {}", String::from_utf8_lossy(&output.stderr));
    };
    run(&["compile", "--dir", "res", "-o", "compiled.zip"]);
    run(&[
        "link",
        "-I",
        android_jar.to_str().unwrap(),
        "--manifest",
        "AndroidManifest.xml",
        "-o",
        "linked.apk",
        "compiled.zip",
    ]);
    Some(dir.join("linked.apk"))
}

#[test]
fn reads_an_aapt2_linked_apk() {
    let dir = tempfile::tempdir().unwrap();
    let Some(path) = link_with_aapt2(dir.path()) else { return };
    let mut apk = Apk::open(&path).unwrap();

    let manifest = apk.manifest().unwrap();
    assert_eq!((manifest.package(), manifest.version_code()), (Some("org.example.linked"), Some(5)));
    assert_eq!(manifest.min_sdk().to_string(), "24");
    assert_eq!(manifest.permissions().collect::<Vec<_>>(), ["android.permission.VIBRATE"]);
    assert_eq!(manifest.native_activity_library(), Some(Ok("main")));
    assert_eq!(apk.label().unwrap().as_deref(), Some("Linked"));

    let printed = manifest.to_string();
    assert!(printed.contains(r#"android:screenOrientation="sensorLandscape""#), "{printed}");
    assert!(printed.contains(r#"android:configChanges="orientation|screenSize""#), "{printed}");

    let bytes = apk.read("resources.arsc").unwrap();
    let table = ResourceTable::parse(&bytes).unwrap();
    let id = |ty, name| table.find(0x7f, ty, name).unwrap_or_else(|| panic!("{ty}/{name} missing"));
    let value = |ty, name| match table.get(id(ty, name)).unwrap().default_value() {
        Some(EntryValue::Simple(value)) => value,
        other => panic!("{ty}/{name}: {other:?}"),
    };
    assert_eq!(value(ResourceType::String, "greeting"), Value::String("Hello, %1$s!".into()));
    assert_eq!(value(ResourceType::Bool, "enabled"), Value::Bool(true));
    assert_eq!(value(ResourceType::Dimen, "margin").to_string(), "16dp");
    assert_eq!(value(ResourceType::Color, "accent").to_string(), "#ff336699");
    let french = table.get(id(ResourceType::String, "app_name")).unwrap().value(&"fr".parse().unwrap());
    assert_eq!(french, Some(EntryValue::Simple(Value::String("Lié".into()))));
}

#[test]
fn aapt2_manifests_re_encode_losslessly() {
    let dir = tempfile::tempdir().unwrap();
    let Some(path) = link_with_aapt2(dir.path()) else { return };
    let manifest = Document::decode(&Apk::open(&path).unwrap().read("AndroidManifest.xml").unwrap()).unwrap();
    assert_eq!(Document::decode(&manifest.encode(&()).unwrap()).unwrap(), manifest);
}

#[test]
fn the_framework_resource_table_reads_completely() {
    let Some(android_jar) = common::android_jar() else { return };
    let bytes = Apk::open(android_jar).unwrap().read("resources.arsc").unwrap();
    let table = ResourceTable::parse(&bytes).unwrap();
    let mut resources = 0;
    for resource in table.resources() {
        resources += 1;
        assert!(!resource.name().is_empty(), "{} has no name", resource.id());
        resource.values().for_each(drop);
    }
    assert!(resources > 10_000, "only {resources} resources");
    let label = table.get(apk::android::attr::LABEL.id()).unwrap();
    assert_eq!((label.type_name(), label.name().as_ref()), ("attr", "label"));
    assert!(label.is_public());
    let theme = apk::android::find_style("Theme.Material").unwrap();
    assert_eq!(table.find(0x01, ResourceType::Style, "Theme.Material"), Some(theme));
}