use std::fmt;
use std::str::FromStr;
use nutype::nutype;
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ResourceId(pub u32);
impl ResourceId {
pub const APP_PACKAGE: u8 = 0x7f;
pub const ANDROID_PACKAGE: u8 = 0x01;
pub const fn new(package: u8, type_id: u8, entry: u16) -> Self {
Self(((package as u32) << 24) | ((type_id as u32) << 16) | entry as u32)
}
pub const fn package(self) -> u8 {
(self.0 >> 24) as u8
}
pub const fn type_id(self) -> u8 {
(self.0 >> 16) as u8
}
pub const fn entry(self) -> u16 {
self.0 as u16
}
}
impl fmt::Display for ResourceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "0x{:08x}", self.0)
}
}
impl fmt::Debug for ResourceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ResourceId(0x{:08x})", self.0)
}
}
macro_rules! resource_types {
($($variant:ident => $name:literal,)+) => {
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub enum ResourceType {
$($variant,)+
}
impl ResourceType {
pub const ALL: &[Self] = &[$(Self::$variant,)+];
pub const fn as_str(self) -> &'static str {
match self {
$(Self::$variant => $name,)+
}
}
}
};
}
resource_types! {
Anim => "anim",
Animator => "animator",
Array => "array",
Attr => "attr",
Bool => "bool",
Color => "color",
Dimen => "dimen",
Drawable => "drawable",
Font => "font",
Fraction => "fraction",
Id => "id",
Integer => "integer",
Interpolator => "interpolator",
Layout => "layout",
Menu => "menu",
Mipmap => "mipmap",
Navigation => "navigation",
Plurals => "plurals",
Raw => "raw",
String => "string",
Style => "style",
Transition => "transition",
Xml => "xml",
}
impl fmt::Display for ResourceType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[error("unknown resource type {name:?}")]
pub struct UnknownResourceType {
pub name: String,
}
impl FromStr for ResourceType {
type Err = UnknownResourceType;
fn from_str(name: &str) -> Result<Self, UnknownResourceType> {
Self::ALL
.iter()
.copied()
.find(|ty| ty.as_str() == name)
.ok_or_else(|| UnknownResourceType { name: name.to_owned() })
}
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum ResourceNameError {
#[error("resource name is empty")]
Empty,
#[error("resource name {name:?} must start with an ASCII letter or `_`")]
InvalidStart { name: String },
#[error("resource name contains {character:?}; only ASCII letters, digits, `_`, and `.` are allowed")]
InvalidCharacter { character: char },
#[error("resource name {name:?} ends with `.`")]
TrailingDot { name: String },
#[error("resource name is {len} bytes; resource tables store at most {max}", max = MAX_RESOURCE_NAME_LEN)]
TooLong { len: usize },
}
pub const MAX_RESOURCE_NAME_LEN: usize = 0x7fff;
fn validate_resource_name(name: &str) -> Result<(), ResourceNameError> {
if name.len() > MAX_RESOURCE_NAME_LEN {
return Err(ResourceNameError::TooLong { len: name.len() });
}
let first = name.chars().next().ok_or(ResourceNameError::Empty)?;
if !(first.is_ascii_alphabetic() || first == '_') {
return Err(ResourceNameError::InvalidStart { name: name.to_owned() });
}
if let Some(character) = name.chars().find(|&c| !(c.is_ascii_alphanumeric() || c == '_' || c == '.')) {
return Err(ResourceNameError::InvalidCharacter { character });
}
if name.ends_with('.') {
return Err(ResourceNameError::TrailingDot { name: name.to_owned() });
}
Ok(())
}
#[nutype(
validate(with = validate_resource_name, error = ResourceNameError),
derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, AsRef, Borrow, Deref, FromStr, TryFrom, Into)
)]
pub struct ResourceName(String);
impl ResourceName {
pub fn is_file_name(&self) -> bool {
self.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
}
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum ResourceRefError {
#[error("reference {text:?} must start with `@`")]
MissingSigil { text: String },
#[error("reference {text:?} has an invalid hexadecimal ID")]
InvalidId { text: String },
#[error("package {package:?} cannot be referenced by name; only `android` can")]
UnsupportedPackage { package: String },
#[error("reference {text:?} must have the form `type/name`")]
MissingType { text: String },
#[error(transparent)]
Type(#[from] UnknownResourceType),
#[error(transparent)]
Name(#[from] ResourceNameError),
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum RefPackage {
App,
Android,
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ResourceRef {
Id(ResourceId),
Named { package: RefPackage, ty: ResourceType, name: ResourceName },
}
impl ResourceRef {
pub fn app(ty: ResourceType, name: impl Into<String>) -> Result<Self, ResourceNameError> {
Ok(Self::Named { package: RefPackage::App, ty, name: ResourceName::try_new(name)? })
}
pub fn android(ty: ResourceType, name: impl Into<String>) -> Result<Self, ResourceNameError> {
Ok(Self::Named { package: RefPackage::Android, ty, name: ResourceName::try_new(name)? })
}
pub(crate) fn parse_body(body: &str, default_ty: Option<ResourceType>) -> Result<Self, ResourceRefError> {
if let Some(hex) = body.strip_prefix("0x") {
let id = u32::from_str_radix(hex, 16).map_err(|_| ResourceRefError::InvalidId { text: body.to_owned() })?;
return Ok(Self::Id(ResourceId(id)));
}
let (package, rest) = match body.split_once(':') {
Some(("android", rest)) => (RefPackage::Android, rest),
Some((package, _)) => return Err(ResourceRefError::UnsupportedPackage { package: package.to_owned() }),
None => (RefPackage::App, body),
};
let (ty, name) = match (rest.split_once('/'), default_ty) {
(Some((ty, name)), _) => (ty.parse()?, name),
(None, Some(ty)) => (ty, rest),
(None, None) => return Err(ResourceRefError::MissingType { text: body.to_owned() }),
};
Ok(Self::Named { package, ty, name: ResourceName::try_new(name)? })
}
}
impl From<ResourceId> for ResourceRef {
fn from(id: ResourceId) -> Self {
Self::Id(id)
}
}
impl FromStr for ResourceRef {
type Err = ResourceRefError;
fn from_str(text: &str) -> Result<Self, ResourceRefError> {
let body = text.strip_prefix('@').ok_or_else(|| ResourceRefError::MissingSigil { text: text.to_owned() })?;
Self::parse_body(body, None)
}
}
impl fmt::Display for ResourceRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Id(id) => write!(f, "@{id}"),
Self::Named { package: RefPackage::App, ty, name } => write!(f, "@{ty}/{name}"),
Self::Named { package: RefPackage::Android, ty, name } => write!(f, "@android:{ty}/{name}"),
}
}
}
pub trait ResourceLookup {
fn lookup(&self, ty: ResourceType, name: &ResourceName) -> Option<ResourceId>;
}
impl ResourceLookup for () {
fn lookup(&self, _: ResourceType, _: &ResourceName) -> Option<ResourceId> {
None
}
}
impl<T: ResourceLookup + ?Sized> ResourceLookup for &T {
fn lookup(&self, ty: ResourceType, name: &ResourceName) -> Option<ResourceId> {
(**self).lookup(ty, name)
}
}
pub fn resolve(reference: &ResourceRef, app: &impl ResourceLookup) -> Option<ResourceId> {
match reference {
ResourceRef::Id(id) => Some(*id),
ResourceRef::Named { package: RefPackage::App, ty, name } => app.lookup(*ty, name),
ResourceRef::Named { package: RefPackage::Android, .. } => crate::android::resolve(reference),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_and_prints_references() {
for text in ["@string/app_name", "@android:style/Theme.Material.NoActionBar", "@0x7f020001"] {
let r: ResourceRef = text.parse().unwrap();
assert_eq!(r.to_string(), text);
}
assert_eq!(
"string/app_name".parse::<ResourceRef>(),
Err(ResourceRefError::MissingSigil { text: "string/app_name".into() })
);
assert_eq!(
"@vendor:string/x".parse::<ResourceRef>(),
Err(ResourceRefError::UnsupportedPackage { package: "vendor".into() })
);
assert_eq!(
"@bogus/x".parse::<ResourceRef>(),
Err(ResourceRefError::Type(UnknownResourceType { name: "bogus".into() }))
);
assert!(matches!("@string/1x".parse::<ResourceRef>(), Err(ResourceRefError::Name(_))));
}
#[test]
fn resource_ids_unpack() {
let id = ResourceId::new(0x7f, 0x02, 0x0003);
assert_eq!((id.package(), id.type_id(), id.entry()), (0x7f, 2, 3));
assert_eq!(id.to_string(), "0x7f020003");
}
}