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    use itertools::chain;
240
241    use crate::KeySpecifierComponentViaDisplayFromStr;
242
243    impl PartialEq for ArtiPathSyntaxError {
244        fn eq(&self, other: &Self) -> bool {
245            use ArtiPathSyntaxError::*;
246
247            match (self, other) {
248                (Slug(err1), Slug(err2)) => err1 == err2,
249                _ => false,
250            }
251        }
252    }
253
254    macro_rules! assert_ok {
255        ($ty:ident, $inner:expr) => {{
256            let path = $ty::new($inner.to_string());
257            let path_fromstr: Result<$ty, _> = $ty::try_from($inner.to_string());
258            let path_tryfrom: Result<$ty, _> = $inner.to_string().try_into();
259            assert!(path.is_ok(), "{} should be valid", $inner);
260            assert_eq!(path.as_ref().unwrap().to_string(), *$inner);
261            assert_eq!(path, path_fromstr);
262            assert_eq!(path, path_tryfrom);
263        }};
264    }
265
266    fn assert_err(path: &str, error_kind: ArtiPathSyntaxError) {
267        let path_anew = ArtiPath::new(path.to_string());
268        let path_fromstr = ArtiPath::try_from(path.to_string());
269        let path_tryfrom: Result<ArtiPath, _> = path.to_string().try_into();
270        assert!(path_anew.is_err(), "{} should be invalid", path);
271        let actual_err = path_anew.as_ref().unwrap_err();
272        assert_eq!(actual_err, &error_kind);
273        assert_eq!(path_anew, path_fromstr);
274        assert_eq!(path_anew, path_tryfrom);
275    }
276
277    #[derive(Display, FromStr)]
278    struct Denotator(String);
279
280    impl KeySpecifierComponentViaDisplayFromStr for Denotator {}
281
282    #[test]
283    fn arti_path_from_path_and_denotators() {
284        let path = ArtiPath::new("my_key_path".into()).unwrap();
285        let denotators = [
286            &Denotator("foo".to_string()) as &dyn KeySpecifierComponent,
287            &Denotator("bar".to_string()) as &dyn KeySpecifierComponent,
288            &Denotator("baz".to_string()) as &dyn KeySpecifierComponent,
289        ];
290
291        let expected_path = ArtiPath::new("my_key_path+foo+bar+baz".into()).unwrap();
292
293        assert_eq!(
294            ArtiPath::from_path_and_denotators(path.clone(), &denotators[..]).unwrap(),
295            expected_path
296        );
297
298        assert_eq!(
299            ArtiPath::from_path_and_denotators(path.clone(), &[]).unwrap(),
300            path
301        );
302    }
303
304    #[test]
305    #[allow(clippy::cognitive_complexity)]
306    fn arti_path_validation() {
307        const VALID_ARTI_PATH_COMPONENTS: &[&str] = &["my-hs-client-2", "hs_client"];
308        const VALID_ARTI_PATHS: &[&str] = &[
309            "path/to/client+subvalue+fish",
310            "_hs_client",
311            "hs_client-",
312            "hs_client_",
313            "_",
314        ];
315
316        const BAD_FIRST_CHAR_ARTI_PATHS: &[&str] = &["-hs_client", "-"];
317
318        const DISALLOWED_CHAR_ARTI_PATHS: &[(&str, char)] = &[
319            ("client?", '?'),
320            ("no spaces please", ' '),
321            ("client٣¾", '٣'),
322            ("clientß", 'ß'),
323        ];
324
325        const EMPTY_PATH_COMPONENT: &[&str] =
326            &["/////", "/alice/bob", "alice//bob", "alice/bob/", "/"];
327
328        for path in chain!(VALID_ARTI_PATH_COMPONENTS, VALID_ARTI_PATHS) {
329            assert_ok!(ArtiPath, path);
330        }
331
332        for (path, bad_char) in DISALLOWED_CHAR_ARTI_PATHS {
333            assert_err(
334                path,
335                ArtiPathSyntaxError::Slug(BadSlug::BadCharacter(*bad_char)),
336            );
337        }
338
339        for path in BAD_FIRST_CHAR_ARTI_PATHS {
340            assert_err(
341                path,
342                ArtiPathSyntaxError::Slug(BadSlug::BadFirstCharacter(path.chars().next().unwrap())),
343            );
344        }
345
346        for path in EMPTY_PATH_COMPONENT {
347            assert_err(
348                path,
349                ArtiPathSyntaxError::Slug(BadSlug::EmptySlugNotAllowed),
350            );
351        }
352
353        const SEP: char = PATH_SEP;
354        // This is a valid ArtiPath, but not a valid Slug
355        let path = format!("a{SEP}client{SEP}key+private");
356        assert_ok!(ArtiPath, path);
357
358        const PATH_WITH_TRAVERSAL: &str = "alice/../bob";
359        assert_err(
360            PATH_WITH_TRAVERSAL,
361            ArtiPathSyntaxError::Slug(BadSlug::BadCharacter('.')),
362        );
363
364        const REL_PATH: &str = "./bob";
365        assert_err(
366            REL_PATH,
367            ArtiPathSyntaxError::Slug(BadSlug::BadCharacter('.')),
368        );
369
370        const EMPTY_DENOTATOR: &str = "c++";
371        assert_err(
372            EMPTY_DENOTATOR,
373            ArtiPathSyntaxError::Slug(BadSlug::EmptySlugNotAllowed),
374        );
375    }
376
377    #[test]
378    #[allow(clippy::cognitive_complexity)]
379    fn arti_path_with_denotator() {
380        const VALID_ARTI_DENOTATORS: &[&str] = &[
381            "foo",
382            "one_two_three-f0ur",
383            "1-2-3-",
384            "1-2-3_",
385            "1-2-3",
386            "_1-2-3",
387            "1-2-3",
388        ];
389
390        const BAD_OUTER_CHAR_DENOTATORS: &[&str] = &["-1-2-3"];
391
392        for denotator in VALID_ARTI_DENOTATORS {
393            let path = format!("foo/bar/qux+{denotator}");
394            assert_ok!(ArtiPath, path);
395        }
396
397        for denotator in BAD_OUTER_CHAR_DENOTATORS {
398            let path = format!("foo/bar/qux+{denotator}");
399
400            assert_err(
401                &path,
402                ArtiPathSyntaxError::Slug(BadSlug::BadFirstCharacter(
403                    denotator.chars().next().unwrap(),
404                )),
405            );
406        }
407
408        // An ArtiPath with multiple denotators
409        let path = format!(
410            "foo/bar/qux+{}+{}+foo",
411            VALID_ARTI_DENOTATORS[0], VALID_ARTI_DENOTATORS[1]
412        );
413        assert_ok!(ArtiPath, path);
414
415        // An invalid ArtiPath with multiple valid denotators and
416        // an empty (invalid) denotator
417        let path = format!(
418            "foo/bar/qux+{}+{}+foo+",
419            VALID_ARTI_DENOTATORS[0], VALID_ARTI_DENOTATORS[1]
420        );
421        assert_err(
422            &path,
423            ArtiPathSyntaxError::Slug(BadSlug::EmptySlugNotAllowed),
424        );
425    }
426
427    #[test]
428    fn substring() {
429        const KEY_PATH: &str = "hello";
430        let path = ArtiPath::new(KEY_PATH.to_string()).unwrap();
431
432        assert_eq!(path.substring(&(0..1).into()).unwrap(), "h");
433        assert_eq!(path.substring(&(2..KEY_PATH.len()).into()).unwrap(), "llo");
434        assert_eq!(
435            path.substring(&(0..KEY_PATH.len()).into()).unwrap(),
436            "hello"
437        );
438        assert_eq!(path.substring(&(0..KEY_PATH.len() + 1).into()), None);
439        assert_eq!(path.substring(&(0..0).into()).unwrap(), "");
440    }
441}