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
45812
        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
40050
        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
53384
        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
45812
    fn validate_str(inner: &str) -> Result<(), ArtiPathSyntaxError> {
125
        // Validate the denotators, if there are any.
126
45812
        let path = if let Some((main_part, denotator_groups)) = inner.split_once(DENOTATOR_SEP) {
127
28546
            for denotators in denotator_groups.split(DENOTATOR_GROUP_SEP) {
128
28546
                let () = validate_denotator_group(denotators)?;
129
            }
130

            
131
28434
            main_part
132
        } else {
133
17360
            inner
134
        };
135

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

            
150
45704
        Ok(())
151
45812
    }
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
766
    pub(crate) fn from_path_and_denotators(
221
766
        path: ArtiPath,
222
766
        cert_denotators: &[&dyn KeySpecifierComponent],
223
766
    ) -> Result<ArtiPath, ArtiPathSyntaxError> {
224
766
        if cert_denotators.is_empty() {
225
728
            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
766
    }
254
}
255

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

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

            
267
28458
    Ok(())
268
28546
}
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
    #![allow(clippy::string_slice)] // See arti#2571
285
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
286
    use super::*;
287

            
288
    use derive_more::{Display, FromStr};
289
    use itertools::chain;
290

            
291
    use crate::KeySpecifierComponentViaDisplayFromStr;
292

            
293
    impl PartialEq for ArtiPathSyntaxError {
294
        fn eq(&self, other: &Self) -> bool {
295
            use ArtiPathSyntaxError::*;
296

            
297
            match (self, other) {
298
                (Slug(err1), Slug(err2)) => err1 == err2,
299
                _ => false,
300
            }
301
        }
302
    }
303

            
304
    macro_rules! assert_ok {
305
        ($ty:ident, $inner:expr) => {{
306
            let path = $ty::new($inner.to_string());
307
            let path_fromstr: Result<$ty, _> = $ty::try_from($inner.to_string());
308
            let path_tryfrom: Result<$ty, _> = $inner.to_string().try_into();
309
            assert!(path.is_ok(), "{} should be valid", $inner);
310
            assert_eq!(path.as_ref().unwrap().to_string(), *$inner);
311
            assert_eq!(path, path_fromstr);
312
            assert_eq!(path, path_tryfrom);
313
        }};
314
    }
315

            
316
    fn assert_err(path: &str, error_kind: ArtiPathSyntaxError) {
317
        let path_anew = ArtiPath::new(path.to_string());
318
        let path_fromstr = ArtiPath::try_from(path.to_string());
319
        let path_tryfrom: Result<ArtiPath, _> = path.to_string().try_into();
320
        assert!(path_anew.is_err(), "{} should be invalid", path);
321
        let actual_err = path_anew.as_ref().unwrap_err();
322
        assert_eq!(actual_err, &error_kind);
323
        assert_eq!(path_anew, path_fromstr);
324
        assert_eq!(path_anew, path_tryfrom);
325
    }
326

            
327
    #[derive(Display, FromStr)]
328
    struct Denotator(String);
329

            
330
    impl KeySpecifierComponentViaDisplayFromStr for Denotator {}
331

            
332
    #[test]
333
    fn arti_path_from_path_and_denotators() {
334
        let denotators = [
335
            &Denotator("foo".to_string()) as &dyn KeySpecifierComponent,
336
            &Denotator("bar".to_string()) as &dyn KeySpecifierComponent,
337
            &Denotator("baz".to_string()) as &dyn KeySpecifierComponent,
338
        ];
339

            
340
        /// Base ArtiPaths and the expected outcome from concatenating
341
        /// the base with the denotator group above.
342
        const TEST_PATHS: &[(&str, &str)] = &[
343
            // A base path with no denotator groups
344
            ("my_key_path", "my_key_path+@foo+bar+baz"),
345
            // A base path with a single denotator groups
346
            ("my_key_path+dino+saur", "my_key_path+dino+saur@foo+bar+baz"),
347
            // A base path with two denotator groups
348
            ("my_key_path+dino@saur", "my_key_path+dino@saur@foo+bar+baz"),
349
            // A base path with two empty denotator groups
350
            (
351
                "my_key_path+dino@@@saur",
352
                "my_key_path+dino@@@saur@foo+bar+baz",
353
            ),
354
        ];
355

            
356
        for (base_path, expected_path) in TEST_PATHS {
357
            let path = ArtiPath::new(base_path.to_string()).unwrap();
358
            let expected_path = ArtiPath::new(expected_path.to_string()).unwrap();
359

            
360
            assert_eq!(
361
                ArtiPath::from_path_and_denotators(path.clone(), &denotators[..]).unwrap(),
362
                expected_path
363
            );
364

            
365
            assert_eq!(
366
                ArtiPath::from_path_and_denotators(path.clone(), &[]).unwrap(),
367
                path
368
            );
369
        }
370
    }
371

            
372
    #[test]
373
    #[allow(clippy::cognitive_complexity)]
