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

            
3
use std::str::FromStr;
4

            
5
use derive_deftly::{define_derive_deftly, 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
29412
        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
25106
        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
3524
        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)]
88
pub struct ArtiPath(String);
89

            
90
/// A separator for `ArtiPath`s.
91
pub(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.
99
pub const DENOTATOR_SEP: char = '+';
100

            
101
impl ArtiPath {
102
    /// Validate the underlying representation of an `ArtiPath`
103
29412
    fn validate_str(inner: &str) -> Result<(), ArtiPathSyntaxError> {
104
        // Validate the denotators, if there are any.
105
29412
        let path = if let Some((main_part, denotators)) = inner.split_once(DENOTATOR_SEP) {
106
17766
            for d in denotators.split(DENOTATOR_SEP) {
107
17766
                let () = slug::check_syntax(d)?;
108
            }
109

            
110
17690
            main_part
111
        } else {
112
11704
            inner
113
        };
114

            
115
29394
        if let Some(e) = path
116
29394
            .split(PATH_SEP)
117
95919
            .map(|s| {
118
94902
                if s.is_empty() {
119
30
                    Err(BadSlug::EmptySlugNotAllowed.into())
120
                } else {
121
94872
                    Ok(slug::check_syntax(s)?)
122
                }
123
95919
            })
124
95919
            .find(|e| e.is_err())
125
        {
126
78
            return e;
127
29316
        }
128
29316

            
129
29316
        Ok(())
130
29412
    }
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
10
    pub fn substring(&self, range: &ArtiPathRange) -> Option<&str> {
153
10
        self.0.get(range.0.clone())
154
10
    }
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
12
    pub(crate) fn from_path_and_denotators(
200
12
        path: ArtiPath,
201
12
        cert_denotators: &[&dyn KeySpecifierComponent],
202
12
    ) -> Result<ArtiPath, ArtiPathSyntaxError> {
203
12
        if cert_denotators.is_empty() {
204
2
            return Ok(path);
205
10
        }
206

            
207
10
        let path: String = [Ok(path.0)]
208
10
            .into_iter()
209
10
            .chain(
210
10
                cert_denotators
211
10
                    .iter()
212
19
                    .map(|s| s.to_slug().map(|s| s.to_string())),
213
10
            )
214
10
            .collect::<Result<Vec<_>, _>>()?
215
10
            .join(&DENOTATOR_SEP.to_string());
216
10

            
217
10
        ArtiPath::new(path)
218
12
    }
219
}
220

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