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(next),
108 (ValidByte::Byte(_), next) => ValidByte::alpha_numeric_hyphen(next),
109 }
110 }
111}
112
113#[derive(Debug, Error)]
115pub enum ParseIdentifierError {
116 #[error("identifiers cannot start with {}", PLUGIN_PREFIX)]
118 StartsWithTauriPlugin,
119
120 #[error("identifiers cannot be empty")]
122 Empty,
123
124 #[error("identifiers cannot be longer than {len}, found {0}", len = MAX_LEN_IDENTIFIER)]
126 Humongous(usize),
127
128 #[error("identifiers can only include lowercase ASCII, hyphens which are not leading or trailing, and a single colon if using a prefix")]
130 InvalidFormat,
131
132 #[error(
134 "identifiers can only include a single separator '{}'",
135 IDENTIFIER_SEPARATOR
136 )]
137 MultipleSeparators,
138
139 #[error("identifiers cannot have a trailing hyphen")]
141 TrailingHyphen,
142
143 #[error("identifiers cannot have a prefix without a base")]
145 PrefixWithoutBase,
146}
147
148impl TryFrom<String> for Identifier {
149 type Error = ParseIdentifierError;
150
151 fn try_from(value: String) -> Result<Self, Self::Error> {
152 if value.starts_with(PLUGIN_PREFIX) {
153 return Err(Self::Error::StartsWithTauriPlugin);
154 }
155
156 if value.is_empty() {
157 return Err(Self::Error::Empty);
158 }
159
160 if value.len() > MAX_LEN_IDENTIFIER {
161 return Err(Self::Error::Humongous(value.len()));
162 }
163
164 let is_core_identifier = value.starts_with(CORE_PLUGIN_IDENTIFIER_PREFIX);
165
166 let mut bytes = value.bytes();
167
168 let mut prev = bytes
170 .next()
171 .and_then(ValidByte::alpha_numeric)
172 .ok_or(Self::Error::InvalidFormat)?;
173
174 let mut idx = 0;
175 let mut separator = None;
176 for byte in bytes {
177 idx += 1; match prev.next(byte) {
179 None => return Err(Self::Error::InvalidFormat),
180 Some(next @ ValidByte::Byte(_)) => prev = next,
181 Some(ValidByte::Separator) => {
182 if separator.is_none() || is_core_identifier {
183 separator = Some(idx.try_into().unwrap());
185 prev = ValidByte::Separator
186 } else {
187 return Err(Self::Error::MultipleSeparators);
188 }
189 }
190 }
191 }
192
193 match prev {
194 ValidByte::Separator => return Err(Self::Error::PrefixWithoutBase),
196
197 ValidByte::Byte(b'-') => return Err(Self::Error::TrailingHyphen),
199
200 _ => (),
201 }
202
203 Ok(Self {
204 inner: value,
205 separator,
206 })
207 }
208}
209
210impl<'de> Deserialize<'de> for Identifier {
211 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
212 where
213 D: Deserializer<'de>,
214 {
215 Self::try_from(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
216 }
217}
218
219impl Serialize for Identifier {
220 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
221 where
222 S: Serializer,
223 {
224 serializer.serialize_str(self.get())
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 fn ident(s: impl Into<String>) -> Result<Identifier, ParseIdentifierError> {
233 Identifier::try_from(s.into())
234 }
235
236 #[test]
237 fn max_len_fits_in_u8() {
238 assert!(MAX_LEN_IDENTIFIER < u8::MAX as usize)
239 }
240
241 #[test]
242 fn format() {
243 assert!(ident("prefix:base").is_ok());
244 assert!(ident("prefix3:base").is_ok());
245 assert!(ident("preFix:base").is_ok());
246
247 assert!(ident("tauri-plugin-prefix:base").is_err());
249
250 assert!(ident("-prefix-:-base-").is_err());
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
256 assert!(ident("pre--fix:base--sep").is_err());
257 assert!(ident("prefix:base--sep").is_err());
258 assert!(ident("pre--fix:base").is_err());
259
260 assert!(ident("prefix::base").is_err());
261 assert!(ident(":base").is_err());
262 assert!(ident("prefix:").is_err());
263 assert!(ident(":prefix:base:").is_err());
264 assert!(ident("base:").is_err());
265
266 assert!(ident("").is_err());
267 assert!(ident("💩").is_err());
268
269 assert!(ident("a".repeat(MAX_LEN_IDENTIFIER + 1)).is_err());
270 }
271
272 #[test]
273 fn base() {
274 assert_eq!(ident("prefix:base").unwrap().get_base(), "base");
275 assert_eq!(ident("base").unwrap().get_base(), "base");
276 }
277
278 #[test]
279 fn prefix() {
280 assert_eq!(ident("prefix:base").unwrap().get_prefix(), Some("prefix"));
281 assert_eq!(ident("base").unwrap().get_prefix(), None);
282 }
283}
284
285#[cfg(feature = "build")]
286mod build {
287 use proc_macro2::TokenStream;
288 use quote::{quote, ToTokens, TokenStreamExt};
289
290 use super::*;
291
292 impl ToTokens for Identifier {
293 fn to_tokens(&self, tokens: &mut TokenStream) {
294 let s = self.get();
295 tokens
296 .append_all(quote! { ::tauri::utils::acl::Identifier::try_from(#s.to_string()).unwrap() })
297 }
298 }
299}