tor_keymgr/
arti_path.rs

1//! [`ArtiPath`] and its associated helpers.
2
3use std::str::FromStr;
4
5use derive_deftly::{define_derive_deftly, Deftly};
6use derive_more::{Deref, Display, Into};
7use serde::{Deserialize, Serialize};
8use tor_persist::slug::{self, BadSlug};
9
10use crate::{ArtiPathRange, ArtiPathSyntaxError, KeySpecifierComponent};
11
12// TODO: this is only used for ArtiPaths (we should consider turning this
13// intro a regular impl ArtiPath {} and removing the macro).
14define_derive_deftly! {
15    /// Implement `new()`, `TryFrom<String>` in terms of `validate_str`, and `as_ref<str>`
16    //
17    // TODO maybe this is generally useful?  Or maybe we should find a crate?
18    ValidatedString for struct, expect items:
19
20    impl $ttype {
21        #[doc = concat!("Create a new [`", stringify!($tname), "`].")]
22        ///
23        /// This function returns an error if `inner` is not in the right syntax.
24        pub fn new(inner: String) -> Result<Self, ArtiPathSyntaxError> {
25            Self::validate_str(&inner)?;
26            Ok(Self(inner))
27        }
28    }
29
30    impl TryFrom<String> for $ttype {
31        type Error = ArtiPathSyntaxError;
32
33        fn try_from(s: String) -> Result<Self, ArtiPathSyntaxError> {
34            Self::new(s)
35        }
36    }
37
38    impl FromStr for $ttype {
39        type Err = ArtiPathSyntaxError;
40
41        fn from_str(s: &str) -> Result<Self, ArtiPathSyntaxError> {
42            Self::validate_str(s)?;
43            Ok(Self(s.to_owned()))
44        }
45    }
46
47    impl AsRef<str> for $ttype {
48        fn as_ref(&self) -> &str {
49            &self.0.as_str()
50        }
51    }
52}
53
54/// A unique identifier for a particular instance of a key.
55///
56/// In an [`ArtiNativeKeystore`](crate::ArtiNativeKeystore), this also represents the path of the
57/// key relative to the root of the keystore, minus the file extension.
58///
59/// An `ArtiPath` is a nonempty sequence of [`Slug`](tor_persist::slug::Slug)s, separated by `/`.  Path
60/// components may contain lowercase ASCII alphanumerics, and  `-` or `_`.
61/// See [slug] for the full syntactic requirements.
62/// Consequently, leading or trailing or duplicated / are forbidden.
63///
64/// The last component of the path may optionally contain the encoded (string) representation
65/// of one or more
66/// [`KeySpecifierComponent`]
67/// s representing the denotators of the key.
68/// They are separated from the rest of the component, and from each other,
69/// by [`DENOTATOR_SEP`] characters.
70/// Denotators are encoded using their
71/// [`KeySpecifierComponent::to_slug`]
72/// implementation.
73/// The denotators **must** come after all the other fields.
74/// Denotator strings are validated in the same way as [`Slug`](tor-persist::slug::Slug)s.
75///
76/// For example, the last component of the path `"foo/bar/bax+denotator_example+1"`
77/// is `"bax+denotator_example+1"`.
78/// Its denotators are `"denotator_example"` and `"1"` (encoded as strings).
79///
80/// NOTE: There is a 1:1 mapping between a value that implements `KeySpecifier` and its
81/// corresponding `ArtiPath`. A `KeySpecifier` can be converted to an `ArtiPath`, but the reverse
82/// conversion is not supported.
83#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Deref, Into, Display)] //
84#[derive(Serialize, Deserialize)]
85#[serde(try_from = "String", into = "String")]
86#[derive(Deftly)]
87#[derive_deftly(ValidatedString)]
88pub struct ArtiPath(String);
89
90/// A separator for `ArtiPath`s.
91pub(crate) const PATH_SEP: char = '/';
92
93/// A separator for that marks the beginning of the keys denotators
94/// within an [`ArtiPath`].
95///
96/// This separator can only appear within the last component of an [`ArtiPath`],
97/// and the substring that follows it is assumed to be the string representation
98/// of the denotators of the path.
99pub const DENOTATOR_SEP: char = '+';
100
101impl ArtiPath {
102    /// Validate the underlying representation of an `ArtiPath`
103    fn validate_str(inner: &str) -> Result<(), ArtiPathSyntaxError> {
104        // Validate the denotators, if there are any.
105        let path = if let Some((main_part, denotators)) = inner.split_once(DENOTATOR_SEP) {
106            for d in denotators.split(DENOTATOR_SEP) {
107                let () = slug::check_syntax(d)?;
108            }
109
110            main_part
111        } else {
112            inner
113        };
114
115        if let Some(e) = path
116            .split(PATH_SEP)
117            .map(|s| {
118                if s.is_empty() {
119                    Err(BadSlug::EmptySlugNotAllowed.into())
120                } else {
121                    Ok(slug::check_syntax(s)?)
122                }
123            })
124            .find(|e| e.is_err())
125        {
126            return e;
127        }
128
129        Ok(())
130    }
131
132    /// Return the substring corresponding to the specified `range`.
133    ///
134    /// Returns `None` if `range` is not within the bounds of this `ArtiPath`.
135    ///
136    /// ### Example
137    /// ```
138    /// # use tor_keymgr::{ArtiPath, ArtiPathRange, ArtiPathSyntaxError};
139    /// # fn demo() -> Result<(), ArtiPathSyntaxError> {
140    /// let path = ArtiPath::new("foo_bar_bax_1".into())?;
141    ///
142    /// let range = ArtiPathRange::from(2..5);
143    /// assert_eq!(path.substring(&range), Some("o_b"));
144    ///
145    /// let range = ArtiPathRange::from(22..50);
146    /// assert_eq!(path.substring(&range), None);
147    /// # Ok(())
148    /// # }
149    /// #
150    /// # demo().unwrap();
151    /// ```
152    pub fn substring(&self, range: &ArtiPathRange) -> Option<&str> {
153        self.0.get(range.0.clone())
154    }
155
156    /// Create an `ArtiPath` from an `ArtiPath` and a list of denotators.
157    ///
158    /// If `cert_denotators` is empty, returns the specified `path` as-is.
159    /// Otherwise, returns an `ArtiPath` that consists of the specified `path`
160    /// followed by a [`DENOTATOR_SEP`] character and the specified denotators
161    /// (the denotators are encoded as described in the [`ArtiPath`] docs).
162    ///
163    /// Returns an error if any of the specified denotators are not valid `Slug`s.
164    //
165    /// ### Example
166    /// ```nocompile
167    /// # // `nocompile` because this function is not pub
168    /// # use tor_keymgr::{
169    /// #    ArtiPath, ArtiPathRange, ArtiPathSyntaxError, KeySpecifierComponent,
170    /// #    KeySpecifierComponentViaDisplayFromStr,
171    /// # };
172    /// # use derive_more::{Display, FromStr};
173    /// # #[derive(Display, FromStr)]
174    /// # struct Denotator(String);
175    /// # impl KeySpecifierComponentViaDisplayFromStr for Denotator {}
176    /// # fn demo() -> Result<(), ArtiPathSyntaxError> {
177    /// let path = ArtiPath::new("my_key_path".into())?;
178    /// let denotators = [
179    ///    &Denotator("foo".to_string()) as &dyn KeySpecifierComponent,
180    ///    &Denotator("bar".to_string()) as &dyn KeySpecifierComponent,
181    /// ];
182    ///
183    /// let expected_path = ArtiPath::new("my_key_path+foo+bar".into())?;
184    ///
185    /// assert_eq!(
186    ///    ArtiPath::from_path_and_denotators(path.clone(), &denotators[..])?,
187    ///    expected_path
188    /// );
189    ///
190    /// assert_eq!(
191    ///    ArtiPath::from_path_and_denotators(path.clone(), &[])?,
192    ///    path
193    /// );
194    /// # Ok(())
195    /// # }
196    /// #
197    /// # demo().unwrap();
198    /// ```
199    pub(crate) fn from_path_and_denotators(
200        path: ArtiPath,
201        cert_denotators: &[&dyn KeySpecifierComponent],
202    ) -> Result<ArtiPath, ArtiPathSyntaxError> {
203        if cert_denotators.is_empty() {
204            return Ok(path);
205        }
206
207        let path: String = [Ok(path.0)]
208            .into_iter()
209            .chain(
210                cert_denotators
211                    .iter()
212                    .map(|s| s.to_slug().map(|s| s.to_string())),
213            )
214            .collect::<Result<Vec<_>, _>>()?
215            .join(&DENOTATOR_SEP.to_string());
216
217        ArtiPath::new(path)
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    // @@ begin test lint list maintained by maint/add_warning @@
224    #![allow(clippy::bool_assert_comparison)]
225    #![allow(clippy::clone_on_copy)]
226    #![allow(clippy::dbg_macro)]
227    #![allow(clippy::mixed_attributes_style)]
228    #![allow(clippy::print_stderr)]
229    #![allow(clippy::print_stdout)]
230    #![allow(clippy::single_char_pattern)]
231    #![allow(clippy::unwrap_used)]
232    #![allow(clippy::unchecked_duration_subtraction)]
233    #![allow(clippy::useless_vec)]
234    #![allow(clippy::needless_pass_by_value)]
235    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
236    use super::*;
237
238    use derive_more::{Display, FromStr};
239
240    use crate::KeySpecifierComponentViaDisplayFromStr;
241
242    #[derive(Display, FromStr)]
243    struct Denotator(String);
244
245    impl KeySpecifierComponentViaDisplayFromStr for Denotator {}
246
247    #[test]
248    fn arti_path_from_path_and_denotators() {
249        let path = ArtiPath::new("my_key_path".into()).unwrap();
250        let denotators = [
251            &Denotator("foo".to_string()) as &dyn KeySpecifierComponent,
252            &Denotator("bar".to_string()) as &dyn KeySpecifierComponent,
253            &Denotator("baz".to_string()) as &dyn KeySpecifierComponent,
254        ];
255
256        let expected_path = ArtiPath::new("my_key_path+foo+bar+baz".into()).unwrap();
257
258        assert_eq!(
259            ArtiPath::from_path_and_denotators(path.clone(), &denotators[..]).unwrap(),
260            expected_path
261        );
262
263        assert_eq!(
264            ArtiPath::from_path_and_denotators(path.clone(), &[]).unwrap(),
265            path
266        );
267    }
268}