rust-apk 1.0.0

Build, align, sign, verify, and inspect Android APKs in pure Rust
Documentation
//! Generates `android::attr` and the framework lookup tables from
//! `src/android/framework.txt`, which `tools/gen-framework` extracts from an
//! SDK platform.

use std::collections::BTreeSet;
use std::path::Path;

use proc_macro2::TokenStream;
use quote::{format_ident, quote};

const SOURCE: &str = "src/android/framework.txt";

/// `attr <id> <name> <format> [manifest] [<symbol>=<value>...]`
struct Attr<'a> {
    id: u32,
    name: &'a str,
    format: u32,
    manifest: bool,
    symbols: Vec<(&'a str, u32)>,
}

/// `<type> <id> <name>`
struct Resource<'a> {
    ty: &'a str,
    id: u32,
    name: &'a str,
}

fn main() {
    println!("cargo::rerun-if-changed={SOURCE}");
    let text = std::fs::read_to_string(SOURCE).unwrap_or_else(|error| panic!("{SOURCE}: {error}"));
    let (api_level, mut attrs, mut resources) = parse(&text);
    attrs.sort_by_key(|attr| attr.name);
    resources.sort_by_key(|resource| (resource.ty, resource.name));
    assert!(attrs.windows(2).all(|w| w[0].name < w[1].name), "{SOURCE} names an attribute twice");
    assert!(
        resources.windows(2).all(|w| (w[0].ty, w[0].name) < (w[1].ty, w[1].name)),
        "{SOURCE} names a resource twice"
    );
    let ids: BTreeSet<u32> = attrs.iter().map(|a| a.id).chain(resources.iter().map(|r| r.id)).collect();
    assert_eq!(ids.len(), attrs.len() + resources.len(), "{SOURCE} uses a resource ID twice");

    let out = Path::new(&std::env::var_os("OUT_DIR").expect("cargo sets OUT_DIR")).join("framework.rs");
    std::fs::write(out, generate(api_level, &attrs, &resources).to_string()).expect("write the generated tables");
}

fn malformed(number: usize, line: &str) -> ! {
    panic!("{SOURCE}:{}: malformed line {line:?}", number + 1)
}

fn parse(text: &str) -> (u32, Vec<Attr<'_>>, Vec<Resource<'_>>) {
    let mut api_level = None;
    let mut attrs = Vec::new();
    let mut resources = Vec::new();
    for (number, line) in text.lines().enumerate() {
        let hex = |field: Option<&str>| {
            let value = field.and_then(|f| u32::from_str_radix(f.strip_prefix("0x")?, 16).ok());
            value.unwrap_or_else(|| malformed(number, line))
        };
        let mut fields = line.split_ascii_whitespace();
        match fields.next() {
            None => {}
            Some(comment) if comment.starts_with('#') => {}
            Some("api") => {
                api_level =
                    Some(fields.next().and_then(|level| level.parse().ok()).unwrap_or_else(|| malformed(number, line)));
            }
            Some("attr") => {
                let id = hex(fields.next());
                let name = fields.next().unwrap_or_else(|| malformed(number, line));
                let format = hex(fields.next());
                let mut attr = Attr { id, name, format, manifest: false, symbols: Vec::new() };
                for field in fields {
                    match field.split_once('=') {
                        Some((symbol, value)) => attr.symbols.push((symbol, hex(Some(value)))),
                        None if field == "manifest" => attr.manifest = true,
                        None => malformed(number, line),
                    }
                }
                attrs.push(attr);
            }
            Some(ty) => {
                let id = hex(fields.next());
                let name = fields.next().unwrap_or_else(|| malformed(number, line));
                if fields.next().is_some() {
                    malformed(number, line);
                }
                resources.push(Resource { ty, id, name });
            }
        }
    }
    (api_level.unwrap_or_else(|| panic!("{SOURCE} has no `api` line")), attrs, resources)
}

/// `SCREAMING_CASE` for a `camelCase` attribute name.
fn constant_name(name: &str) -> String {
    let mut out = String::with_capacity(name.len() + 8);
    for (i, c) in name.char_indices() {
        if c.is_ascii_uppercase() && i > 0 && !out.ends_with('_') {
            out.push('_');
        }
        out.push(c.to_ascii_uppercase());
    }
    out
}

/// The positions of `ids` in ascending ID order.
fn by_id(ids: impl Iterator<Item = u32>) -> Vec<u16> {
    let mut indices: Vec<(u32, u16)> = ids
        .enumerate()
        .map(|(index, id)| (id, u16::try_from(index).expect("framework index exceeds 16 bits")))
        .collect();
    indices.sort();
    indices.into_iter().map(|(_, index)| index).collect()
}

fn generate(api_level: u32, attrs: &[Attr<'_>], resources: &[Resource<'_>]) -> TokenStream {
    let attr_defs = attrs.iter().map(|Attr { id, name, format, symbols, .. }| {
        let symbols = symbols.iter().map(|(symbol, value)| quote!(Symbol::new(#symbol, #value)));
        quote!(AttrDef::new(#name, ResourceId(#id), AttrFormat::from_bits_retain(#format), &[#(#symbols),*]))
    });
    let attrs_by_id = by_id(attrs.iter().map(|attr| attr.id));
    let resource_tuples = resources.iter().map(|Resource { ty, id, name }| quote!((#ty, #name, ResourceId(#id))));
    let resources_by_id = by_id(resources.iter().map(|resource| resource.id));
    let (attr_count, resource_count) = (attrs.len(), resources.len());

    let mut constants = BTreeSet::new();
    let attr_constants = attrs.iter().enumerate().map(|(index, attr)| {
        let constant = constant_name(attr.name);
        assert!(constants.insert(constant.clone()), "attribute constant {constant} collides");
        let constant = format_ident!("{constant}");
        let usage = if attr.manifest { " (used in manifests)" } else { "" };
        let doc = format!("`android:{}`{usage}", attr.name);
        quote! {
            #[doc = #doc]
            pub static #constant: &AttrDef = &ATTRS[#index];
        }
    });

    quote! {
        /// The API level of the platform these resources come from.
        pub const PLATFORM_API_LEVEL: u32 = #api_level;

        /// Every public framework attribute, sorted by name.
        pub(super) static ATTRS: [AttrDef; #attr_count] = [#(#attr_defs),*];

        /// Indices into [`ATTRS`], ordered by resource ID.
        pub(super) static ATTRS_BY_ID: [u16; #attr_count] = [#(#attrs_by_id),*];

        /// Every other public framework resource as (type, name, ID), sorted by type and name.
        pub(super) static RESOURCES: [(&str, &str, ResourceId); #resource_count] = [#(#resource_tuples),*];

        /// Indices into [`RESOURCES`], ordered by resource ID.
        pub(super) static RESOURCES_BY_ID: [u16; #resource_count] = [#(#resources_by_id),*];

        /// Every public framework attribute, as `attr::SCREAMING_CASE`.
        pub mod attr {
            use super::{ATTRS, AttrDef};

            #(#attr_constants)*
        }
    }
}