tauri_utils/acl/
identifier.rs1use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use std::num::NonZeroU8;
9use thiserror::Error;
10
11const IDENTIFIER_SEPARATOR: u8 = b':';
12const PLUGIN_PREFIX: &str = "tauri-plugin-";
13const CORE_PLUGIN_IDENTIFIER_PREFIX: &str = "core:";
14
15const MAX_LEN_PREFIX: usize = 64 - PLUGIN_PREFIX.len();
17const MAX_LEN_BASE: usize = 64;
18const MAX_LEN_IDENTIFIER: usize = MAX_LEN_PREFIX + 1 + MAX_LEN_BASE;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct Identifier {
23 inner: String,
24 separator: Option<NonZeroU8>,
25}
26
27#[cfg(feature = "schema")]
28impl schemars::JsonSchema for Identifier {
29 fn schema_name() -> String {
30 "Identifier".to_string()
31 }
32
33 fn schema_id() -> std::borrow::Cow<'static, str> {
34 std::borrow::Cow::Borrowed(concat!(module_path!(), "::Identifier"))
36 }
37
38 fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
39 String::json_schema(gen)
40 }
41}
42
43impl AsRef<str> for Identifier {
44 #[inline(always)]
45 fn as_ref(&self) -> &str {
46 &self.inner
47 }
48}
49
50impl Identifier {
51 #[inline(always)]
53 pub fn get(&self) -> &str {
54 self.as_ref()
55 }
56
57 pub fn get_base(&self) -> &str {
59 match self.separator_index() {
60 None => self.get(),
61 Some(i) => &self.inner[i + 1..],
62 }
63 }
64
65 pub fn get_prefix(&self) -> Option<&str> {
67 self.separator_index().map(|i| &self.inner[0..i])
68 }
69
70 pub fn set_prefix(&mut self) -> Result<(), ParseIdentifierError> {
72 todo!()
73 }
74
75 pub fn into_inner(self) -> (String, Option<NonZeroU8>) {
77 (self.inner, self.separator)
78 }
79
80 fn separator_index(&self) -> Option<usize> {
81 self.separator.map(|i| i.get() as usize)
82 }
83}
84
85#[derive(Debug)]
86enum ValidByte {
87 Separator,
88 Byte(u8),
89}
90
91impl ValidByte {
92 fn alpha_numeric(byte: u8) -> Option<Self> {
93 byte.is_ascii_alphanumeric().then_some(Self::Byte(byte))
94 }
95
96 fn alpha_numeric_hyphen(byte: u8) -> Option<Self> {
97 (byte.is_ascii_alphanumeric() || byte == b'-').then_some(Self::Byte(byte))
98 }
99
100 fn next(&self, next: u8) -> Option<ValidByte> {
101 match (self, next) {
102 (ValidByte::Byte(b'-'), IDENTIFIER_SEPARATOR) => None,
103 (ValidByte::Separator, b'-') => None,
104
105 (_, IDENTIFIER_SEPARATOR) => Some(ValidByte::Separator),
106 (ValidByte::Separator, next) => ValidByte::alpha_numeric(next),
107 (ValidByte::Byte(b'-'), next) => ValidByte::alpha_numeric_hyphen(next),
108 (ValidByte::Byte(b'_'), next) => ValidByte::alpha_numeric_hyphen(next),
109 (ValidByte::Byte(_), next) => ValidByte::alpha_numeric_hyphen(next),
110 }
111 }
112}
113
114#[derive(Debug, Error)]
116pub enum ParseIdentifierError {
117 #[error("identifiers cannot start with {}", PLUGIN_PREFIX)]
119 StartsWithTauriPlugin,
120
121 #[error("identifiers cannot be empty")]
123 Empty,
124
125 #[error("identifiers cannot be longer than {len}, found {0}", len = MAX_LEN_IDENTIFIER)]
127 Humongous(usize),
128
129 #[error("identifiers can only include lowercase ASCII, hyphens which are not leading or trailing, and a single colon if using a prefix")]
131 InvalidFormat,
132
133 #[error(
135 "identifiers can only include a single separator '{}'",
136 IDENTIFIER_SEPARATOR
137 )]
138 MultipleSeparators,
139
140 #[error("identifiers cannot have a trailing hyphen")]
142 TrailingHyphen,
143
144 #[error("identifiers cannot have a prefix without a base")]
146 PrefixWithoutBase,
147}
148
149impl TryFrom<String> for Identifier {
150 type Error = ParseIdentifierError;
151
152 fn try_from(value: String) -> Result<Self, Self::Error> {
153 if value.starts_with(PLUGIN_PREFIX) {
154 return Err(Self::Error::StartsWithTauriPlugin);
155 }
156
157 if value.is_empty() {
158 return Err(Self::Error::Empty);
159 }
160
161 if value.len() > MAX_LEN_IDENTIFIER {
162 return Err(Self::Error::Humongous(value.len()));
163 }
164
165 let is_core_identifier = value.starts_with(CORE_PLUGIN_IDENTIFIER_PREFIX);
166
167 let mut bytes = value.bytes();
168
169 let mut prev = bytes
171 .next()
172 .and_then(ValidByte::alpha_numeric)
173 .ok_or(Self::Error::InvalidFormat)?;
174
175 let mut idx = 0;
176 let mut separator = None;
177 for byte in bytes {
178 idx += 1; match prev.next(byte) {
180 None => return Err(Self::Error::InvalidFormat),
181 Some(next @ ValidByte::Byte(_)) => prev = next,
182 Some(ValidByte::Separator) => {
183 if separator.is_none() || is_core_identifier {
184 separator = Some(idx.try_into().unwrap());
186 prev = ValidByte::Separator
187 } else {
188 return Err(Self::Error::MultipleSeparators);
189 }
190 }
191 }
192 }
193
194 match prev {
195 ValidByte::Separator => return Err(Self::Error::PrefixWithoutBase),
197
198 ValidByte::Byte(b'-') => return Err(Self::Error::TrailingHyphen),
200
201 _ => (),
202 }
203
204 Ok(Self {
205 inner: value,
206 separator,
207 })
208 }
209}
210
211impl<'de> Deserialize<'de> for Identifier {
212 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
213 where
214 D: Deserializer<'de>,
215 {
216 Self::try_from(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
217 }
218}
219
220impl Serialize for Identifier {
221 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
222 where
223 S: Serializer,
224 {
225 serializer.serialize_str(self.get())
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 fn ident(s: impl Into<String>) -> Result<Identifier, ParseIdentifierError> {
234 Identifier::try_from(s.into())
235 }
236
237 #[test]
238 fn max_len_fits_in_u8() {
239 assert!(MAX_LEN_IDENTIFIER < u8::MAX as usize)
240 }
241
242 #[test]
243 fn format() {
244 assert!(ident("prefix:base").is_ok());
245 assert!(ident("prefix3:base").is_ok());
246 assert!(ident("preFix:base").is_ok());
247
248 assert!(ident("tauri-plugin-prefix:base").is_err());
250
251 assert!(ident("-prefix-:-base-").is_err());
252 assert!(ident("-prefix:base").is_err());
253 assert!(ident("prefix-:base").is_err());
254 assert!(ident("prefix:-base").is_err());
255 assert!(ident("prefix:base-").is_err());
256
257 assert!(ident("pre--fix:base--sep").is_err());
258 assert!(ident("prefix:base--sep").is_err());
259 assert!(ident("pre--fix:base").is_err());
260
261 assert!(ident("prefix::base").is_err());
262 assert!(ident(":base").is_err());
263 assert!(ident("prefix:").is_err());
264 assert!(ident(":prefix:base:").is_err());
265 assert!(ident("base:").is_err());
266
267 assert!(ident("").is_err());
268 assert!(ident("💩").is_err());
269
270 assert!(ident("a".repeat(MAX_LEN_IDENTIFIER + 1)).is_err());
271 }
272
273 #[test]
274 fn base() {
275 assert_eq!(ident("prefix:base").unwrap().get_base(), "base");
276 assert_eq!(ident("base").unwrap().get_base(), "base");
277 }
278
279 #[test]
280 fn prefix() {
281 assert_eq!(ident("prefix:base").unwrap().get_prefix(), Some("prefix"));
282 assert_eq!(ident("base").unwrap().get_prefix(), None);
283 }
284}
285
286#[cfg(feature = "build")]
287mod build {
288 use proc_macro2::TokenStream;
289 use quote::{quote, ToTokens, TokenStreamExt};
290
291 use super::*;
292
293 impl ToTokens for Identifier {
294 fn to_tokens(&self, tokens: &mut TokenStream) {
295 let s = self.get();
296 tokens
297 .append_all(quote! { ::tauri::utils::acl::Identifier::try_from(#s.to_string()).unwrap() })
298 }
299 }
300}