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