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(next),
108      (ValidByte::Byte(_), next) => ValidByte::alpha_numeric_hyphen(next),
109    }
110  }
111}
112
113/// Errors that can happen when parsing an identifier.
114#[derive(Debug, Error)]
115pub enum ParseIdentifierError {
116  /// Identifier start with the plugin prefix.
117  #[error("identifiers cannot start with {}", PLUGIN_PREFIX)]
118  StartsWithTauriPlugin,
119
120  /// Identifier empty.
121  #[error("identifiers cannot be empty")]
122  Empty,
123
124  /// Identifier is too long.
125  #[error("identifiers cannot be longer than {len}, found {0}", len = MAX_LEN_IDENTIFIER)]
126  Humongous(usize),
127
128  /// Identifier is not in a valid format.
129  #[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  /// Identifier has multiple separators.
133  #[error(
134    "identifiers can only include a single separator '{}'",
135    IDENTIFIER_SEPARATOR
136  )]
137  MultipleSeparators,
138
139  /// Identifier has a trailing hyphen.
140  #[error("identifiers cannot have a trailing hyphen")]
141  TrailingHyphen,
142
143  /// Identifier has a prefix without a base.
144  #[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    // grab the first byte only before parsing the rest
169    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; // we already consumed first item
178      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            // safe to unwrap because idx starts at 1 and cannot go over MAX_IDENTIFIER_LEN
184            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      // empty base
195      ValidByte::Separator => return Err(Self::Error::PrefixWithoutBase),
196
197      // trailing hyphen
198      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    // bad
248    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}