tauri_utils/acl/
identifier.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! Identifier for plugins.
6
7use 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
15// <https://doc.rust-lang.org/cargo/reference/manifest.html#the-name-field>
16const 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/// Plugin identifier.
21#[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    // Include the module, in case a type with the same name is in another module/crate
35    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  /// Get the identifier str.
52  #[inline(always)]
53  pub fn get(&self) -> &str {
54    self.as_ref()
55  }
56
57  /// Get the identifier without prefix.
58  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  /// Get the prefix of the identifier.
66  pub fn get_prefix(&self) -> Option<&str> {
67    self.separator_index().map(|i| &self.inner[0..i])
68  }
69
70  /// Set the identifier prefix.
71  pub fn set_prefix(&mut self) -> Result<(), ParseIdentifierError> {
72    todo!()
73  }
74
75  /// Get the identifier string and its separator.
76  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/// Errors that can happen when parsing an identifier.
115#[derive(Debug, Error)]
116pub enum ParseIdentifierError {
117  /// Identifier start with the plugin prefix.
118  #[error("identifiers cannot start with {}", PLUGIN_PREFIX)]
119  StartsWithTauriPlugin,
120
121  /// Identifier empty.
122  #[error("identifiers cannot be empty")]
123  Empty,
124
125  /// Identifier is too long.
126  #[error("identifiers cannot be longer than {len}, found {0}", len = MAX_LEN_IDENTIFIER)]
127  Humongous(usize),
128
129  /// Identifier is not in a valid format.
130  #[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  /// Identifier has multiple separators.
134  #[error(
135    "identifiers can only include a single separator '{}'",
136    IDENTIFIER_SEPARATOR
137  )]
138  MultipleSeparators,
139
140  /// Identifier has a trailing hyphen.
141  #[error("identifiers cannot have a trailing hyphen")]
142  TrailingHyphen,
143
144  /// Identifier has a prefix without a base.
145  #[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    // grab the first byte only before parsing the rest
170    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; // we already consumed first item
179      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            // safe to unwrap because idx starts at 1 and cannot go over MAX_IDENTIFIER_LEN
185            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      // empty base
196      ValidByte::Separator => return Err(Self::Error::PrefixWithoutBase),
197
198      // trailing hyphen
199      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    // bad
249    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}