use nutype::nutype;
use super::is_java_identifier;
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum PackageNameError {
#[error("package name has {segments} segment(s); Android requires at least two")]
TooFewSegments { segments: usize },
#[error("package name segment {segment:?} is not an ASCII Java identifier without `$`")]
InvalidSegment { segment: String },
}
fn validate_package_name(value: &str) -> Result<(), PackageNameError> {
let segments = value.split('.').count();
if segments < 2 {
return Err(PackageNameError::TooFewSegments { segments });
}
match value.split('.').find(|s| !is_java_identifier(s) || s.contains('$')) {
Some(segment) => Err(PackageNameError::InvalidSegment { segment: segment.to_owned() }),
None => Ok(()),
}
}
#[nutype(
validate(with = validate_package_name, error = PackageNameError),
derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, AsRef, Deref, FromStr, TryFrom, Into)
)]
pub struct PackageName(String);
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum ClassNameError {
#[error("class name is empty")]
Empty,
#[error("class name segment {segment:?} is not an ASCII Java identifier")]
InvalidSegment { segment: String },
}
fn validate_class_name(value: &str) -> Result<(), ClassNameError> {
let body = value.strip_prefix('.').unwrap_or(value);
if body.is_empty() {
return Err(ClassNameError::Empty);
}
match body.split('.').find(|s| !is_java_identifier(s)) {
Some(segment) => Err(ClassNameError::InvalidSegment { segment: segment.to_owned() }),
None => Ok(()),
}
}
#[nutype(
validate(with = validate_class_name, error = ClassNameError),
derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, AsRef, Deref, FromStr, TryFrom, Into)
)]
pub struct ClassName(String);
impl ClassName {
pub fn native_activity() -> Self {
Self::try_new("android.app.NativeActivity").expect("valid class name")
}
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum PermissionNameError {
#[error("permission name has {segments} segment(s); at least two are required")]
TooFewSegments { segments: usize },
#[error("permission name segment {segment:?} is not an ASCII Java identifier")]
InvalidSegment { segment: String },
}
fn validate_permission_name(value: &str) -> Result<(), PermissionNameError> {
let segments = value.split('.').count();
if segments < 2 {
return Err(PermissionNameError::TooFewSegments { segments });
}
match value.split('.').find(|s| !is_java_identifier(s)) {
Some(segment) => Err(PermissionNameError::InvalidSegment { segment: segment.to_owned() }),
None => Ok(()),
}
}
#[nutype(
validate(with = validate_permission_name, error = PermissionNameError),
derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, AsRef, Deref, FromStr, TryFrom, Into)
)]
pub struct PermissionName(String);
impl PermissionName {
pub fn android(name: &str) -> Result<Self, PermissionNameError> {
Self::try_new(format!("android.permission.{name}"))
}
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum LibraryNameError {
#[error("library name is empty")]
Empty,
#[error("library name is {len} bytes long; the limit is {max}")]
TooLong { len: usize, max: usize },
#[error("library name {name:?} includes the `lib` prefix and `.so` suffix; pass only the stem")]
IncludesFileAffixes { name: String },
#[error("library name contains {character:?}; only ASCII letters, digits, `_`, `-`, `.`, and `+` are allowed")]
InvalidCharacter { character: char },
#[error("file name {file_name:?} does not have the form `lib<name>.so`")]
NotALibraryFileName { file_name: String },
}
const MAX_LIBRARY_NAME: usize = 200;
fn validate_library_name(value: &str) -> Result<(), LibraryNameError> {
if value.is_empty() {
return Err(LibraryNameError::Empty);
}
if value.len() > MAX_LIBRARY_NAME {
return Err(LibraryNameError::TooLong { len: value.len(), max: MAX_LIBRARY_NAME });
}
if value.starts_with("lib") && value.ends_with(".so") {
return Err(LibraryNameError::IncludesFileAffixes { name: value.to_owned() });
}
match value.chars().find(|&c| !(c.is_ascii_alphanumeric() || "_-.+".contains(c))) {
Some(character) => Err(LibraryNameError::InvalidCharacter { character }),
None => Ok(()),
}
}
#[nutype(
validate(with = validate_library_name, error = LibraryNameError),
derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, AsRef, Deref, FromStr, TryFrom, Into)
)]
pub struct LibraryName(String);
impl LibraryName {
pub fn from_file_name(file_name: &str) -> Result<Self, LibraryNameError> {
let stem = file_name.strip_prefix("lib").and_then(|rest| rest.strip_suffix(".so"));
let stem = stem.ok_or_else(|| LibraryNameError::NotALibraryFileName { file_name: file_name.to_owned() })?;
Self::try_new(stem)
}
pub fn file_name(&self) -> String {
format!("lib{}.so", self.as_ref())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn package_names() {
for valid in ["org.example", "a.b.c", "com.Example_1.app_"] {
assert!(PackageName::try_new(valid).is_ok(), "{valid}");
}
assert_eq!(PackageName::try_new("single"), Err(PackageNameError::TooFewSegments { segments: 1 }));
assert_eq!(
PackageName::try_new("org.1example"),
Err(PackageNameError::InvalidSegment { segment: "1example".into() })
);
for invalid in ["org..example", ".org.example", "org.ex-ample", "org.ex$ample", "org.example."] {
assert!(PackageName::try_new(invalid).is_err(), "{invalid}");
}
}
#[test]
fn class_names_accept_relative_names() {
assert!(ClassName::try_new(".MainActivity").is_ok());
assert!(ClassName::try_new("org.example.Main$Inner").is_ok());
assert_eq!(ClassName::try_new("."), Err(ClassNameError::Empty));
assert!(ClassName::try_new("org.example.").is_err());
}
#[test]
fn library_names_cover_ndk_conventions() {
assert_eq!(LibraryName::try_new("c++_shared").unwrap().file_name(), "libc++_shared.so");
assert_eq!(LibraryName::from_file_name("libmain.so").unwrap().as_ref(), "main");
assert_eq!(
LibraryName::try_new("libmain.so"),
Err(LibraryNameError::IncludesFileAffixes { name: "libmain.so".into() })
);
assert_eq!(LibraryName::try_new("a/b"), Err(LibraryNameError::InvalidCharacter { character: '/' }));
assert!(matches!(LibraryName::from_file_name("main.so"), Err(LibraryNameError::NotALibraryFileName { .. })));
}
}