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() -> 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  /// Get the identifier str.
54  #[inline(always)]
55  pub fn get(&self) -> &str {
56    self.as_ref()
57  }
58
59  /// Get the identifier without prefix.
60  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  /// Get the prefix of the identifier.
68  pub fn get_prefix(&self) -> Option<&str> {
69    self.separator_index().map(|i| &self.inner[0..i])
70  }
71
72  /// Set the identifier prefix.
73  pub fn set_prefix(&mut self) -> Result<(), ParseIdentifierError> {
74    todo!()
75  }
76
77  /// Get the identifier string and its separator.
78  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    // `*` is allowed so the implicit `allow-*`/`deny-*` wildcard command permissions can be referenced
100    (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/// 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(
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  /// Identifier has multiple separators.
139  #[error(
140    "identifiers can only include a single separator '{}'",
141    IDENTIFIER_SEPARATOR
142  )]
143  MultipleSeparators,
144
145  /// Identifier has a trailing hyphen.
146  #[error("identifiers cannot have a trailing hyphen")]
147  TrailingHyphen,
148
149  /// Identifier has a prefix without a base.
150  #[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    // grab the first byte only before parsing the rest
175    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; // we already consumed first item
184      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            // safe to unwrap because idx starts at 1 and cannot go over MAX_IDENTIFIER_LEN
190            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      // empty base
201      ValidByte::Separator => return Err(Self::Error::PrefixWithoutBase),
202
203      // trailing hyphen
204      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    // bad
259    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    // wildcard command permissions
272    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}