rust-apk 1.0.0

Build, align, sign, verify, and inspect Android APKs in pure Rust
Documentation
//! Public resources of the Android framework (the `android:` namespace).
//!
//! The tables are generated at build time from `framework.txt`, which
//! `tools/gen-framework` extracts from an SDK platform's `android.jar`, so
//! attribute IDs, formats, and enum values match the platform exactly.
//!
//! ```
//! use apk::android::{self, attr};
//!
//! assert_eq!(attr::SCREEN_ORIENTATION.symbol("landscape"), Some(0));
//! assert_eq!(android::find_attr("label").unwrap().id().0, 0x0101_0001);
//! assert!(android::find_style("Theme.Material.NoActionBar").is_some());
//! assert!(android::find_resource(apk::res::ResourceType::Color, "white").is_some());
//! ```

/// The tables `build.rs` generates from `framework.txt`.
mod generated {
    use super::{AttrDef, AttrFormat, Symbol};
    use crate::res::ResourceId;

    include!(concat!(env!("OUT_DIR"), "/framework.rs"));
}

use std::fmt;

use bitflags::bitflags;

pub use generated::{PLATFORM_API_LEVEL, attr};

use crate::res::{RefPackage, ResourceId, ResourceRef, ResourceType, Value};

/// The XML namespace URI bound to the `android:` prefix.
pub const NAMESPACE: &str = "http://schemas.android.com/apk/res/android";

bitflags! {
    /// The value formats an attribute accepts (`<attr format="...">`).
    #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
    pub struct AttrFormat: u32 {
        const REFERENCE = 1 << 0;
        const STRING = 1 << 1;
        const INTEGER = 1 << 2;
        const BOOLEAN = 1 << 3;
        const COLOR = 1 << 4;
        const FLOAT = 1 << 5;
        const DIMENSION = 1 << 6;
        const FRACTION = 1 << 7;
        const ENUM = 1 << 16;
        const FLAGS = 1 << 17;
    }
}

impl AttrFormat {
    /// Every simple value format, used by attributes declared without a format.
    pub const ANY: Self = Self::from_bits_retain(0xffff);
}

/// A named value of an enum or flags attribute, e.g. `landscape = 0`.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Symbol {
    pub name: &'static str,
    pub value: u32,
}

impl Symbol {
    pub(crate) const fn new(name: &'static str, value: u32) -> Self {
        Self { name, value }
    }
}

/// A public framework attribute.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct AttrDef {
    name: &'static str,
    id: ResourceId,
    format: AttrFormat,
    symbols: &'static [Symbol],
}

impl AttrDef {
    pub(crate) const fn new(
        name: &'static str,
        id: ResourceId,
        format: AttrFormat,
        symbols: &'static [Symbol],
    ) -> Self {
        Self { name, id, format, symbols }
    }

    /// The attribute name without its `android:` prefix.
    pub const fn name(&self) -> &'static str {
        self.name
    }

    pub const fn id(&self) -> ResourceId {
        self.id
    }

    /// The accepted formats; [`AttrFormat::ANY`] when undeclared.
    pub fn format(&self) -> AttrFormat {
        if self.format.is_empty() { AttrFormat::ANY } else { self.format }
    }

    /// Whether `value` has a format this attribute accepts, as aapt2 would
    /// have typed it. References, `@null`, `@empty`, and values of types
    /// this crate does not interpret are resolved at runtime, so they are
    /// always accepted.
    ///
    /// ```
    /// use apk::android::attr;
    /// use apk::res::Value;
    ///
    /// assert!(attr::HAS_CODE.accepts(&Value::Bool(false)));
    /// assert!(!attr::HAS_CODE.accepts(&Value::String("false".into())));
    /// assert!(attr::SCREEN_ORIENTATION.accepts(&Value::Int(0)));
    /// ```
    pub fn accepts(&self, value: &Value) -> bool {
        let needed = match value {
            Value::String(_) => AttrFormat::STRING,
            Value::Bool(_) => AttrFormat::BOOLEAN,
            Value::Int(_) | Value::Hex(_) => AttrFormat::INTEGER | AttrFormat::ENUM | AttrFormat::FLAGS,
            Value::Float(_) => AttrFormat::FLOAT,
            Value::Dimension(_) => AttrFormat::DIMENSION,
            Value::Fraction(_) => AttrFormat::FRACTION,
            Value::Color(_) => AttrFormat::COLOR,
            Value::Reference(_) | Value::Attribute(_) | Value::Null | Value::Empty | Value::Raw { .. } => {
                return true;
            }
        };
        self.format().intersects(needed)
    }

    /// The enum or flag values this attribute defines.
    pub const fn symbols(&self) -> &'static [Symbol] {
        self.symbols
    }

    /// The value of the enum or flag named `name`.
    pub fn symbol(&self, name: &str) -> Option<u32> {
        self.symbols.iter().find(|s| s.name == name).map(|s| s.value)
    }

    /// The name of the enum value `value`.
    pub fn enum_name(&self, value: u32) -> Option<&'static str> {
        self.symbols.iter().find(|s| s.value == value).map(|s| s.name)
    }

    /// Names the flags set in `value`, if `value` is exactly a union of them.
    pub fn flag_names(&self, value: u32) -> Option<Vec<&'static str>> {
        let mut remaining = value;
        let mut names = Vec::new();
        // Prefer wide masks such as `stateAlwaysHidden` over their component bits.
        let mut symbols: Vec<_> = self.symbols.iter().filter(|s| s.value != 0).collect();
        symbols.sort_by_key(|s| std::cmp::Reverse(s.value.count_ones()));
        for symbol in symbols {
            if value & symbol.value == symbol.value && remaining & symbol.value != 0 {
                names.push(symbol.name);
                remaining &= !symbol.value;
            }
        }
        (remaining == 0).then_some(names)
    }
}

