1use std::str::FromStr;
4
5use derive_deftly::{define_derive_deftly, Deftly};
6use derive_more::{Deref, Display, Into};
7use serde::{Deserialize, Serialize};
8use tor_persist::slug::{self, BadSlug};
9
10use crate::{ArtiPathRange, ArtiPathSyntaxError, KeySpecifierComponent};
11
12define_derive_deftly! {
15 ValidatedString for struct, expect items:
19
20 impl $ttype {
21 #[doc = concat!("Create a new [`", stringify!($tname), "`].")]
22 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#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Deref, Into, Display)] #[derive(Serialize, Deserialize)]
85#[serde(try_from = "String", into = "String")]
86#[derive(Deftly)]
87#[derive_deftly(ValidatedString)]
88pub struct ArtiPath(String);
89
90pub(crate) const PATH_SEP: char = '/';
92
93pub const DENOTATOR_SEP: char = '+';
100
101impl ArtiPath {
102 fn validate_str(inner: &str) -> Result<(), ArtiPathSyntaxError> {
104 let path = if let Some((main_part, denotators)) = inner.split_once(DENOTATOR_SEP) {
106 for d in denotators.split(DENOTATOR_SEP) {
107 let () = slug::check_syntax(d)?;
108 }
109
110 main_part
111 } else {
112 inner
113 };
114
115 if let Some(e) = path
116 .split(PATH_SEP)
117 .map(|s| {
118 if s.is_empty() {
119 Err(BadSlug::EmptySlugNotAllowed.into())
120 } else {
121 Ok(slug::check_syntax(s)?)
122 }
123 })
124 .find(|e| e.is_err())
125 {
126 return e;
127 }
128
129 Ok(())
130 }
131
132 pub fn substring(&self, range: &ArtiPathRange) -> Option<&str> {
153 self.0.get(range.0.clone())
154 }
155
156 pub(crate) fn from_path_and_denotators(
200 path: ArtiPath,
201 cert_denotators: &[&dyn KeySpecifierComponent],
202 ) -> Result<ArtiPath, ArtiPathSyntaxError> {
203 if cert_denotators.is_empty() {
204 return Ok(path);
205 }
206
207 let path: String = [Ok(path.0)]
208 .into_iter()
209 .chain(
210 cert_denotators
211 .iter()
212 .map(|s| s.to_slug().map(|s| s.to_string())),
213 )
214 .collect::<Result<Vec<_>, _>>()?
215 .join(&DENOTATOR_SEP.to_string());
216
217 ArtiPath::new(path)
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 #![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 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 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 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 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}