1use std::fmt;
4use std::str::FromStr;
5
6use nutype::nutype;
7
8#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
10pub struct ResourceId(pub u32);
11
12impl ResourceId {
13 pub const APP_PACKAGE: u8 = 0x7f;
15 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 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 #[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#[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#[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
135pub 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#[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 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#[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
190pub enum RefPackage {
191 App,
193 Android,
195}
196
197#[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 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 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 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
263pub 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
283pub 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}