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() -> std::borrow::Cow<'static, str> {
33 "Identifier".into()
34 }
35
36 fn schema_id() -> std::borrow::Cow<'static, str> {
37 std::borrow::Cow::Borrowed(concat!(module_path!(), "::Identifier"))
38 }
39
40 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
41 String::json_schema(generator)
42 }
43}
44
45impl AsRef<str> for Identifier {
46 #[inline(always)]
47 fn as_ref(&self) -> &str {
48 &self.inner
49 }
50}
51
52impl Identifier {
53 #[inline(always)]
55 pub fn get(&self) -> &str {
56 self.as_ref()
57 }
58
59 pub fn get_base(&self) -> &str {
61 match self.separator_index() {
62 None => self.get(),
63 Some(i) => &self.inner[i + 1..],
64 }
65 }
66
67 pub fn get_prefix(&self) -> Option<&str> {
69 self.separator_index().map(|i| &self.inner[0..i])
70 }
71
72 pub fn set_prefix(&mut self) -> Result<(), ParseIdentifierError> {
74 todo!()
75 }
76
77 pub fn into_inner(self) -> (String, Option<NonZeroU8>) {
79 (self.inner, self.separator)
80 }
81
82 fn separator_index(&self) -> Option<usize> {
83 self.separator.map(|i| i.get() as usize)
84 }
85}
86
87#[derive(Debug)]
88enum ValidByte {
89 Separator,
90 Byte(u8),
91}
92
93impl ValidByte {
94 fn alpha_numeric(byte: u8) -> Option<Self> {
95 byte.is_ascii_alphanumeric().then_some(Self::Byte(byte))
96 }
97
98 fn alpha_numeric_hyphen(byte: u8) -> Option<Self> {
99 (byte.is_ascii_alphanumeric() || byte == b'-' || 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("allow-*").is_ok());
273 assert!(ident("deny-*").is_ok());
274 assert!(ident("prefix:allow-*").is_ok());
275
276 assert!(ident("prefix::base").is_err());
277 assert!(ident(":base").is_err());
278 assert!(ident("prefix:").is_err());
279 assert!(ident(":prefix:base:").is_err());
280 assert!(ident("base:").is_err());
281
282 assert!(ident("").is_err());
283 assert!(ident("💩").is_err());
284
285 assert!(ident("a".repeat(MAX_LEN_IDENTIFIER + 1)).is_err());
286 }
287
288 #[test]
289 fn base() {
290 assert_eq!(ident("prefix:base").unwrap().get_base(), "base");
291 assert_eq!(ident("base").unwrap().get_base(), "base");
292 }
293
294 #[test]
295 fn prefix() {
296 assert_eq!(ident("prefix:base").unwrap().get_prefix(), Some("prefix"));
297 assert_eq!(ident("base").unwrap().get_prefix(), None);
298 }
299}
300
301#[cfg(any(feature = "build", feature = "build-2"))]
302mod build {
303 use proc_macro2::TokenStream;
304 use quote::{ToTokens, TokenStreamExt, quote};
305
306 use super::*;
307
308 impl ToTokens for Identifier {
309 fn to_tokens(&self, tokens: &mut TokenStream) {
310 let s = self.get();
311 tokens
312 .append_all(quote! { ::tauri::utils::acl::Identifier::try_from(#s.to_string()).unwrap() })
313 }
314 }
315}