Skip to main content

apk/res/
id.rs

1//! Resource identifiers, names, and symbolic references.
2
3use std::fmt;
4use std::str::FromStr;
5
6use nutype::nutype;
7
8/// A packed resource identifier: `0xPPTTEEEE` (package, type, entry).
9#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
10pub struct ResourceId(pub u32);
11
12impl ResourceId {
13    /// The package ID of application resources.
14    pub const APP_PACKAGE: u8 = 0x7f;
15    /// The package ID of framework (`android:`) resources.
16    pub const ANDROID_PACKAGE: u8 = 0x01;
17
18    pub const fn new(package: u8, type_id: u8, entry: u16) -> Self {
19        Self(((package as u32) << 24) | ((type_id as u32) << 16) | entry as u32)
20    }
21
22    pub const fn package(self) -> u8 {
23        (self.0 >> 24) as u8
24    }
25
26    /// The 1-based type index within the package.
27    pub const fn type_id(self) -> u8 {
28        (self.0 >> 16) as u8
29    }
30
31    pub const fn entry(self) -> u16 {
32        self.0 as u16
33    }
34}
35
36impl fmt::Display for ResourceId {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        write!(f, "0x{:08x}", self.0)
39    }
40}
41
42impl fmt::Debug for ResourceId {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        write!(f, "ResourceId(0x{:08x})", self.0)
45    }
46}
47
48macro_rules! resource_types {
49    ($($variant:ident => $name:literal,)+) => {
50        /// An Android resource type, the `type` in `@type/name`.
51        #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
52        #[non_exhaustive]
53        pub enum ResourceType {
54            $($variant,)+
55        }
56
57        impl ResourceType {
58            pub const ALL: &[Self] = &[$(Self::$variant,)+];
59
60            pub const fn as_str(self) -> &'static str {
61                match self {
62                    $(Self::$variant => $name,)+
63                }
64            }
65        }
66    };
67}
68
69resource_types! {
70    Anim => "anim",
71    Animator => "animator",
72    Array => "array",
73    Attr => "attr",
74    Bool => "bool",
75    Color => "color",
76    Dimen => "dimen",
77    Drawable => "drawable",
78    Font => "font",
79    Fraction => "fraction",
80    Id => "id",
81    Integer => "integer",
82    Interpolator => "interpolator",
83    Layout => "layout",
84    Menu => "menu",
85    Mipmap => "mipmap",
86    Navigation => "navigation",
87    Plurals => "plurals",
88    Raw => "raw",
89    String => "string",
90    Style => "style",
91    Transition => "transition",
92    Xml => "xml",
93}
94
95impl fmt::Display for ResourceType {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        f.write_str(self.as_str())
98    }
99}
100
101/// A resource type name Android does not define.
102#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
103#[error("unknown resource type {name:?}")]
104pub struct UnknownResourceType {
105    pub name: String,
106}
107
108impl FromStr for ResourceType {
109    type Err = UnknownResourceType;
110    fn from_str(name: &str) -> Result<Self, UnknownResourceType> {
111        Self::ALL
112            .iter()
113            .copied()
114            .find(|ty| ty.as_str() == name)
115            .ok_or_else(|| UnknownResourceType { name: name.to_owned() })
116    }
117}
118
119/// Why a [`ResourceName`] was rejected.
120#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
121#[non_exhaustive]
122pub enum ResourceNameError {
123    #[error("resource name is empty")]
124    Empty,
125    #[error("resource name {name:?} must start with an ASCII letter or `_`")]
126    InvalidStart { name: String },
127    #[error("resource name contains {character:?}; only ASCII letters, digits, `_`, and `.` are allowed")]
128    InvalidCharacter { character: char },
129    #[error("resource name {name:?} ends with `.`")]
130    TrailingDot { name: String },
131    #[error("resource name is {len} bytes; resource tables store at most {max}", max = MAX_RESOURCE_NAME_LEN)]
132    TooLong { len: usize },
133}
134
135/// The longest resource name: the limit of a UTF-8 string pool entry.
136pub const MAX_RESOURCE_NAME_LEN: usize = 0x7fff;
137
138fn validate_resource_name(name: &str) -> Result<(), ResourceNameError> {
139    if name.len() > MAX_RESOURCE_NAME_LEN {
140        return Err(ResourceNameError::TooLong { len: name.len() });
141    }
142    let first = name.chars().next().ok_or(ResourceNameError::Empty)?;
143    if !(first.is_ascii_alphabetic() || first == '_') {
144        return Err(ResourceNameError::InvalidStart { name: name.to_owned() });
145    }
146    if let Some(character) = name.chars().find(|&c| !(c.is_ascii_alphanumeric() || c == '_' || c == '.')) {
147        return Err(ResourceNameError::InvalidCharacter { character });
148    }
149    if name.ends_with('.') {
150        return Err(ResourceNameError::TrailingDot { name: name.to_owned() });
151    }
152    Ok(())
153}
154
155/// A resource entry name such as `app_name` or `Theme.App`.
156#[nutype(
157    validate(with = validate_resource_name, error = ResourceNameError),
158    derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, AsRef, Borrow, Deref, FromStr, TryFrom, Into)
159)]
160pub struct ResourceName(String);
161
162impl ResourceName {
163    /// Whether this name is valid for a file resource (`res/<type>/<name>.<ext>`):
164    /// lowercase letters, digits, and `_`.
165    pub fn is_file_name(&self) -> bool {
166        self.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
167    }
168}
169
170/// Why a resource reference such as `@string/app_name` was rejected.
171#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
172#[non_exhaustive]
173pub enum ResourceRefError {
174    #[error("reference {text:?} must start with `@`")]
175    MissingSigil { text: String },
176    #[error("reference {text:?} has an invalid hexadecimal ID")]
177    InvalidId { text: String },
178    #[error("package {package:?} cannot be referenced by name; only `android` can")]
179    UnsupportedPackage { package: String },
180    #[error("reference {text:?} must have the form `type/name`")]
181    MissingType { text: String },
182    #[error(transparent)]
183    Type(#[from] UnknownResourceType),
184    #[error(transparent)]
185    Name(#[from] ResourceNameError),
186}
187
188/// The package that owns a named resource reference.
189#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
190pub enum RefPackage {
191    /// The application being built (`@type/name`).
192    App,
193    /// The Android framework (`@android:type/name`).
194    Android,
195}
196
197/// A reference to a resource, either by numeric ID or by name.
198///
199/// Parses and prints the XML notation: `@string/app_name`,
200/// `@android:style/Theme.Material`, or `@0x7f010000`.
201#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
202pub enum ResourceRef {
203    Id(ResourceId),
204    Named { package: RefPackage, ty: ResourceType, name: ResourceName },
205}
206
207impl ResourceRef {
208    /// `@<ty>/<name>` in the application package.
209    pub fn app(ty: ResourceType, name: impl Into<String>) -> Result<Self, ResourceNameError> {
210        Ok(Self::Named { package: RefPackage::App, ty, name: ResourceName::try_new(name)? })
211    }
212
213    /// `@android:<ty>/<name>`.
214    pub fn android(ty: ResourceType, name: impl Into<String>) -> Result<Self, ResourceNameError> {
215        Ok(Self::Named { package: RefPackage::Android, ty, name: ResourceName::try_new(name)? })
216    }
217
218    /// Parses the body of a reference after its `@` or `?` sigil. A body
219    /// without `type/` uses `default_ty` (`?android:windowFullscreen` means `attr`).
220    pub(crate) fn parse_body(body: &str, default_ty: Option<ResourceType>) -> Result<Self, ResourceRefError> {
221        if let Some(hex) = body.strip_prefix("0x") {
222            let id = u32::from_str_radix(hex, 16).map_err(|_| ResourceRefError::InvalidId { text: body.to_owned() })?;
223            return Ok(Self::Id(ResourceId(id)));
224        }
225        let (package, rest) = match body.split_once(':') {
226            Some(("android", rest)) => (RefPackage::Android, rest),
227            Some((package, _)) => return Err(ResourceRefError::UnsupportedPackage { package: package.to_owned() }),
228            None => (RefPackage::App, body),
229        };
230        let (ty, name) = match (rest.split_once('/'), default_ty) {
231            (Some((ty, name)), _) => (ty.parse()?, name),
232            (None, Some(ty)) => (ty, rest),
233            (None, None) => return Err(ResourceRefError::MissingType { text: body.to_owned() }),
234        };
235        Ok(Self::Named { package, ty, name: ResourceName::try_new(name)? })
236    }
237}
238
239impl From<ResourceId> for ResourceRef {
240    fn from(id: ResourceId) -> Self {
241        Self::Id(id)
242    }
243}
244
245impl FromStr for ResourceRef {
246    type Err = ResourceRefError;
247    fn from_str(text: &str) -> Result<Self, ResourceRefError> {
248        let body = text.strip_prefix('@').ok_or_else(|| ResourceRefError::MissingSigil { text: text.to_owned() })?;
249        Self::parse_body(body, None)
250    }
251}
252
253impl fmt::Display for ResourceRef {
254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255        match self {
256            Self::Id(id) => write!(f, "@{id}"),
257            Self::Named { package: RefPackage::App, ty, name } => write!(f, "@{ty}/{name}"),
258            Self::Named { package: RefPackage::Android, ty, name } => write!(f, "@android:{ty}/{name}"),
259        }
260    }
261}
262
263/// Maps named application resources (`@type/name`) to their IDs.
264///
265/// Implemented by [`ResourceTable`](super::ResourceTable) and by
266/// [`CompiledResources`](super::CompiledResources). `()` resolves nothing.
267pub trait ResourceLookup {
268    fn lookup(&self, ty: ResourceType, name: &ResourceName) -> Option<ResourceId>;
269}
270
271impl ResourceLookup for () {
272    fn lookup(&self, _: ResourceType, _: &ResourceName) -> Option<ResourceId> {
273        None
274    }
275}
276
277impl<T: ResourceLookup + ?Sized> ResourceLookup for &T {
278    fn lookup(&self, ty: ResourceType, name: &ResourceName) -> Option<ResourceId> {
279        (**self).lookup(ty, name)
280    }
281}
282
283/// Resolves `reference` against the framework and `app` resources.
284pub fn resolve(reference: &ResourceRef, app: &impl ResourceLookup) -> Option<ResourceId> {
285    match reference {
286        ResourceRef::Id(id) => Some(*id),
287        ResourceRef::Named { package: RefPackage::App, ty, name } => app.lookup(*ty, name),
288        ResourceRef::Named { package: RefPackage::Android, .. } => crate::android::resolve(reference),
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn parses_and_prints_references() {
298        for text in ["@string/app_name", "@android:style/Theme.Material.NoActionBar", "@0x7f020001"] {
299            let r: ResourceRef = text.parse().unwrap();
300            assert_eq!(r.to_string(), text);
301        }
302        assert_eq!(
303            "string/app_name".parse::<ResourceRef>(),
304            Err(ResourceRefError::MissingSigil { text: "string/app_name".into() })
305        );
306        assert_eq!(
307            "@vendor:string/x".parse::<ResourceRef>(),
308            Err(ResourceRefError::UnsupportedPackage { package: "vendor".into() })
309        );
310        assert_eq!(
311            "@bogus/x".parse::<ResourceRef>(),
312            Err(ResourceRefError::Type(UnknownResourceType { name: "bogus".into() }))
313        );
314        assert!(matches!("@string/1x".parse::<ResourceRef>(), Err(ResourceRefError::Name(_))));
315    }
316
317    #[test]
318    fn resource_ids_unpack() {
319        let id = ResourceId::new(0x7f, 0x02, 0x0003);
320        assert_eq!((id.package(), id.type_id(), id.entry()), (0x7f, 2, 3));
321        assert_eq!(id.to_string(), "0x7f020003");
322    }
323}