374
    fn arti_path_validation() {
375
        const VALID_ARTI_PATH_COMPONENTS: &[&str] = &["my-hs-client-2", "hs_client"];
376
        const VALID_ARTI_PATHS: &[&str] = &[
377
            "path/to/client+subvalue+fish",
378
            "_hs_client",
379
            "hs_client-",
380
            "hs_client_",
381
            "_",
382
            // A path with an empty denotator group
383
            "my_key_path+dino@@saur",
384
            // Paths with a trailing empty denotator group.
385
            // Our implementation doesn't encode empty trailing
386
            // denotator groups in ArtiPaths, but our parsing rules
387
            // don't forbid them.
388
            "my_key_path+dino@",
389
            "my_key_path+@",
390
        ];
391

            
392
        const BAD_FIRST_CHAR_ARTI_PATHS: &[&str] = &["-hs_client", "-"];
393

            
394
        const DISALLOWED_CHAR_ARTI_PATHS: &[(&str, char)] = &[
395
            ("client?", '?'),
396
            ("no spaces please", ' '),
397
            ("client٣¾", '٣'),
398
            ("clientß", 'ß'),
399
            // Invalid paths: the main component of the path
400
            // must be separated from the denotator groups by a `+` character
401
            ("my_key_path@", '@'),
402
            ("my_key_path@dino+saur", '@'),
403
        ];
404

            
405
        const EMPTY_PATH_COMPONENT: &[&str] =
406
            &["/////", "/alice/bob", "alice//bob", "alice/bob/", "/"];
407

            
408
        for path in chain!(VALID_ARTI_PATH_COMPONENTS, VALID_ARTI_PATHS) {
409
            assert_ok!(ArtiPath, path);
410
        }
411

            
412
        for (path, bad_char) in DISALLOWED_CHAR_ARTI_PATHS {
413
            assert_err(
414
                path,
415
                ArtiPathSyntaxError::Slug(BadSlug::BadCharacter(*bad_char)),
416
            );
417
        }
418

            
419
        for path in BAD_FIRST_CHAR_ARTI_PATHS {
420
            assert_err(
421
                path,
422
                ArtiPathSyntaxError::Slug(BadSlug::BadFirstCharacter(path.chars().next().unwrap())),
423
            );
424
        }
425

            
426
        for path in EMPTY_PATH_COMPONENT {
427
            assert_err(
428
                path,
429
                ArtiPathSyntaxError::Slug(BadSlug::EmptySlugNotAllowed),
430
            );
431
        }
432

            
433
        const SEP: char = PATH_SEP;
434
        // This is a valid ArtiPath, but not a valid Slug
435
        let path = format!("a{SEP}client{SEP}key+private");
436
        assert_ok!(ArtiPath, path);
437

            
438
        const PATH_WITH_TRAVERSAL: &str = "alice/../bob";
439
        assert_err(
440
            PATH_WITH_TRAVERSAL,
441
            ArtiPathSyntaxError::Slug(BadSlug::BadCharacter('.')),
442
        );
443

            
444
        const REL_PATH: &str = "./bob";
445
        assert_err(
446
            REL_PATH,
447
            ArtiPathSyntaxError::Slug(BadSlug::BadCharacter('.')),
448
        );
449

            
450
        const EMPTY_DENOTATOR: &str = "c++";
451
        assert_err(
452
            EMPTY_DENOTATOR,
453
            ArtiPathSyntaxError::Slug(BadSlug::EmptySlugNotAllowed),
454
        );
455
    }
456

            
457
    #[test]
458
    #[allow(clippy::cognitive_complexity)]
459
    fn arti_path_with_denotator() {
460
        const VALID_ARTI_DENOTATORS: &[&str] = &[
461
            "foo",
462
            "one_two_three-f0ur",
463
            "1-2-3-",
464
            "1-2-3_",
465
            "1-2-3",
466
            "_1-2-3",
467
            "1-2-3",
468
        ];
469

            
470
        const BAD_OUTER_CHAR_DENOTATORS: &[&str] = &["-1-2-3"];
471

            
472
        for denotator in VALID_ARTI_DENOTATORS {
473
            let path = format!("foo/bar/qux+{denotator}");
474
            assert_ok!(ArtiPath, path);
475
        }
476

            
477
        for denotator in BAD_OUTER_CHAR_DENOTATORS {
478
            let path = format!("foo/bar/qux+{denotator}");
479

            
480
            assert_err(
481
                &path,
482
                ArtiPathSyntaxError::Slug(BadSlug::BadFirstCharacter(
483
                    denotator.chars().next().unwrap(),
484
                )),
485
            );
486
        }
487

            
488
        // An ArtiPath with multiple denotators
489
        let path = format!(
490
            "foo/bar/qux+{}+{}+foo",
491
            VALID_ARTI_DENOTATORS[0], VALID_ARTI_DENOTATORS[1]
492
        );
493
        assert_ok!(ArtiPath, path);
494

            
495
        // An invalid ArtiPath with multiple valid denotators and
496
        // an empty (invalid) denotator
497
        let path = format!(
498
            "foo/bar/qux+{}+{}+foo+",
499
            VALID_ARTI_DENOTATORS[0], VALID_ARTI_DENOTATORS[1]
500
        );
501
        assert_err(
502
            &path,
503
            ArtiPathSyntaxError::Slug(BadSlug::EmptySlugNotAllowed),
504
        );
505
    }
506

            
507
    #[test]
508
    fn substring() {
509
        const KEY_PATH: &str = "hello";
510
        let path = ArtiPath::new(KEY_PATH.to_string()).unwrap();
511

            
512
        assert_eq!(path.substring(&(0..1).into()).unwrap(), "h");
513
        assert_eq!(path.substring(&(2..KEY_PATH.len()).into()).unwrap(), "llo");
514
        assert_eq!(
515
            path.substring(&(0..KEY_PATH.len()).into()).unwrap(),
516
            "hello"
517
        );
518
        assert_eq!(path.substring(&(0..KEY_PATH.len() + 1).into()), None);
519
        assert_eq!(path.substring(&(0..0).into()).unwrap(), "");
520
    }
521
}