Skip to main content

tor_keymgr/
arti_path.rs

1//! [`ArtiPath`] and its associated helpers.
2
3use std::str::FromStr;
4
5use derive_deftly::{Deftly, define_derive_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 *denotator groups*.
66/// A denotator group consists
67/// of one or more
68/// [`KeySpecifierComponent`]
69/// s representing the denotators of the key.
70/// [`DENOTATOR_SEP`] denotes the beginning of the denotator groups.
71///
72/// Within a denotator group, denotators are separated
73/// by [`DENOTATOR_SEP`] characters.
74///
75/// Denotator groups are separated from each other
76/// by [`DENOTATOR_GROUP_SEP`] characters.
77///
78/// Empty denotator groups are allowed,
79/// but trailing empty denotator groups are not represented in `ArtiPath`s.
80/// Consequently, two abstract paths which differ only
81/// in trailing empty denotator groups cannot be distinguished;
82/// or to put it another way, the number of denotator groups
83/// is not recoverable from the path.
84///
85/// Denotators are encoded using their
86/// [`KeySpecifierComponent::to_slug`]
87/// implementation.
88/// The denotators **must** come after all the other fields.
89/// Denotator strings are validated in the same way as [`Slug`](tor-persist::slug::Slug)s.
90///
91/// For example, the last component of the path `"foo/bar/bax+denotator_example+1"`
92/// is the denotator group `"denotator_example+1"`.
93/// Its denotators are `"denotator_example"` and `"1"` (encoded as strings).
94/// As another example, the path `"foo/bar/bax+denotator_example+1@foo+bar@baz"`
95/// has three denotator groups, separated by `@`,
96/// `"denotator_example+1"`, `foo+bar`, and `baz`.
97///
98/// NOTE: There is a 1:1 mapping between a value that implements `KeySpecifier` and its
99/// corresponding `ArtiPath`. A `KeySpecifier` can be converted to an `ArtiPath`, but the reverse
100/// conversion is not supported.
101#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Deref, Into, Display)] //
102#[derive(Serialize, Deserialize)]
103#[serde(try_from = "String", into = "String")]
104#[derive(Deftly)]
105#[derive_deftly(ValidatedString)]
106pub struct ArtiPath(String);
107
108/// A separator for `ArtiPath`s.
109pub(crate) const PATH_SEP: char = '/';
110
111/// A separator for that marks the beginning of the keys denotators
112/// within an [`ArtiPath`].
113///
114/// This separator can only appear within the last component of an [`ArtiPath`],
115/// and the substring that follows it is assumed to be the string representation
116/// of the denotator groups of the path.
117pub const DENOTATOR_SEP: char = '+';
118
119/// A separator for separating individual denotator groups from each other.
120pub const DENOTATOR_GROUP_SEP: char = '@';
121
122impl ArtiPath {
123    /// Validate the underlying representation of an `ArtiPath`
124    fn validate_str(inner: &str) -> Result<(), ArtiPathSyntaxError> {
125        // Validate the denotators, if there are any.
126        let path = if let Some((main_part, denotator_groups)) = inner.split_once(DENOTATOR_SEP) {
127            for denotators in denotator_groups.split(DENOTATOR_GROUP_SEP) {
128                let () = validate_denotator_group(denotators)?;
129            }
130
131            main_part
132        } else {
133            inner
134        };
135
136        if let Some(e) = path
137            .split(PATH_SEP)
138            .map(|s| {
139                if s.is_empty() {
140                    Err(BadSlug::EmptySlugNotAllowed.into())
141                } else {
142                    Ok(slug::check_syntax(s)?)
143                }
144            })
145            .find(|e| e.is_err())
146        {
147            return e;
148        }
149
150        Ok(())
151    }
152
153    /// Return the substring corresponding to the specified `range`.
154    ///
155    /// Returns `None` if `range` is not within the bounds of this `ArtiPath`.
156    ///
157    /// ### Example
158    /// ```
159    /// # use tor_keymgr::{ArtiPath, ArtiPathRange, ArtiPathSyntaxError};
160    /// # fn demo() -> Result<(), ArtiPathSyntaxError> {
161    /// let path = ArtiPath::new("foo_bar_bax_1".into())?;
162    ///
163    /// let range = ArtiPathRange::from(2..5);
164    /// assert_eq!(path.substring(&range), Some("o_b"));
165    ///
166    /// let range = ArtiPathRange::from(22..50);
167    /// assert_eq!(path.substring(&range), None);
168    /// # Ok(())
169    /// # }
170    /// #
171    /// # demo().unwrap();
172    /// ```
173    pub fn substring(&self, range: &ArtiPathRange) -> Option<&str> {
174        self.0.get(range.0.clone())
175    }
176
177    /// Create an `ArtiPath` from an `ArtiPath` and a list of denotators.
178    ///
179    /// If `cert_denotators` is empty, returns the specified `path` as-is.
180    /// Otherwise, returns an `ArtiPath` that consists of the specified `path`
181    /// followed by a [`DENOTATOR_GROUP_SEP`] character and the specified denotators
182    /// (the denotators are encoded as described in the [`ArtiPath`] docs).
183    ///
184    /// Returns an error if any of the specified denotators are not valid `Slug`s.
185    //
186    /// ### Example
187    /// ```nocompile
188    /// # // `nocompile` because this function is not pub
189    /// # use tor_keymgr::{
190    /// #    ArtiPath, ArtiPathRange, ArtiPathSyntaxError, KeySpecifierComponent,
191    /// #    KeySpecifierComponentViaDisplayFromStr,
192    /// # };
193    /// # use derive_more::{Display, FromStr};
194    /// # #[derive(Display, FromStr)]
195    /// # struct Denotator(String);
196    /// # impl KeySpecifierComponentViaDisplayFromStr for Denotator {}
197    /// # fn demo() -> Result<(), ArtiPathSyntaxError> {
198    /// let path = ArtiPath::new("my_key_path".into())?;
199    /// let denotators = [
200    ///    &Denotator("foo".to_string()) as &dyn KeySpecifierComponent,
201    ///    &Denotator("bar".to_string()) as &dyn KeySpecifierComponent,
202    /// ];
203    ///
204    /// let expected_path = ArtiPath::new("my_key_path+foo+bar".into())?;
205    ///
206    /// assert_eq!(
207    ///    ArtiPath::from_path_and_denotators(path.clone(), &denotators[..])?,
208    ///    expected_path
209    /// );
210    ///
211    /// assert_eq!(
212    ///    ArtiPath::from_path_and_denotators(path.clone(), &[])?,
213    ///    path
214    /// );
215    /// # Ok(())
216    /// # }
217    /// #
218    /// # demo().unwrap();
219    /// ```
220    pub(crate) fn from_path_and_denotators(
221        path: ArtiPath,
222        cert_denotators: &[&dyn KeySpecifierComponent],
223    ) -> Result<ArtiPath, ArtiPathSyntaxError> {
224        if cert_denotators.is_empty() {
225            return Ok(path);
226        }
227
228        let cert_denotators = cert_denotators
229            .iter()
230            .map(|s| s.to_slug().map(|s| s.to_string()))
231            .collect::<Result<Vec<_>, _>>()?
232            .join(&DENOTATOR_SEP.to_string());
233
234        let path = if cert_denotators.is_empty() {
235            format!("{path}")
236        } else {
237            // If the path already contains some denotators,
238            // we need to use the denotator group separator
239            // to separate them from the certificate denotators.
240            // Otherwise, we simply use the regular DENOTATOR_SEP
241            // to indicate the start of the denotator section.
242            if path.contains(DENOTATOR_SEP) {
243                format!("{path}{DENOTATOR_GROUP_SEP}{cert_denotators}")
244            } else {
245                // If the key path has no denotators, we need to manually insert
246                // an empty denotator group before the `cert_denotators` denotator group.
247                // This ensures the origin (key vs cert specifier) of the denotators is unambiguous.
248                format!("{path}{DENOTATOR_SEP}{DENOTATOR_GROUP_SEP}{cert_denotators}")
249            }
250        };
251
252        ArtiPath::new(path)
253    }
254}
255
256/// Validate a single denotator group.
257fn validate_denotator_group(denotators: &str) -> Result<(), ArtiPathSyntaxError> {
258    // Empty denotator groups are allowed
259    if denotators.is_empty() {
260        return Ok(());
261    }
262
263    for d in denotators.split(DENOTATOR_SEP) {
264        let () = slug::check_syntax(d)?;
265    }
266
267    Ok(())
268}
269
270#[cfg(test)]
271mod tests {
272    // @@ begin test lint list maintained by maint/add_warning @@
273    #![allow(clippy::bool_assert_comparison)]
274    #![allow(clippy::clone_on_copy)]
275    #![allow(clippy::dbg_macro)]
276    #![allow(clippy::mixed_attributes_style)]
277    #![allow(clippy::print_stderr)]
278    #![allow(clippy::print_stdout)]
279    #![allow(clippy::single_char_pattern)]
280    #![allow(clippy::unwrap_used)]
281    #![allow(clippy::unchecked_time_subtraction)]
282    #![allow(clippy::useless_vec)]
283    #![allow(clippy::needless_pass_by_value)]
284    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
285    use super::*;
286
287    use derive_more::{Display, FromStr};
288    use itertools::chain;
289
290    use crate::KeySpecifierComponentViaDisplayFromStr;
291
292    impl PartialEq for ArtiPathSyntaxError {
293        fn eq(&self, other: &Self) -> bool {
294            use ArtiPathSyntaxError::*;
295
296            match (self, other) {
297                (Slug(err1), Slug(err2)) => err1 == err2,
298                _ => false,
299            }
300        }
301    }
302
303    macro_rules! assert_ok {
304        ($ty:ident, $inner:expr) => {{
305            let path = $ty::new($inner.to_string());
306            let path_fromstr: Result<$ty, _> = $ty::try_from($inner.to_string());
307            let path_tryfrom: Result<$ty, _> = $inner.to_string().try_into();
308            assert!(path.is_ok(), "{} should be valid", $inner);
309            assert_eq!(path.as_ref().unwrap().to_string(), *$inner);
310            assert_eq!(path, path_fromstr);
311            assert_eq!(path, path_tryfrom);
312        }};
313    }
314
315    fn assert_err(path: &str, error_kind: ArtiPathSyntaxError) {
316        let path_anew = ArtiPath::new(path.to_string());
317        let path_fromstr = ArtiPath::try_from(path.to_string());
318        let path_tryfrom: Result<ArtiPath, _> = path.to_string().try_into();
319        assert!(path_anew.is_err(), "{} should be invalid", path);
320        let actual_err = path_anew.as_ref().unwrap_err();
321        assert_eq!(actual_err, &error_kind);
322        assert_eq!(path_anew, path_fromstr);
323        assert_eq!(path_anew, path_tryfrom);
324    }
325
326    #[derive(Display, FromStr)]
327    struct Denotator(String);
328
329    impl KeySpecifierComponentViaDisplayFromStr for Denotator {}
330
331    #[test]
332    fn arti_path_from_path_and_denotators() {
333        let denotators = [
334            &Denotator("foo".to_string()) as &dyn KeySpecifierComponent,
335            &Denotator("bar".to_string()) as &dyn KeySpecifierComponent,
336            &Denotator("baz".to_string()) as &dyn KeySpecifierComponent,
337        ];
338
339        /// Base ArtiPaths and the expected outcome from concatenating
340        /// the base with the denotator group above.
341        const TEST_PATHS: &[(&str, &str)] = &[
342            // A base path with no denotator groups
343            ("my_key_path", "my_key_path+@foo+bar+baz"),
344            // A base path with a single denotator groups
345            ("my_key_path+dino+saur", "my_key_path+dino+saur@foo+bar+baz"),
346            // A base path with two denotator groups
347            ("my_key_path+dino@saur", "my_key_path+dino@saur@foo+bar+baz"),
348            // A base path with two empty denotator groups
349            (
350                "my_key_path+dino@@@saur",
351                "my_key_path+dino@@@saur@foo+bar+baz",
352            ),
353        ];
354
355        for (base_path, expected_path) in TEST_PATHS {
356            let path = ArtiPath::new(base_path.to_string()).unwrap();
357            let expected_path = ArtiPath::new(expected_path.to_string()).unwrap();
358
359            assert_eq!(
360                ArtiPath::from_path_and_denotators(path.clone(), &denotators[..]).unwrap(),
361                expected_path
362            );
363
364            assert_eq!(
365                ArtiPath::from_path_and_denotators(path.clone(), &[]).unwrap(),
366                path
367            );
368        }
369    }
370
371    #[test]
372    #[allow(clippy::cognitive_complexity)]
373    fn arti_path_validation() {
374        const VALID_ARTI_PATH_COMPONENTS: &[&str] = &["my-hs-client-2", "hs_client"];
375        const VALID_ARTI_PATHS: &[&str] = &[
376            "path/to/client+subvalue+fish",
377            "_hs_client",
378            "hs_client-",
379            "hs_client_",
380            "_",
381            // A path with an empty denotator group
382            "my_key_path+dino@@saur",
383            // Paths with a trailing empty denotator group.
384            // Our implementation doesn't encode empty trailing
385            // denotator groups in ArtiPaths, but our parsing rules
386            // don't forbid them.
387            "my_key_path+dino@",
388            "my_key_path+@",
389        ];
390
391        const BAD_FIRST_CHAR_ARTI_PATHS: &[&str] = &["-hs_client", "-"];
392
393        const DISALLOWED_CHAR_ARTI_PATHS: &[(&str, char)] = &[
394            ("client?", '?'),
395            ("no spaces please", ' '),
396            ("client٣¾", '٣'),
397            ("clientß", 'ß'),
398            // Invalid paths: the main component of the path
399            // must be separated from the denotator groups by a `+` character
400            ("my_key_path@", '@'),
401            ("my_key_path@dino+saur", '@'),
402        ];
403
404        const EMPTY_PATH_COMPONENT: &[&str] =
405            &["/////", "/alice/bob", "alice//bob", "alice/bob/", "/"];
406
407        for path in chain!(VALID_ARTI_PATH_COMPONENTS, VALID_ARTI_PATHS) {
408            assert_ok!(ArtiPath, path);
409        }
410
411        for (path, bad_char) in DISALLOWED_CHAR_ARTI_PATHS {
412            assert_err(
413                path,
414                ArtiPathSyntaxError::Slug(BadSlug::BadCharacter(*bad_char)),
415            );
416        }
417
418        for path in BAD_FIRST_CHAR_ARTI_PATHS {
419            assert_err(
420                path,
421                ArtiPathSyntaxError::Slug(BadSlug::BadFirstCharacter(path.chars().next().unwrap())),
422            );
423        }
424
425        for path in EMPTY_PATH_COMPONENT {
426            assert_err(
427                path,
428                ArtiPathSyntaxError::Slug(BadSlug::EmptySlugNotAllowed),
429            );
430        }
431
432        const SEP: char = PATH_SEP;
433        // This is a valid ArtiPath, but not a valid Slug
434        let path = format!("a{SEP}client{SEP}key+private");
435        assert_ok!(ArtiPath, path);
436
437        const PATH_WITH_TRAVERSAL: &str = "alice/../bob";
438        assert_err(
439            PATH_WITH_TRAVERSAL,
440            ArtiPathSyntaxError::Slug(BadSlug::BadCharacter('.')),
441        );
442
443        const REL_PATH: &str = "./bob";
444        assert_err(
445            REL_PATH,
446            ArtiPathSyntaxError::Slug(BadSlug::BadCharacter('.')),
447        );
448
449        const EMPTY_DENOTATOR: &str = "c++";
450        assert_err(
451            EMPTY_DENOTATOR,
452            ArtiPathSyntaxError::Slug(BadSlug::EmptySlugNotAllowed),
453        );
454    }
455
456    #[test]
457    #[allow(clippy::cognitive_complexity)]
458    fn arti_path_with_denotator() {
459        const VALID_ARTI_DENOTATORS: &[&str] = &[
460            "foo",
461            "one_two_three-f0ur",
462            "1-2-3-",
463            "1-2-3_",
464            "1-2-3",
465            "_1-2-3",
466            "1-2-3",
467        ];
468
469        const BAD_OUTER_CHAR_DENOTATORS: &[&str] = &["-1-2-3"];
470
471        for denotator in VALID_ARTI_DENOTATORS {
472            let path = format!("foo/bar/qux+{denotator}");
473            assert_ok!(ArtiPath, path);
474        }
475
476        for denotator in BAD_OUTER_CHAR_DENOTATORS {
477            let path = format!("foo/bar/qux+{denotator}");
478
479            assert_err(
480                &path,
481                ArtiPathSyntaxError::Slug(BadSlug::BadFirstCharacter(
482                    denotator.chars().next().unwrap(),
483                )),
484            );
485        }
486
487        // An ArtiPath with multiple denotators
488        let path = format!(
489            "foo/bar/qux+{}+{}+foo",
490            VALID_ARTI_DENOTATORS[0], VALID_ARTI_DENOTATORS[1]
491        );
492        assert_ok!(ArtiPath, path);
493
494        // An invalid ArtiPath with multiple valid denotators and
495        // an empty (invalid) denotator
496        let path = format!(
497            "foo/bar/qux+{}+{}+foo+",
498            VALID_ARTI_DENOTATORS[0], VALID_ARTI_DENOTATORS[1]
499        );
500        assert_err(
501            &path,
502            ArtiPathSyntaxError::Slug(BadSlug::EmptySlugNotAllowed),
503        );
504    }
505
506    #[test]
507    fn substring() {
508        const KEY_PATH: &str = "hello";
509        let path = ArtiPath::new(KEY_PATH.to_string()).unwrap();
510
511        assert_eq!(path.substring(&(0..1).into()).unwrap(), "h");
512        assert_eq!(path.substring(&(2..KEY_PATH.len()).into()).unwrap(), "llo");
513        assert_eq!(
514            path.substring(&(0..KEY_PATH.len()).into()).unwrap(),
515            "hello"
516        );
517        assert_eq!(path.substring(&(0..KEY_PATH.len() + 1).into()), None);
518        assert_eq!(path.substring(&(0..0).into()).unwrap(), "");
519    }
520}