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}