use std::str::FromStr;
use derive_deftly::{Deftly, define_derive_deftly};
use derive_more::{Deref, Display, Into};
use serde::{Deserialize, Serialize};
use tor_persist::slug::{self, BadSlug};
use crate::{ArtiPathRange, ArtiPathSyntaxError, KeySpecifierComponent};
define_derive_deftly! {
ValidatedString for struct, expect items:
impl $ttype {
#[doc = concat!("Create a new [`", stringify!($tname), "`].")]
pub fn new(inner: String) -> Result<Self, ArtiPathSyntaxError> {
Self::validate_str(&inner)?;
Ok(Self(inner))
}
}
impl TryFrom<String> for $ttype {
type Error = ArtiPathSyntaxError;
fn try_from(s: String) -> Result<Self, ArtiPathSyntaxError> {
Self::new(s)
}
}
impl FromStr for $ttype {
type Err = ArtiPathSyntaxError;
fn from_str(s: &str) -> Result<Self, ArtiPathSyntaxError> {
Self::validate_str(s)?;
Ok(Self(s.to_owned()))
}
}
impl AsRef<str> for $ttype {
fn as_ref(&self) -> &str {
&self.0.as_str()
}
}
}
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Deref, Into, Display)] #[derive(Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
#[derive(Deftly)]
#[derive_deftly(ValidatedString)]
pub struct ArtiPath(String);
pub(crate) const PATH_SEP: char = '/';
pub const DENOTATOR_SEP: char = '+';
pub const DENOTATOR_GROUP_SEP: char = '@';
impl ArtiPath {
fn validate_str(inner: &str) -> Result<(), ArtiPathSyntaxError> {
let path = if let Some((main_part, denotator_groups)) = inner.split_once(DENOTATOR_SEP) {
for denotators in denotator_groups.split(DENOTATOR_GROUP_SEP) {
let () = validate_denotator_group(denotators)?;
}
main_part
} else {
inner
};
if let Some(e) = path
.split(PATH_SEP)
.map(|s| {
if s.is_empty() {
Err(BadSlug::EmptySlugNotAllowed.into())
} else {
Ok(slug::check_syntax(s)?)
}
})
.find(|e| e.is_err())
{
return e;
}
Ok(())
}
pub fn substring(&self, range: &ArtiPathRange) -> Option<&str> {
self.0.get(range.0.clone())
}
pub(crate) fn from_path_and_denotators(
path: ArtiPath,
cert_denotators: &[&dyn KeySpecifierComponent],
) -> Result<ArtiPath, ArtiPathSyntaxError> {
if cert_denotators.is_empty() {
return Ok(path);
}
let cert_denotators = cert_denotators
.iter()
.map(|s| s.to_slug().map(|s| s.to_string()))
.collect::<Result<Vec<_>, _>>()?
.join(&DENOTATOR_SEP.to_string());
let path = if cert_denotators.is_empty() {
format!("{path}")
} else {
if path.contains(DENOTATOR_SEP) {
format!("{path}{DENOTATOR_GROUP_SEP}{cert_denotators}")
} else {
format!("{path}{DENOTATOR_SEP}{DENOTATOR_GROUP_SEP}{cert_denotators}")
}
};
ArtiPath::new(path)
}
}
fn validate_denotator_group(denotators: &str) -> Result<(), ArtiPathSyntaxError> {
if denotators.is_empty() {
return Ok(());
}
for d in denotators.split(DENOTATOR_SEP) {
let () = slug::check_syntax(d)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
use derive_more::{Display, FromStr};
use itertools::chain;
use crate::KeySpecifierComponentViaDisplayFromStr;
impl PartialEq for ArtiPathSyntaxError {
fn eq(&self, other: &Self) -> bool {
use ArtiPathSyntaxError::*;
match (self, other) {
(Slug(err1), Slug(err2)) => err1 == err2,
_ => false,
}
}
}
macro_rules! assert_ok {
($ty:ident, $inner:expr) => {{
let path = $ty::new($inner.to_string());
let path_fromstr: Result<$ty, _> = $ty::try_from($inner.to_string());
let path_tryfrom: Result<$ty, _> = $inner.to_string().try_into();
assert!(path.is_ok(), "{} should be valid", $inner);
assert_eq!(path.as_ref().unwrap().to_string(), *$inner);
assert_eq!(path, path_fromstr);
assert_eq!(path, path_tryfrom);
}};
}
fn assert_err(path: &str, error_kind: ArtiPathSyntaxError) {
let path_anew = ArtiPath::new(path.to_string());
let path_fromstr = ArtiPath::try_from(path.to_string());
let path_tryfrom: Result<ArtiPath, _> = path.to_string().try_into();
assert!(path_anew.is_err(), "{} should be invalid", path);
let actual_err = path_anew.as_ref().unwrap_err();
assert_eq!(actual_err, &error_kind);
assert_eq!(path_anew, path_fromstr);
assert_eq!(path_anew, path_tryfrom);
}
#[derive(Display, FromStr)]
struct Denotator(String);
impl KeySpecifierComponentViaDisplayFromStr for Denotator {}
#[test]
fn arti_path_from_path_and_denotators() {
let denotators = [
&Denotator("foo".to_string()) as &dyn KeySpecifierComponent,
&Denotator("bar".to_string()) as &dyn KeySpecifierComponent,
&Denotator("baz".to_string()) as &dyn KeySpecifierComponent,
];
const TEST_PATHS: &[(&str, &str)] = &[
("my_key_path", "my_key_path+@foo+bar+baz"),
("my_key_path+dino+saur", "my_key_path+dino+saur@foo+bar+baz"),
("my_key_path+dino@saur", "my_key_path+dino@saur@foo+bar+baz"),
(
"my_key_path+dino@@@saur",
"my_key_path+dino@@@saur@foo+bar+baz",
),
];
for (base_path, expected_path) in TEST_PATHS {
let path = ArtiPath::new(base_path.to_string()).unwrap();
let expected_path = ArtiPath::new(expected_path.to_string()).unwrap();
assert_eq!(
ArtiPath::from_path_and_denotators(path.clone(), &denotators[..]).unwrap(),
expected_path
);
assert_eq!(
ArtiPath::from_path_and_denotators(path.clone(), &[]).unwrap(),
path
);
}
}
#[test]
#[allow(clippy::cognitive_complexity)]
fn arti_path_validation() {
const VALID_ARTI_PATH_COMPONENTS: &[&str] = &["my-hs-client-2", "hs_client"];
const VALID_ARTI_PATHS: &[&str] = &[
"path/to/client+subvalue+fish",
"_hs_client",
"hs_client-",
"hs_client_",
"_",
"my_key_path+dino@@saur",
"my_key_path+dino@",
"my_key_path+@",
];
const BAD_FIRST_CHAR_ARTI_PATHS: &[&str] = &["-hs_client", "-"];
const DISALLOWED_CHAR_ARTI_PATHS: &[(&str, char)] = &[
("client?", '?'),
("no spaces please", ' '),
("client٣¾", '٣'),
("clientß", 'ß'),
("my_key_path@", '@'),
("my_key_path@dino+saur", '@'),
];
const EMPTY_PATH_COMPONENT: &[&str] =
&["/////", "/alice/bob", "alice//bob", "alice/bob/", "/"];
for path in chain!(VALID_ARTI_PATH_COMPONENTS, VALID_ARTI_PATHS) {
assert_ok!(ArtiPath, path);
}
for (path, bad_char) in DISALLOWED_CHAR_ARTI_PATHS {
assert_err(
path,
ArtiPathSyntaxError::Slug(BadSlug::BadCharacter(*bad_char)),
);
}
for path in BAD_FIRST_CHAR_ARTI_PATHS {
assert_err(
path,
ArtiPathSyntaxError::Slug(BadSlug::BadFirstCharacter(path.chars().next().unwrap())),
);
}
for path in EMPTY_PATH_COMPONENT {
assert_err(
path,
ArtiPathSyntaxError::Slug(BadSlug::EmptySlugNotAllowed),
);
}
const SEP: char = PATH_SEP;
let path = format!("a{SEP}client{SEP}key+private");
assert_ok!(ArtiPath, path);
const PATH_WITH_TRAVERSAL: &str = "alice/../bob";
assert_err(
PATH_WITH_TRAVERSAL,
ArtiPathSyntaxError::Slug(BadSlug::BadCharacter('.')),
);
const REL_PATH: &str = "./bob";
assert_err(
REL_PATH,
ArtiPathSyntaxError::Slug(BadSlug::BadCharacter('.')),
);
const EMPTY_DENOTATOR: &str = "c++";
assert_err(
EMPTY_DENOTATOR,
ArtiPathSyntaxError::Slug(BadSlug::EmptySlugNotAllowed),
);
}
#[test]
#[allow(clippy::cognitive_complexity)]
fn arti_path_with_denotator() {
const VALID_ARTI_DENOTATORS: &[&str] = &[
"foo",
"one_two_three-f0ur",
"1-2-3-",
"1-2-3_",
"1-2-3",
"_1-2-3",
"1-2-3",
];
const BAD_OUTER_CHAR_DENOTATORS: &[&str] = &["-1-2-3"];
for denotator in VALID_ARTI_DENOTATORS {
let path = format!("foo/bar/qux+{denotator}");
assert_ok!(ArtiPath, path);
}
for denotator in BAD_OUTER_CHAR_DENOTATORS {
let path = format!("foo/bar/qux+{denotator}");
assert_err(
&path,
ArtiPathSyntaxError::Slug(BadSlug::BadFirstCharacter(
denotator.chars().next().unwrap(),
)),
);
}
let path = format!(
"foo/bar/qux+{}+{}+foo",
VALID_ARTI_DENOTATORS[0], VALID_ARTI_DENOTATORS[1]
);
assert_ok!(ArtiPath, path);
let path = format!(
"foo/bar/qux+{}+{}+foo+",
VALID_ARTI_DENOTATORS[0], VALID_ARTI_DENOTATORS[1]
);
assert_err(
&path,
ArtiPathSyntaxError::Slug(BadSlug::EmptySlugNotAllowed),
);
}
#[test]
fn substring() {
const KEY_PATH: &str = "hello";
let path = ArtiPath::new(KEY_PATH.to_string()).unwrap();
assert_eq!(path.substring(&(0..1).into()).unwrap(), "h");
assert_eq!(path.substring(&(2..KEY_PATH.len()).into()).unwrap(), "llo");
assert_eq!(
path.substring(&(0..KEY_PATH.len()).into()).unwrap(),
"hello"
);
assert_eq!(path.substring(&(0..KEY_PATH.len() + 1).into()), None);
assert_eq!(path.substring(&(0..0).into()).unwrap(), "");
}
}