1
//! [`ArtiPath`] and its associated helpers.
2

            
3
use std::str::FromStr;
4

            
5
use derive_deftly::{Deftly, define_derive_deftly};
6
use derive_more::{Deref, Display, Into};
7
use serde::{Deserialize, Serialize};
8
use tor_persist::slug::{self, BadSlug};
9

            
10
use 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).
14
define_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
51984
        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
44986
        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
29392
        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)]
106
pub struct ArtiPath(String);
107

            
108
/// A separator for `ArtiPath`s.
109
pub(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.
117
pub const DENOTATOR_SEP: char = '+';
118

            
119
/// A separator for separating individual denotator groups from each other.
120
pub const DENOTATOR_GROUP_SEP: char = '@';
121

            
122
impl ArtiPath {
123
    /// Validate the underlying representation of an `ArtiPath`
124
51984
    fn validate_str(inner: &str) -> Result<(), ArtiPathSyntaxError> {
125
        // Validate the denotators, if there are any.
126
51984
        let path = if let Some((main_part, denotator_groups)) = inner.split_once(DENOTATOR_SEP) {
127
30790
            for denotators in denotator_groups.split(DENOTATOR_GROUP_SEP) {
128
30790
                let () = validate_denotator_group(denotators)?;
129
            }
130

            
131
30678
            main_part
132
        } else {
133
21288
            inner
134
        };
135

            
136
51966
        if let Some(e) = path
137
51966
            .split(PATH_SEP)
138
157002
            .map(|s| {
139
155544
                if s.is_empty() {
140
30
                    Err(BadSlug::EmptySlugNotAllowed.into())
141
                } else {
142
155514
                    Ok(slug::check_syntax(s)?)
143
                }
144
155544
            })
145
157002
            .find(|e| e.is_err())
146
        {
147
90
            return e;
148
51876
        }
149

            
150
51876
        Ok(())
151
51984
    }
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
10
    pub fn substring(&self, range: &ArtiPathRange) -> Option<&str> {
174
10
        self.0.get(range.0.clone())
175
10
    }
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
670
    pub(crate) fn from_path_and_denotators(
221
670
        path: ArtiPath,
222
670
        cert_denotators: &[&dyn KeySpecifierComponent],
223
670
    ) -> Result<ArtiPath, ArtiPathSyntaxError> {
224
670
        if cert_denotators.is_empty() {
225
632
            return Ok(path);
226
38
        }
227

            
228
38
        let cert_denotators = cert_denotators
229
38
            .iter()
230
73
            .map(|s| s.to_slug().map(|s| s.to_string()))
231
38
            .collect::<Result<Vec<_>, _>>()?
232
38
            .join(&DENOTATOR_SEP.to_string());
233

            
234
38
        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
38
            if path.contains(DENOTATOR_SEP) {
243
6
                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
32
                format!("{path}{DENOTATOR_SEP}{DENOTATOR_GROUP_SEP}{cert_denotators}")
249
            }
250
        };
251

            
252
38
        ArtiPath::new(path)
253
670
    }
254
}
255

            
256
/// Validate a single denotator group.
257
30790
fn validate_denotator_group(denotators: &str) -> Result<(), ArtiPathSyntaxError> {
258
    // Empty denotator groups are allowed
259
30790
    if denotators.is_empty() {
260
70
        return Ok(());
261
30720
    }
262

            
263
30808
    for d in denotators.split(DENOTATOR_SEP) {
264
30808
        let () = slug::check_syntax(d)?;
265
    }
266

            
267
30702
    Ok(())
268
30790
}
269

            
270
#[cfg(test)]
271
mod 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
}