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")));
}
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));
}