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