use chumsky::{
IterParser, Parser,
error::Rich,
extra,
prelude::{any, just, none_of, one_of},
};
use itertools::{EitherOrBoth, Itertools};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use std::{cmp::Ordering, fmt::Display, str::FromStr};
use thiserror::Error;
#[derive(Debug, DeserializeFromStr, SerializeDisplay, Clone, PartialEq, Eq)]
pub struct SimpleDN {
rdns: Vec<SimpleRDN>,
}
impl Display for SimpleDN {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.rdns.iter().format(","))
}
}
impl FromStr for SimpleDN {
type Err = SimpleDnParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match simple_dn_parser().parse(s).into_result() {
Ok(simple_rdn) => Ok(simple_rdn),
Err(rich_errors) => Err(SimpleDnParseError {
errors: rich_errors
.into_iter()
.map(|rich_err| ToString::to_string(&rich_err))
.collect(),
}),
}
}
}
impl PartialOrd for SimpleDN {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
let most_significant_differing_rdn = self
.rdns
.iter()
.rev()
.zip_longest(other.rdns.iter().rev())
.find(
|maybe_both| !matches!(maybe_both, EitherOrBoth::Both(this, that) if this == that),
);
match most_significant_differing_rdn {
None => Some(Ordering::Equal),
Some(maybe_both) => match maybe_both {
EitherOrBoth::Both(_, _) => None,
EitherOrBoth::Left(_) => Some(Ordering::Less),
EitherOrBoth::Right(_) => Some(Ordering::Greater),
},
}
}
}
pub fn common_ancestor(left: &SimpleDN, right: &SimpleDN) -> Option<SimpleDN> {
let mut common_rdns = left
.rdns
.iter()
.rev()
.zip(right.rdns.iter().rev())
.take_while(|(left, right)| left == right)
.map(|(left, _)| left.clone())
.collect_vec();
common_rdns.reverse();
if common_rdns.is_empty() {
None
} else {
Some(SimpleDN { rdns: common_rdns })
}
}
fn simple_dn_parser<'src>() -> impl Parser<'src, &'src str, SimpleDN, extra::Err<Rich<'src, char>>>
{
simple_rdn_parser()
.separated_by(just(','))
.collect::<Vec<SimpleRDN>>()
.map(|rdns| SimpleDN { rdns })
}
impl SimpleDN {
pub fn get(&self, key: &str) -> Option<&str> {
self.rdns
.iter()
.find(|rdn| rdn.key == key)
.map(|rdn| rdn.value.as_str())
}
pub fn get_starting_from(&self, key: &str) -> Option<SimpleDN> {
self.rdns
.iter()
.position(|rdn| rdn.key == key)
.map(|position| {
let (_, tail) = self.rdns.as_slice().split_at(position);
SimpleDN {
rdns: tail.to_owned(),
}
})
}
pub fn get_type(&self) -> &str {
#[allow(clippy::expect_used, reason = "Relying on struct invariant.")]
&self
.rdns
.first()
.expect("Invariant violation. SimpleDN should never be empty.")
.key
}
pub fn parent(&self) -> Option<SimpleDN> {
match self.rdns.as_slice() {
[_, rest @ ..] if !rest.is_empty() => Some(SimpleDN {
rdns: rest.to_owned(),
}),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)]
#[display("{key}={value}")]
struct SimpleRDN {
pub key: String,
pub value: String,
}
fn simple_rdn_parser<'src>() -> impl Parser<'src, &'src str, SimpleRDN, extra::Err<Rich<'src, char>>>
{
let rdn_key = any()
.filter(|c: &char| c.is_ascii_alphanumeric())
.repeated()
.at_least(1)
.collect::<String>()
.then_ignore(just('='));
let special = r##",\#+<>;"="##;
let escaped = just('\\')
.then(one_of(special))
.to_slice();
let rdn_value = none_of(special)
.to_slice()
.or(escaped)
.repeated()
.at_least(1)
.to_slice()
.map(ToString::to_string);
rdn_key
.then(rdn_value)
.map(|(key, value)| SimpleRDN { key, value })
}
#[derive(Error, Debug)]
#[error("Couldn't parse DN: {:?}", self.errors)]
pub struct SimpleDnParseError {
errors: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
static EXAMPLE_DN: &str = "CN=Yabukita,OU=Green,OU=Tea,DC=Japan";
static EXAMPLE_DN_QUOTED: &str = "\"CN=Yabukita,OU=Green,OU=Tea,DC=Japan\"";
fn example_simple_dn() -> SimpleDN {
SimpleDN {
rdns: vec![
SimpleRDN {
key: String::from("CN"),
value: String::from("Yabukita"),
},
SimpleRDN {
key: String::from("OU"),
value: String::from("Green"),
},
SimpleRDN {
key: String::from("OU"),
value: String::from("Tea"),
},
SimpleRDN {
key: String::from("DC"),
value: String::from("Japan"),
},
],
}
}
#[test]
fn parse_simple_rdn_ok() {
let key = "CN";
let value = "Tea Drinker";
let unstructured = String::new() + key + "=" + value;
let rdn = simple_rdn_parser()
.parse(&unstructured)
.into_result()
.unwrap();
assert_eq!(key, rdn.key);
assert_eq!(value, rdn.value);
}
#[test]
fn parse_simple_rdn_fail() {
let key = "CN";
let value = "Tea Drinker";
let unstructured = String::new() + key + "=" + value + "+ANOTHER=5";
let parse_result = simple_rdn_parser().parse(&unstructured).into_result();
let errors = parse_result.unwrap_err();
println!("{errors:#?}");
}
#[test]
fn parse_sipmle_dn_ok() {
let parsed_dn = simple_dn_parser().parse(EXAMPLE_DN).into_result().unwrap();
assert_eq!(parsed_dn, example_simple_dn());
}
#[test]
fn parse_complex_dn() {
"CN=one+OTHER=two,OU=some,DC=thing"
.parse::<SimpleDN>()
.expect_err("Multivalued DN should be rejected.");
}
#[test]
fn parse_dn_escapes() -> anyhow::Result<()> {
let parsed = SimpleDN::from_str(r"CN=tea \+ milk \= milktea,OU=mixes,DC=odd\,domain")?;
let expected = SimpleDN {
rdns: vec![
SimpleRDN {
key: String::from("CN"),
value: String::from("tea \\+ milk \\= milktea"),
},
SimpleRDN {
key: String::from("OU"),
value: String::from("mixes"),
},
SimpleRDN {
key: String::from("DC"),
value: String::from("odd\\,domain"),
},
],
};
assert_eq!(parsed, expected);
Ok(())
}
#[test]
fn dispaly_simple_dn() {
let displayed = example_simple_dn().to_string();
assert_eq!(displayed, EXAMPLE_DN);
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(transparent)]
struct DnStruct {
pub dn: SimpleDN,
}
impl DnStruct {
fn example() -> Self {
DnStruct {
dn: example_simple_dn(),
}
}
}
#[test]
fn serialize() -> anyhow::Result<()> {
let serialized = serde_json::to_string(&DnStruct::example())?;
assert_eq!(serialized, EXAMPLE_DN_QUOTED);
Ok(())
}
#[test]
fn deserialize() -> anyhow::Result<()> {
let deserialized: DnStruct = serde_json::from_str(EXAMPLE_DN_QUOTED)?;
assert_eq!(deserialized.dn, DnStruct::example().dn);
Ok(())
}
#[test]
fn get() {
let example_dn = example_simple_dn();
assert_eq!(example_dn.get("OU"), Some("Green"));
assert_eq!(example_dn.get("CN"), Some("Yabukita"));
assert_eq!(example_dn.get("Nonsense"), None);
}
#[test]
fn get_type() {
assert_eq!(example_simple_dn().get_type(), "CN");
}
#[test]
fn get_parent() {
let parent = example_simple_dn().parent();
let correct_parent = SimpleDN {
rdns: vec![
SimpleRDN {
key: String::from("OU"),
value: String::from("Green"),
},
SimpleRDN {
key: String::from("OU"),
value: String::from("Tea"),
},
SimpleRDN {
key: String::from("DC"),
value: String::from("Japan"),
},
],
};
assert_eq!(parent, Some(correct_parent.clone()));
let no_parents = SimpleDN {
rdns: vec![SimpleRDN {
key: String::from("DC"),
value: String::from("Tea"),
}],
};
assert_eq!(no_parents.parent(), None);
}
#[test]
fn get_starting_from() {
let example_dn = example_simple_dn();
let got = example_dn.get_starting_from("OU");
let correct = example_dn.parent();
assert!(got.is_some());
assert_eq!(got, correct);
let non_existent = example_dn.get_starting_from("Coffee");
assert_eq!(non_existent, None);
}
#[test]
fn get_type_starting_from() {
let example_dn = example_simple_dn();
let dn_type = example_dn.get_type();
let starting_from = example_dn.get_starting_from(dn_type);
assert_eq!(starting_from, Some(example_dn));
}
#[test]
fn partial_compare() {
let reflexivity = example_simple_dn().partial_cmp(&example_simple_dn());
assert_eq!(reflexivity, Some(Ordering::Equal));
let great = SimpleDN {
rdns: vec![SimpleRDN {
key: String::from("DC"),
value: String::from("Big"),
}],
};
let lesser = SimpleDN {
rdns: vec![
SimpleRDN {
key: String::from("OU"),
value: String::from("Medium"),
},
SimpleRDN {
key: String::from("DC"),
value: String::from("Big"),
},
],
};
assert_eq!(great.partial_cmp(&lesser), Some(Ordering::Greater));
assert_eq!(lesser.partial_cmp(&great), Some(Ordering::Less));
let incomparable = SimpleDN {
rdns: vec![
SimpleRDN {
key: String::from("OU"),
value: String::from("Else"),
},
SimpleRDN {
key: String::from("DC"),
value: String::from("Big"),
},
],
};
assert!(lesser.partial_cmp(&incomparable).is_none());
assert!(incomparable.partial_cmp(&lesser).is_none());
}
#[test]
fn test_common_ancestor() -> anyhow::Result<()> {
let left = SimpleDN::from_str("CN=puerh,OU=post-fermented,DC=tea")?;
let right = SimpleDN::from_str("CN=liu an,OU=post-fermented,DC=tea")?;
let correct_ancestor = SimpleDN::from_str("OU=post-fermented,DC=tea")?;
let found_ancestor = common_ancestor(&left, &right);
assert_eq!(found_ancestor, Some(correct_ancestor));
Ok(())
}
}