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 #![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}