1use nutype::nutype;
2
3use super::is_java_identifier;
4
5#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
7#[non_exhaustive]
8pub enum PackageNameError {
9 #[error("package name has {segments} segment(s); Android requires at least two")]
10 TooFewSegments { segments: usize },
11 #[error("package name segment {segment:?} is not an ASCII Java identifier without `$`")]
12 InvalidSegment { segment: String },
13}
14
15fn validate_package_name(value: &str) -> Result<(), PackageNameError> {
16 let segments = value.split('.').count();
17 if segments < 2 {
18 return Err(PackageNameError::TooFewSegments { segments });
19 }
20 match value.split('.').find(|s| !is_java_identifier(s) || s.contains('$')) {
21 Some(segment) => Err(PackageNameError::InvalidSegment { segment: segment.to_owned() }),
22 None => Ok(()),
23 }
24}
25
26#[nutype(
28 validate(with = validate_package_name, error = PackageNameError),
29 derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, AsRef, Deref, FromStr, TryFrom, Into)
30)]
31pub struct PackageName(String);
32
33#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
35#[non_exhaustive]
36pub enum ClassNameError {
37 #[error("class name is empty")]
38 Empty,
39 #[error("class name segment {segment:?} is not an ASCII Java identifier")]
40 InvalidSegment { segment: String },
41}
42
43fn validate_class_name(value: &str) -> Result<(), ClassNameError> {
44 let body = value.strip_prefix('.').unwrap_or(value);
45 if body.is_empty() {
46 return Err(ClassNameError::Empty);
47 }
48 match body.split('.').find(|s| !is_java_identifier(s)) {
49 Some(segment) => Err(ClassNameError::InvalidSegment { segment: segment.to_owned() }),
50 None => Ok(()),
51 }
52}
53
54#[nutype(
59 validate(with = validate_class_name, error = ClassNameError),
60 derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, AsRef, Deref, FromStr, TryFrom, Into)
61)]
62pub struct ClassName(String);
63
64impl ClassName {
65 pub fn native_activity() -> Self {
67 Self::try_new("android.app.NativeActivity").expect("valid class name")
68 }
69}
70
71#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
73#[non_exhaustive]
74pub enum PermissionNameError {
75 #[error("permission name has {segments} segment(s); at least two are required")]
76 TooFewSegments { segments: usize },
77 #[error("permission name segment {segment:?} is not an ASCII Java identifier")]
78 InvalidSegment { segment: String },
79}
80
81fn validate_permission_name(value: &str) -> Result<(), PermissionNameError> {
82 let segments = value.split('.').count();
83 if segments < 2 {
84 return Err(PermissionNameError::TooFewSegments { segments });
85 }
86 match value.split('.').find(|s| !is_java_identifier(s)) {
87 Some(segment) => Err(PermissionNameError::InvalidSegment { segment: segment.to_owned() }),
88 None => Ok(()),
89 }
90}
91
92#[nutype(
94 validate(with = validate_permission_name, error = PermissionNameError),
95 derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, AsRef, Deref, FromStr, TryFrom, Into)
96)]
97pub struct PermissionName(String);
98
99impl PermissionName {
100 pub fn android(name: &str) -> Result<Self, PermissionNameError> {
102 Self::try_new(format!("android.permission.{name}"))
103 }
104}
105
106#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
108#[non_exhaustive]
109pub enum LibraryNameError {
110 #[error("library name is empty")]
111 Empty,
112 #[error("library name is {len} bytes long; the limit is {max}")]
113 TooLong { len: usize, max: usize },
114 #[error("library name {name:?} includes the `lib` prefix and `.so` suffix; pass only the stem")]
115 IncludesFileAffixes { name: String },
116 #[error("library name contains {character:?}; only ASCII letters, digits, `_`, `-`, `.`, and `+` are allowed")]
117 InvalidCharacter { character: char },
118 #[error("file name {file_name:?} does not have the form `lib<name>.so`")]
119 NotALibraryFileName { file_name: String },
120}
121
122const MAX_LIBRARY_NAME: usize = 200;
123
124fn validate_library_name(value: &str) -> Result<(), LibraryNameError> {
125 if value.is_empty() {
126 return Err(LibraryNameError::Empty);
127 }
128 if value.len() > MAX_LIBRARY_NAME {
129 return Err(LibraryNameError::TooLong { len: value.len(), max: MAX_LIBRARY_NAME });
130 }
131 if value.starts_with("lib") && value.ends_with(".so") {
132 return Err(LibraryNameError::IncludesFileAffixes { name: value.to_owned() });
133 }
134 match value.chars().find(|&c| !(c.is_ascii_alphanumeric() || "_-.+".contains(c))) {
135 Some(character) => Err(LibraryNameError::InvalidCharacter { character }),
136 None => Ok(()),
137 }
138}
139
140#[nutype(
143 validate(with = validate_library_name, error = LibraryNameError),
144 derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Display, AsRef, Deref, FromStr, TryFrom, Into)
145)]
146pub struct LibraryName(String);
147
148impl LibraryName {
149 pub fn from_file_name(file_name: &str) -> Result<Self, LibraryNameError> {
151 let stem = file_name.strip_prefix("lib").and_then(|rest| rest.strip_suffix(".so"));
152 let stem = stem.ok_or_else(|| LibraryNameError::NotALibraryFileName { file_name: file_name.to_owned() })?;
153 Self::try_new(stem)
154 }
155
156 pub fn file_name(&self) -> String {
158 format!("lib{}.so", self.as_ref())
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn package_names() {
168 for valid in ["org.example", "a.b.c", "com.Example_1.app_"] {
169 assert!(PackageName::try_new(valid).is_ok(), "{valid}");
170 }
171 assert_eq!(PackageName::try_new("single"), Err(PackageNameError::TooFewSegments { segments: 1 }));
172 assert_eq!(
173 PackageName::try_new("org.1example"),
174 Err(PackageNameError::InvalidSegment { segment: "1example".into() })
175 );
176 for invalid in ["org..example", ".org.example", "org.ex-ample", "org.ex$ample", "org.example."] {
177 assert!(PackageName::try_new(invalid).is_err(), "{invalid}");
178 }
179 }
180
181 #[test]
182 fn class_names_accept_relative_names() {
183 assert!(ClassName::try_new(".MainActivity").is_ok());
184 assert!(ClassName::try_new("org.example.Main$Inner").is_ok());
185 assert_eq!(ClassName::try_new("."), Err(ClassNameError::Empty));
186 assert!(ClassName::try_new("org.example.").is_err());
187 }
188
189 #[test]
190 fn library_names_cover_ndk_conventions() {
191 assert_eq!(LibraryName::try_new("c++_shared").unwrap().file_name(), "libc++_shared.so");
192 assert_eq!(LibraryName::from_file_name("libmain.so").unwrap().as_ref(), "main");
193 assert_eq!(
194 LibraryName::try_new("libmain.so"),
195 Err(LibraryNameError::IncludesFileAffixes { name: "libmain.so".into() })
196 );
197 assert_eq!(LibraryName::try_new("a/b"), Err(LibraryNameError::InvalidCharacter { character: '/' }));
198 assert!(matches!(LibraryName::from_file_name("main.so"), Err(LibraryNameError::NotALibraryFileName { .. })));
199 }
200}