Skip to main content

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/// Permission identifier.
21///
22/// Typically used in the [`permissions`](crate::acl::Capability::permissions) field of a capability file.
23/// (e.g. `core:default`, `sample:allow-ping-scoped`)
24#[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    // Include the module, in case a type with the same name is in another module/crate
38    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  /// Get the identifier str.
55  #[inline(always)]
56  pub fn get(&self) -> &str {
57    self.as_ref()
58  }
59
60  /// Get the identifier without prefix.
61  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  /// Get the prefix of the identifier.
69  pub fn get_prefix(&self) -> Option<&str> {
70    self.separator_index().map(|i| &self.inner[0..i])
71  }
72
73  /// Set the identifier prefix.
74  pub fn set_prefix(&mut self) -> Result<(), ParseIdentifierError> {
75    todo!()
76  }
77
78  /// Get the identifier string and its separator.
79  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/// Errors that can happen when parsing an identifier.
118#[derive(Debug, Error)]
119pub enum ParseIdentifierError {
120  /// Identifier start with the plugin prefix.
121  #[error("identifiers cannot start with {}", PLUGIN_PREFIX)]
122  StartsWithTauriPlugin,
123
124  /// Identifier empty.
125  #[error("identifiers cannot be empty")]
126  Empty,
127
128  /// Identifier is too long.
129  #[error("identifiers cannot be longer than {len}, found {0}", len = MAX_LEN_IDENTIFIER)]
130  Humongous(usize),
131
132  /// Identifier is not in a valid format.
133  #[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  /// Identifier has multiple separators.
137  #[error(
138    "identifiers can only include a single separator '{}'",
139    IDENTIFIER_SEPARATOR
140  )]
141  MultipleSeparators,
142
143  /// Identifier has a trailing hyphen.
144  #[error("identifiers cannot have a trailing hyphen")]
145  TrailingHyphen,
146
147  /// Identifier has a prefix without a base.
148  #[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    // grab the first byte only before parsing the rest
173    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; // we already consumed first item
182      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            // safe to unwrap because idx starts at 1 and cannot go over MAX_IDENTIFIER_LEN
188            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      // empty base
199      ValidByte::Separator => return Err(Self::Error::PrefixWithoutBase),
200
201      // trailing hyphen
202      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    // bad
257    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}