impl fmt::Display for AttrDef {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "android:{}", self.name)
    }
}

/// Looks up a public framework attribute by name (without `android:`).
pub fn find_attr(name: &str) -> Option<&'static AttrDef> {
    let attrs = &generated::ATTRS;
    attrs.binary_search_by(|attr| attr.name.cmp(name)).ok().map(|i| &attrs[i])
}

/// Looks up a public framework attribute by resource ID.
pub fn attr_by_id(id: ResourceId) -> Option<&'static AttrDef> {
    let by_id = &generated::ATTRS_BY_ID;
    let index = by_id.binary_search_by_key(&id, |&i| generated::ATTRS[usize::from(i)].id).ok()?;
    Some(&generated::ATTRS[usize::from(by_id[index])])
}

/// Looks up a public framework resource other than an attribute, such as
/// `(Style, "Theme.Material.NoActionBar")` or `(Drawable, "sym_def_app_icon")`.
pub fn find_resource(ty: ResourceType, name: &str) -> Option<ResourceId> {
    let resources = &generated::RESOURCES;
    let index = resources.binary_search_by(|(t, n, _)| (*t, *n).cmp(&(ty.as_str(), name))).ok()?;
    Some(resources[index].2)
}

/// Looks up a public framework style such as `Theme.Material.NoActionBar`.
pub fn find_style(name: &str) -> Option<ResourceId> {
    find_resource(ResourceType::Style, name)
}

/// The type and name of the public framework resource (other than an
/// attribute) with ID `id`.
pub fn resource_name(id: ResourceId) -> Option<(ResourceType, &'static str)> {
    let by_id = &generated::RESOURCES_BY_ID;
    let index = by_id.binary_search_by_key(&id, |&i| generated::RESOURCES[usize::from(i)].2).ok()?;
    let (ty, name, _) = generated::RESOURCES[usize::from(by_id[index])];
    Some((ty.parse().ok()?, name))
}

/// Resolves an `@android:type/name` reference to a public framework resource.
pub fn resolve(reference: &ResourceRef) -> Option<ResourceId> {
    match reference {
        ResourceRef::Id(id) => Some(*id),
        ResourceRef::Named { package: RefPackage::Android, ty: ResourceType::Attr, name } => {
            find_attr(name.as_ref()).map(AttrDef::id)
        }
        ResourceRef::Named { package: RefPackage::Android, ty, name } => find_resource(*ty, name.as_ref()),
        ResourceRef::Named { package: RefPackage::App, .. } => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn tables_are_sorted_for_binary_search() {
        assert!(generated::ATTRS.windows(2).all(|w| w[0].name < w[1].name));
        assert!(generated::RESOURCES.windows(2).all(|w| (w[0].0, w[0].1) < (w[1].0, w[1].1)));
        assert!(generated::RESOURCES.iter().all(|(ty, _, _)| ty.parse::<ResourceType>().is_ok()));
    }

    #[test]
    fn manifest_attributes_have_platform_ids_and_values() {
        assert_eq!(attr::LABEL.id(), ResourceId(0x0101_0001));
        assert_eq!(attr::NAME.id(), ResourceId(0x0101_0003));
        assert_eq!(attr::MIN_SDK_VERSION.id(), ResourceId(0x0101_020c));
        assert!(attr::EXPORTED.format().contains(AttrFormat::BOOLEAN));
        assert_eq!(attr::LAUNCH_MODE.symbol("singleTask"), Some(2));
        assert_eq!(attr::CONFIG_CHANGES.symbol("orientation"), Some(0x80));
        assert_eq!(attr::WINDOW_SOFT_INPUT_MODE.flag_names(0x11).unwrap(), ["adjustResize", "stateUnchanged"]);
    }

    #[test]
    fn resolves_framework_references() {
        let theme: ResourceRef = "@android:style/Theme.Material.NoActionBar".parse().unwrap();
        assert_eq!(resolve(&theme), find_style("Theme.Material.NoActionBar"));
        assert_eq!(resource_name(resolve(&theme).unwrap()), Some((ResourceType::Style, "Theme.Material.NoActionBar")));
        let icon: ResourceRef = "@android:drawable/sym_def_app_icon".parse().unwrap();
        assert_eq!(resolve(&icon), Some(ResourceId(0x0108_0093)));
        assert_eq!(attr_by_id(attr::LABEL.id()), Some(attr::LABEL));
        let app: ResourceRef = "@string/app_name".parse().unwrap();
        assert_eq!(resolve(&app), None);
    }
}