1use std::{
3 collections::{HashMap, HashSet},
4 hash::{Hash, Hasher},
5};
6
7use educe::Educe;
8use oid::ObjectIdentifier;
9
10#[cfg(feature = "chumsky")]
11use chumsky::{prelude::*, text::digits};
12
13#[cfg(feature = "chumsky")]
14use itertools::Itertools as _;
15
16#[cfg(feature = "chumsky")]
17use ariadne::{Color, Fmt as _, Label, Report, ReportKind, Source};
18
19#[cfg(feature = "serde")]
20use serde::{
21 de::SeqAccess, ser::SerializeSeq as _, Deserialize, Deserializer, Serialize, Serializer,
22};
23
24#[cfg(feature = "diff")]
25use diff::Diff;
26
27#[cfg(feature = "chumsky")]
31#[derive(Debug)]
32pub struct ChumskyError<E> {
33 pub description: String,
35 pub source: String,
37 pub errors: Vec<E>,
39}
40
41#[cfg(feature = "chumsky")]
42impl std::fmt::Display for ChumskyError<chumsky::error::Rich<'static, char>> {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 for e in &self.errors {
45 let msg = format!(
46 "While parsing {}: {}{}, expected {}",
47 self.description,
48 if e.found().is_some() {
49 "Unexpected token"
50 } else {
51 "Unexpected end of input"
52 },
53 format_args!(" while parsing {:?}", e.contexts().collect::<Vec<_>>()),
54 if e.expected().len() == 0 {
55 "end of input".to_string()
56 } else {
57 e.expected()
58 .map(|rich_pattern| rich_pattern.to_string())
59 .collect::<Vec<_>>()
60 .join(", ")
61 },
62 );
63
64 let report = Report::build(ReportKind::Error, e.span().start..e.span().end)
65 .with_code(3)
66 .with_message(msg)
67 .with_label(
68 Label::new(e.span().start..e.span().end)
69 .with_message(format!(
70 "Unexpected {}",
71 e.found().map_or_else(
72 || "end of input".to_string(),
73 |c| format!("token {}", c.fg(Color::Red))
74 )
75 ))
76 .with_color(Color::Red),
77 );
78
79 let report = match e.reason() {
80 chumsky::error::RichReason::ExpectedFound {
81 expected: _,
82 found: _,
83 } => report,
84 chumsky::error::RichReason::Custom(msg) => report.with_label(
85 Label::new(e.span().start..e.span().end)
86 .with_message(format!("{}", msg.fg(Color::Yellow)))
87 .with_color(Color::Yellow),
88 ),
89 };
90
91 let mut s: Vec<u8> = Vec::new();
92 report
93 .finish()
94 .write(Source::from(&self.source), &mut s)
95 .map_err(|_err| <std::fmt::Error as std::default::Default>::default())?;
96 let s = std::str::from_utf8(&s)
97 .map_err(|_err| <std::fmt::Error as std::default::Default>::default())?;
98 write!(f, "{s}")?;
99 }
100 Ok(())
101 }
102}
103
104#[cfg(feature = "chumsky")]
105impl<E> std::error::Error for ChumskyError<E>
106where
107 E: std::fmt::Debug,
108 Self: std::fmt::Display,
109{
110 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
111 None
112 }
113}
114
115#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
122pub struct RootDSE {
123 pub supported_ldap_version: String,
125 pub supported_controls: Vec<ObjectIdentifier>,
129 pub supported_extensions: Vec<ObjectIdentifier>,
133 pub supported_features: Vec<ObjectIdentifier>,
137 pub supported_sasl_mechanisms: Vec<String>,
141 pub config_context: String,
145 pub naming_contexts: Vec<String>,
152 pub subschema_subentry: String,
159}
160
161impl std::fmt::Debug for RootDSE {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
163 f.debug_struct("RootDSE")
164 .field("supported_ldap_version", &self.supported_ldap_version)
165 .field(
166 "supported_controls",
167 &self
168 .supported_controls
169 .iter()
170 .map(|x| x.into())
171 .collect::<Vec<String>>(),
172 )
173 .field(
174 "supported_extensions",
175 &self
176 .supported_extensions
177 .iter()
178 .map(|x| x.into())
179 .collect::<Vec<String>>(),
180 )
181 .field(
182 "supported_features",
183 &self
184 .supported_features
185 .iter()
186 .map(|x| x.into())
187 .collect::<Vec<String>>(),
188 )
189 .field("supported_sasl_mechanisms", &self.supported_sasl_mechanisms)
190 .field("config_context", &self.config_context)
191 .field("naming_contexts", &self.naming_contexts)
192 .field("subschema_subentry", &self.subschema_subentry)
193 .finish()
194 }
195}
196
197#[cfg(feature = "chumsky")]
199#[must_use]
200pub fn oid_parser<'src>(
201) -> impl Parser<'src, &'src str, ObjectIdentifier, extra::Err<Rich<'src, char>>> {
202 digits(10)
203 .collect::<String>()
204 .separated_by(just('.'))
205 .collect::<Vec<_>>()
206 .try_map(|x, span| {
207 x.into_iter()
208 .join(".")
209 .try_into()
210 .map_err(|e| Rich::custom(span, format!("{e:?}")))
211 })
212}
213
214#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Hash)]
217#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
218pub struct KeyString(pub String);
219
220impl std::fmt::Display for KeyString {
221 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
222 std::fmt::Display::fmt(&self.0, f)?;
223 Ok(())
224 }
225}
226
227impl KeyString {
228 #[must_use]
234 pub fn describes_case_insensitive_match(&self) -> bool {
235 match self {
236 Self(s) if s == "objectIdentifierMatch" => true,
237 Self(s) if s == "caseIgnoreMatch" => true,
238 Self(s) if s == "caseIgnoreListMatch" => true,
239 Self(s) if s == "caseIgnoreIA5Match" => true,
240 Self(s) if s == "caseIgnoreListSubstringsMatch" => true,
241 Self(s) if s == "caseIgnoreSubstringsMatch" => true,
242 Self(s) if s == "caseIgnoreOrderingMatch" => true,
243 Self(s) if s == "caseIgnoreIA5SubstringsMatch" => true,
244 _ => false,
245 }
246 }
247
248 #[must_use]
250 pub fn to_lowercase(&self) -> Self {
251 let Self(s) = self;
252 Self(s.to_lowercase())
253 }
254}
255
256impl TryFrom<KeyStringOrOID> for KeyString {
257 type Error = ();
258
259 fn try_from(value: KeyStringOrOID) -> Result<Self, Self::Error> {
260 match value {
261 KeyStringOrOID::KeyString(ks) => Ok(ks),
262 KeyStringOrOID::OID(_) => Err(()),
263 }
264 }
265}
266
267impl TryFrom<&KeyStringOrOID> for KeyString {
268 type Error = ();
269
270 fn try_from(value: &KeyStringOrOID) -> Result<Self, Self::Error> {
271 match value {
272 KeyStringOrOID::KeyString(ks) => Ok(ks.to_owned()),
273 KeyStringOrOID::OID(_) => Err(()),
274 }
275 }
276}
277
278#[cfg(feature = "chumsky")]
280pub fn keystring_parser<'src>(
281) -> impl Parser<'src, &'src str, KeyString, extra::Err<Rich<'src, char>>> {
282 any()
283 .filter(|c: &char| c.is_ascii_alphabetic())
284 .then(
285 any()
286 .filter(|c: &char| c.is_ascii_alphanumeric() || *c == '-' || *c == ';')
287 .repeated()
288 .collect::<String>(),
289 )
290 .map(|(c, rest)| format!("{c}{rest}"))
291 .map(KeyString)
292}
293
294#[cfg(feature = "chumsky")]
296#[must_use]
297pub fn quoted_keystring_parser<'src>(
298) -> impl Parser<'src, &'src str, KeyString, extra::Err<Rich<'src, char>>> {
299 keystring_parser().delimited_by(just('\''), just('\''))
300}
301
302pub fn hash_oid<H: Hasher>(s: &ObjectIdentifier, state: &mut H) {
305 Hash::hash(&format!("{s:?}"), state);
306}
307
308#[derive(Clone, Debug, enum_as_inner::EnumAsInner, Educe)]
311#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
312#[educe(PartialEq, Eq, Hash)]
313pub enum KeyStringOrOID {
314 #[cfg_attr(feature = "serde", serde(rename = "key_string"))]
316 KeyString(KeyString),
317 #[cfg_attr(feature = "serde", serde(rename = "oid"))]
319 OID(#[educe(Hash(method = "hash_oid"))] ObjectIdentifier),
320}
321
322impl PartialOrd for KeyStringOrOID {
323 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
324 Some(self.cmp(other))
325 }
326}
327
328impl Ord for KeyStringOrOID {
329 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
330 match (self, other) {
331 (Self::KeyString(s1), Self::KeyString(s2)) => s1.cmp(s2),
332 (Self::KeyString(_), Self::OID(_)) => std::cmp::Ordering::Less,
333 (Self::OID(_), Self::KeyString(_)) => std::cmp::Ordering::Greater,
334 (Self::OID(oid1), Self::OID(oid2)) => {
335 let s1: String = oid1.into();
336 let s2: String = oid2.into();
337 s1.cmp(&s2)
338 }
339 }
340 }
341}
342
343impl std::fmt::Display for KeyStringOrOID {
344 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
345 match &self {
346 Self::KeyString(s) => {
347 std::fmt::Display::fmt(s, f)?;
348 Ok(())
349 }
350 Self::OID(oid) => {
351 let string_oid: String = oid.clone().into();
352 std::fmt::Display::fmt(&string_oid, f)?;
353 Ok(())
354 }
355 }
356 }
357}
358
359#[cfg(feature = "chumsky")]
360impl TryFrom<&str> for KeyStringOrOID {
361 type Error = ChumskyError<chumsky::error::Rich<'static, char>>;
362 fn try_from(value: &str) -> Result<Self, Self::Error> {
363 (keystring_or_oid_parser().then_ignore(chumsky::primitive::end()))
364 .parse(value)
365 .into_result()
366 .map_err(|e| ChumskyError {
367 description: "keystring or OID".to_string(),
368 source: value.to_string(),
369 errors: e.into_iter().map(|e| e.into_owned()).collect(),
370 })
371 }
372}
373
374#[cfg(feature = "chumsky")]
375impl TryFrom<String> for KeyStringOrOID {
376 type Error = ChumskyError<chumsky::error::Rich<'static, char>>;
377 fn try_from(value: String) -> Result<Self, Self::Error> {
378 (keystring_or_oid_parser().then_ignore(chumsky::primitive::end()))
379 .parse(&value)
380 .into_result()
381 .map_err(|e| ChumskyError {
382 description: "keystring or OID".to_string(),
383 source: value.to_string(),
384 errors: e.into_iter().map(|e| e.into_owned()).collect(),
385 })
386 }
387}
388
389#[cfg(feature = "chumsky")]
390impl<'src> TryFrom<&'src String> for KeyStringOrOID {
391 type Error = ChumskyError<chumsky::error::Rich<'static, char>>;
392 fn try_from(value: &'src String) -> Result<Self, Self::Error> {
393 (keystring_or_oid_parser().then_ignore(chumsky::primitive::end()))
394 .parse(value)
395 .into_result()
396 .map_err(|e| ChumskyError {
397 description: "keystring or OID".to_string(),
398 source: value.to_string(),
399 errors: e.into_iter().map(|e| e.into_owned()).collect(),
400 })
401 }
402}
403
404#[cfg(feature = "chumsky")]
405impl std::str::FromStr for KeyStringOrOID {
406 type Err = ChumskyError<chumsky::error::Rich<'static, char>>;
407
408 fn from_str(s: &str) -> Result<Self, Self::Err> {
409 (keystring_or_oid_parser().then_ignore(chumsky::primitive::end()))
410 .parse(s)
411 .into_result()
412 .map_err(|e| ChumskyError {
413 description: "keystring or OID".to_string(),
414 source: s.to_string(),
415 errors: e.into_iter().map(|e| e.into_owned()).collect(),
416 })
417 }
418}
419
420impl From<&Self> for KeyStringOrOID {
421 fn from(value: &Self) -> Self {
422 value.to_owned()
423 }
424}
425
426impl From<KeyString> for KeyStringOrOID {
427 fn from(value: KeyString) -> Self {
428 Self::KeyString(value)
429 }
430}
431
432impl From<&KeyString> for KeyStringOrOID {
433 fn from(value: &KeyString) -> Self {
434 Self::KeyString(value.to_owned())
435 }
436}
437
438impl From<ObjectIdentifier> for KeyStringOrOID {
439 fn from(value: ObjectIdentifier) -> Self {
440 Self::OID(value)
441 }
442}
443
444impl From<&ObjectIdentifier> for KeyStringOrOID {
445 fn from(value: &ObjectIdentifier) -> Self {
446 Self::OID(value.to_owned())
447 }
448}
449
450impl TryFrom<KeyStringOrOID> for ObjectIdentifier {
451 type Error = ();
452
453 fn try_from(value: KeyStringOrOID) -> Result<Self, Self::Error> {
454 match value {
455 KeyStringOrOID::OID(oid) => Ok(oid),
456 KeyStringOrOID::KeyString(_) => Err(()),
457 }
458 }
459}
460
461impl TryFrom<&KeyStringOrOID> for ObjectIdentifier {
462 type Error = ();
463
464 fn try_from(value: &KeyStringOrOID) -> Result<Self, Self::Error> {
465 match value {
466 KeyStringOrOID::OID(oid) => Ok(oid.to_owned()),
467 KeyStringOrOID::KeyString(_) => Err(()),
468 }
469 }
470}
471
472#[cfg(feature = "chumsky")]
474pub fn keystring_or_oid_parser<'src>(
475) -> impl Parser<'src, &'src str, KeyStringOrOID, extra::Err<Rich<'src, char>>> {
476 keystring_parser()
477 .map(KeyStringOrOID::KeyString)
478 .or(oid_parser().map(KeyStringOrOID::OID))
479}
480
481#[derive(Clone, Educe)]
484#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
485#[educe(PartialEq, Eq, Hash)]
486pub struct OIDWithLength {
487 #[educe(Hash(method = "hash_oid"))]
489 pub oid: ObjectIdentifier,
490 pub length: Option<usize>,
492}
493
494impl From<OIDWithLength> for ObjectIdentifier {
495 fn from(value: OIDWithLength) -> Self {
496 value.oid
497 }
498}
499
500impl From<&OIDWithLength> for ObjectIdentifier {
501 fn from(value: &OIDWithLength) -> Self {
502 value.oid.to_owned()
503 }
504}
505
506impl std::fmt::Debug for OIDWithLength {
507 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
508 let string_oid: String = self.oid.clone().into();
509 f.debug_struct("OIDWithLength")
510 .field("oid", &string_oid)
511 .field("length", &self.length)
512 .finish()
513 }
514}
515
516#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
522#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
523pub struct RelativeDistinguishedName {
524 #[cfg_attr(
526 feature = "serde",
527 serde(serialize_with = "serialize_rdn", deserialize_with = "deserialize_rdn")
528 )]
529 pub attributes: Vec<(KeyStringOrOID, Vec<u8>)>,
530}
531
532impl std::fmt::Display for RelativeDistinguishedName {
533 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
534 let mut first = true;
535 for (k, v) in &self.attributes {
536 if !first {
537 write!(f, "+")?;
538 } else {
539 first = false;
540 }
541 write!(f, "{k}")?;
542 write!(f, "=")?;
543 if let Ok(s) = std::str::from_utf8(v) {
544 write!(f, "{s}")?;
545 } else {
546 write!(f, "#{}", hex::encode(v))?;
547 }
548 }
549 Ok(())
550 }
551}
552
553#[cfg(feature = "chumsky")]
554impl TryFrom<&str> for RelativeDistinguishedName {
555 type Error = ChumskyError<chumsky::error::Rich<'static, char>>;
556
557 fn try_from(value: &str) -> Result<Self, Self::Error> {
558 (rdn_parser().then_ignore(chumsky::primitive::end()))
559 .parse(value)
560 .into_result()
561 .map_err(|e| ChumskyError {
562 description: "relative distinguished name".to_string(),
563 source: value.to_string(),
564 errors: e.into_iter().map(|e| e.into_owned()).collect(),
565 })
566 }
567}
568
569#[cfg(feature = "chumsky")]
570impl TryFrom<String> for RelativeDistinguishedName {
571 type Error = ChumskyError<chumsky::error::Rich<'static, char>>;
572
573 fn try_from(value: String) -> Result<Self, Self::Error> {
574 (rdn_parser().then_ignore(chumsky::primitive::end()))
575 .parse(&value)
576 .into_result()
577 .map_err(|e| ChumskyError {
578 description: "relative distinguished name".to_string(),
579 source: value.to_string(),
580 errors: e.into_iter().map(|e| e.into_owned()).collect(),
581 })
582 }
583}
584
585#[cfg(feature = "chumsky")]
586impl std::str::FromStr for RelativeDistinguishedName {
587 type Err = ChumskyError<chumsky::error::Rich<'static, char>>;
588
589 fn from_str(s: &str) -> Result<Self, Self::Err> {
590 (rdn_parser().then_ignore(chumsky::primitive::end()))
591 .parse(s)
592 .into_result()
593 .map_err(|e| ChumskyError {
594 description: "relative distinguished name".to_string(),
595 source: s.to_string(),
596 errors: e.into_iter().map(|e| e.into_owned()).collect(),
597 })
598 }
599}
600
601impl From<RelativeDistinguishedName> for String {
602 fn from(rdn: RelativeDistinguishedName) -> Self {
603 rdn.to_string()
604 }
605}
606
607#[cfg(feature = "serde")]
614pub fn serialize_rdn<S>(xs: &[(KeyStringOrOID, Vec<u8>)], s: S) -> Result<S::Ok, S::Error>
615where
616 S: Serializer,
617{
618 let mut seq = s.serialize_seq(Some(xs.len()))?;
619 for e @ (k, v) in xs {
620 if let Ok(s) = std::str::from_utf8(v) {
621 seq.serialize_element(&(k, s))?;
622 } else {
623 seq.serialize_element(e)?;
624 }
625 }
626 seq.end()
627}
628
629#[cfg(feature = "serde")]
635pub fn deserialize_rdn<'de, D>(d: D) -> Result<Vec<(KeyStringOrOID, Vec<u8>)>, D::Error>
636where
637 D: Deserializer<'de>,
638{
639 #[derive(Deserialize)]
641 #[serde(untagged)]
642 enum StringOrBytes {
643 String(String),
645 Bytes(Vec<u8>),
647 }
648
649 struct RDNVisitor;
651
652 impl<'de> serde::de::Visitor<'de> for RDNVisitor {
653 type Value = Vec<(KeyStringOrOID, StringOrBytes)>;
654
655 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
656 write!(formatter, "an array of tuples of attribute name and attribute value (either a string or a sequence of integers)")
657 }
658
659 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
660 where
661 A: SeqAccess<'de>,
662 {
663 let mut result = Vec::new();
664 while let Some(e) = seq.next_element()? {
665 result.push(e);
666 }
667 Ok(result)
668 }
669 }
670
671 let parse_result = d.deserialize_seq(RDNVisitor)?;
672 let mut results = Vec::new();
673 for (ref k, ref v) in parse_result {
674 match v {
675 StringOrBytes::String(s) => {
676 results.push((k.to_owned(), s.as_bytes().to_vec()));
677 }
678 StringOrBytes::Bytes(b) => results.push((k.to_owned(), b.to_vec())),
679 }
680 }
681 Ok(results)
682}
683
684#[cfg(feature = "chumsky")]
686#[expect(
687 clippy::missing_panics_doc,
688 reason = "the panic from the unwrap can never happen since we are decoding exactly 2 hex digits so we have exactly 1 byte of output, never 0"
689)]
690#[must_use]
691pub fn hex_byte_parser<'src>() -> impl Parser<'src, &'src str, u8, extra::Err<Rich<'src, char>>> {
692 any()
693 .filter(|c: &char| c.is_ascii_hexdigit())
694 .repeated()
695 .exactly(2)
696 .collect::<String>()
697 .try_map(|ds, span| {
698 hex::decode(ds.as_bytes()).map_err(|e| Rich::custom(span, format!("{e:?}")))
699 })
700 .map(|v: Vec<u8>| {
701 #[expect(clippy::unwrap_used, reason = "since we collect exactly two hex digits the result of hex::decode should be exactly 1 u8 long")]
702 v.first().unwrap().to_owned()
703 })
704}
705
706#[cfg(feature = "chumsky")]
708#[must_use]
709pub fn rdn_attribute_binary_value_parser<'src>(
710) -> impl Parser<'src, &'src str, Vec<u8>, extra::Err<Rich<'src, char>>> {
711 just('#').ignore_then(hex_byte_parser().repeated().collect())
712}
713
714#[cfg(feature = "chumsky")]
716#[must_use]
717pub fn rdn_attribute_string_value_parser<'src>(
718) -> impl Parser<'src, &'src str, Vec<u8>, extra::Err<Rich<'src, char>>> {
719 none_of(",+\"\\<>;")
720 .or(just('\\').ignore_then(one_of(" ,+\"\\<>;")))
721 .or(just('\\').ignore_then(hex_byte_parser().map(char::from)))
722 .repeated()
723 .collect::<String>()
724 .map(|s| s.as_bytes().to_vec())
725}
726
727#[cfg(feature = "chumsky")]
729#[must_use]
730pub fn rdn_attribute_value_parser<'src>(
731) -> impl Parser<'src, &'src str, Vec<u8>, extra::Err<Rich<'src, char>>> {
732 rdn_attribute_binary_value_parser().or(rdn_attribute_string_value_parser())
733}
734
735#[cfg(feature = "chumsky")]
737#[must_use]
738pub fn rdn_parser<'src>(
739) -> impl Parser<'src, &'src str, RelativeDistinguishedName, extra::Err<Rich<'src, char>>> {
740 keystring_or_oid_parser()
741 .then(just('=').ignore_then(rdn_attribute_value_parser()))
742 .separated_by(just('+'))
743 .at_least(1)
744 .collect()
745 .map(|attributes| RelativeDistinguishedName { attributes })
746}
747
748#[derive(Debug, PartialEq, Eq, Clone, Hash)]
754#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
755pub struct DistinguishedName {
756 pub rdns: Vec<RelativeDistinguishedName>,
758}
759
760impl DistinguishedName {
761 #[must_use]
763 pub const fn is_empty(&self) -> bool {
764 self.rdns.is_empty()
765 }
766 #[must_use]
769 pub fn parent(&self) -> Option<Self> {
770 if self.is_empty() {
771 None
772 } else {
773 Some(Self {
774 rdns: self.rdns.iter().skip(1).cloned().collect(),
775 })
776 }
777 }
778
779 #[must_use]
784 pub fn is_ancestor_of(&self, other: &Self) -> bool {
785 let mut it = self.rdns.iter().rev();
786 let mut other_it = other.rdns.iter().rev();
787 loop {
788 let e = it.next();
789 let other_e = other_it.next();
790 match (e, other_e) {
791 (None | Some(_), None) => {
794 return false;
795 }
796 (None, Some(_)) => {
797 return true;
800 }
801 (Some(e), Some(other_e)) => {
802 if e != other_e {
803 return false;
806 }
807 }
810 }
811 }
812 }
813
814 #[must_use]
816 pub fn add_suffix(&self, other: &Self) -> Self {
817 Self {
818 rdns: [self.rdns.to_vec(), other.rdns.to_vec()].concat(),
819 }
820 }
821
822 #[must_use]
824 pub fn strip_suffix(&self, other: &Self) -> Option<Self> {
825 if !other.is_ancestor_of(self) {
826 None
827 } else {
828 let self_len = self.rdns.len();
829 let other_len = other.rdns.len();
830 Some(Self {
831 rdns: self
832 .rdns
833 .split_at(self_len.saturating_sub(other_len))
834 .0
835 .to_vec(),
836 })
837 }
838 }
839}
840
841#[cfg(feature = "chumsky")]
842impl TryFrom<&str> for DistinguishedName {
843 type Error = ChumskyError<chumsky::error::Rich<'static, char>>;
844
845 fn try_from(value: &str) -> Result<Self, Self::Error> {
846 (dn_parser().then_ignore(chumsky::primitive::end()))
847 .parse(value)
848 .into_result()
849 .map_err(|e| ChumskyError {
850 description: "distinguished name".to_string(),
851 source: value.to_string(),
852 errors: e.into_iter().map(|e| e.into_owned()).collect(),
853 })
854 }
855}
856
857#[cfg(feature = "chumsky")]
858impl TryFrom<String> for DistinguishedName {
859 type Error = ChumskyError<chumsky::error::Rich<'static, char>>;
860
861 fn try_from(value: String) -> Result<Self, Self::Error> {
862 (dn_parser().then_ignore(chumsky::primitive::end()))
863 .parse(&value)
864 .into_result()
865 .map_err(|e| ChumskyError {
866 description: "distinguished name".to_string(),
867 source: value.to_string(),
868 errors: e.into_iter().map(|e| e.into_owned()).collect(),
869 })
870 }
871}
872
873#[cfg(feature = "chumsky")]
874impl std::str::FromStr for DistinguishedName {
875 type Err = ChumskyError<chumsky::error::Rich<'static, char>>;
876
877 fn from_str(s: &str) -> Result<Self, Self::Err> {
878 (dn_parser().then_ignore(chumsky::primitive::end()))
879 .parse(s)
880 .into_result()
881 .map_err(|e| ChumskyError {
882 description: "distinguished name".to_string(),
883 source: s.to_string(),
884 errors: e.into_iter().map(|e| e.into_owned()).collect(),
885 })
886 }
887}
888
889impl From<DistinguishedName> for String {
890 fn from(dn: DistinguishedName) -> Self {
891 dn.to_string()
892 }
893}
894
895impl std::fmt::Display for DistinguishedName {
896 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
897 let mut first = true;
898 for rdn in &self.rdns {
899 if !first {
900 write!(f, ",")?;
901 } else {
902 first = false;
903 }
904 write!(f, "{rdn}")?;
905 }
906 Ok(())
907 }
908}
909
910impl PartialOrd for DistinguishedName {
911 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
912 Some(self.cmp(other))
913 }
914}
915
916impl Ord for DistinguishedName {
917 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
918 self.rdns
919 .iter()
920 .rev()
921 .zip(other.rdns.iter().rev())
922 .map(|(a, b)| a.cmp(b))
923 .fold(std::cmp::Ordering::Equal, |acc, e| acc.then(e))
924 .then(self.rdns.len().cmp(&other.rdns.len()))
925 }
926}
927
928#[cfg(feature = "chumsky")]
930#[must_use]
931pub fn dn_parser<'src>(
932) -> impl Parser<'src, &'src str, DistinguishedName, extra::Err<Rich<'src, char>>> {
933 rdn_parser()
934 .separated_by(just(','))
935 .collect()
936 .map(|rdns| DistinguishedName { rdns })
937}
938
939#[derive(Debug, Clone, PartialEq, Eq)]
943#[cfg_attr(feature = "diff", derive(Diff))]
944#[cfg_attr(feature = "diff", diff(attr(#[derive(Debug)] #[expect(missing_docs, reason = "there is no way we can add docs to the derived Diff code")])))]
945#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
946pub struct LDAPEntry {
947 pub dn: String,
949 pub attrs: HashMap<String, Vec<String>>,
951 pub bin_attrs: HashMap<String, Vec<Vec<u8>>>,
953}
954
955impl LDAPEntry {
956 #[must_use]
958 pub fn combined_attrs(&self) -> Vec<(Vec<u8>, HashSet<Vec<u8>>)> {
959 let mut result: HashMap<Vec<u8>, HashSet<Vec<u8>>> = HashMap::new();
960 for (attr_name, attr_values) in &self.attrs {
961 let attr_name = attr_name.as_bytes().to_vec();
962 let attr_values = attr_values.iter().map(|x| x.as_bytes().to_vec()).collect();
963 if let Some(values) = result.get_mut(&attr_name) {
964 values.extend(attr_values);
965 } else {
966 result.insert(attr_name, attr_values);
967 }
968 }
969 for (attr_name, attr_values) in &self.bin_attrs {
970 let attr_name = attr_name.as_bytes().to_vec();
971 let attr_values = attr_values.iter().map(|x| x.to_vec()).collect();
972 if let Some(values) = result.get_mut(&attr_name) {
973 values.extend(attr_values);
974 } else {
975 result.insert(attr_name, attr_values);
976 }
977 }
978 result.into_iter().collect()
979 }
980}
981
982#[cfg(feature = "ldap3")]
983impl From<ldap3::SearchEntry> for LDAPEntry {
984 fn from(entry: ldap3::SearchEntry) -> Self {
985 Self {
986 dn: entry.dn,
987 attrs: entry.attrs,
988 bin_attrs: entry.bin_attrs,
989 }
990 }
991}
992
993#[cfg(feature = "ldap3")]
994impl From<LDAPEntry> for ldap3::SearchEntry {
995 fn from(entry: LDAPEntry) -> Self {
996 Self {
997 dn: entry.dn,
998 attrs: entry.attrs,
999 bin_attrs: entry.bin_attrs,
1000 }
1001 }
1002}
1003
1004#[derive(Debug, Clone)]
1008#[cfg(feature = "ldap3")]
1009pub enum LDAPOperation {
1010 Add(LDAPEntry),
1012 Delete {
1014 dn: String,
1016 },
1017 Modify {
1019 dn: String,
1021 mods: Vec<ldap3::Mod<String>>,
1023 bin_mods: Vec<ldap3::Mod<Vec<u8>>>,
1025 },
1026}
1027
1028#[cfg(feature = "ldap3")]
1029impl LDAPOperation {
1030 #[cfg(feature = "chumsky")]
1032 #[must_use]
1033 pub fn operation_apply_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1034 match (self, other) {
1035 (Self::Add(entry1 @ LDAPEntry { .. }), Self::Add(entry2 @ LDAPEntry { .. })) => {
1036 let parsed_dn1: Result<DistinguishedName, _> =
1037 dn_parser().parse(&entry1.dn).into_result();
1038 let parsed_dn2: Result<DistinguishedName, _> =
1039 dn_parser().parse(&entry2.dn).into_result();
1040 if let (Ok(parsed_dn1), Ok(parsed_dn2)) = (parsed_dn1, parsed_dn2) {
1041 Some(parsed_dn1.cmp(&parsed_dn2))
1042 } else {
1043 None
1044 }
1045 }
1046 (Self::Delete { dn: dn1 }, Self::Delete { dn: dn2 }) => {
1047 let parsed_dn1: Result<DistinguishedName, _> = dn_parser().parse(dn1).into_result();
1048 let parsed_dn2: Result<DistinguishedName, _> = dn_parser().parse(dn2).into_result();
1049 if let (Ok(parsed_dn1), Ok(parsed_dn2)) = (parsed_dn1, parsed_dn2) {
1050 Some(parsed_dn1.cmp(&parsed_dn2))
1051 } else {
1052 None
1053 }
1054 }
1055 _ => None,
1056 }
1057 }
1058}
1059
1060#[cfg(test)]
1061mod test {
1062 use super::*;
1063 use pretty_assertions::assert_eq;
1064
1065 #[cfg(feature = "chumsky")]
1066 #[test]
1067 fn test_parse_oid() {
1068 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1069 oid_parser().parse("1.2.3.4").into_result().unwrap();
1070 }
1071
1072 #[cfg(feature = "chumsky")]
1073 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1074 #[test]
1075 fn test_parse_oid_value() {
1076 assert_eq!(
1077 oid_parser().parse("1.2.3.4").into_result(),
1078 Ok("1.2.3.4".to_string().try_into().unwrap())
1079 );
1080 }
1081
1082 #[cfg(feature = "chumsky")]
1083 #[test]
1084 fn test_dn_parser_empty_dn() {
1085 assert_eq!(
1086 dn_parser().parse("").into_result(),
1087 Ok(DistinguishedName { rdns: vec![] })
1088 );
1089 }
1090
1091 #[cfg(feature = "chumsky")]
1092 #[test]
1093 fn test_dn_parser_single_rdn_single_string_attribute() {
1094 assert_eq!(
1095 dn_parser().parse("cn=Foobar").into_result(),
1096 Ok(DistinguishedName {
1097 rdns: vec![RelativeDistinguishedName {
1098 attributes: vec![(
1099 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1100 "Foobar".as_bytes().to_vec()
1101 )]
1102 }]
1103 })
1104 );
1105 }
1106
1107 #[cfg(feature = "chumsky")]
1108 #[test]
1109 fn test_dn_parser_single_rdn_single_string_attribute_with_escaped_comma() {
1110 assert_eq!(
1111 dn_parser().parse("cn=Foo\\,bar").into_result(),
1112 Ok(DistinguishedName {
1113 rdns: vec![RelativeDistinguishedName {
1114 attributes: vec![(
1115 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1116 "Foo,bar".as_bytes().to_vec()
1117 )]
1118 }]
1119 })
1120 );
1121 }
1122
1123 #[cfg(feature = "chumsky")]
1124 #[test]
1125 fn test_dn_parser_single_rdn_single_binary_attribute() {
1126 assert_eq!(
1127 dn_parser().parse("cn=#466f6f626172").into_result(),
1128 Ok(DistinguishedName {
1129 rdns: vec![RelativeDistinguishedName {
1130 attributes: vec![(
1131 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1132 "Foobar".as_bytes().to_vec()
1133 )]
1134 }]
1135 })
1136 );
1137 }
1138
1139 #[cfg(feature = "chumsky")]
1140 #[test]
1141 fn test_dn_parser_single_rdn_multiple_string_attributes() {
1142 assert_eq!(
1143 dn_parser().parse("cn=Foo\\,bar+uid=foobar").into_result(),
1144 Ok(DistinguishedName {
1145 rdns: vec![RelativeDistinguishedName {
1146 attributes: vec![
1147 (
1148 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1149 "Foo,bar".as_bytes().to_vec()
1150 ),
1151 (
1152 KeyStringOrOID::KeyString(KeyString("uid".to_string())),
1153 "foobar".as_bytes().to_vec()
1154 ),
1155 ]
1156 }]
1157 })
1158 );
1159 }
1160
1161 #[cfg(feature = "chumsky")]
1162 #[test]
1163 fn test_dn_parser_multiple_rdns() {
1164 assert_eq!(
1165 dn_parser().parse("cn=Foo\\,bar,uid=foobar").into_result(),
1166 Ok(DistinguishedName {
1167 rdns: vec![
1168 RelativeDistinguishedName {
1169 attributes: vec![(
1170 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1171 "Foo,bar".as_bytes().to_vec()
1172 )]
1173 },
1174 RelativeDistinguishedName {
1175 attributes: vec![(
1176 KeyStringOrOID::KeyString(KeyString("uid".to_string())),
1177 "foobar".as_bytes().to_vec()
1178 )]
1179 },
1180 ]
1181 })
1182 );
1183 }
1184
1185 #[test]
1186 fn test_dn_cmp() {
1187 assert_eq!(
1188 DistinguishedName { rdns: vec![] }.cmp(&DistinguishedName {
1189 rdns: vec![RelativeDistinguishedName {
1190 attributes: vec![(
1191 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1192 "Foo,bar".as_bytes().to_vec()
1193 )]
1194 }]
1195 }),
1196 std::cmp::Ordering::Less
1197 );
1198 }
1199
1200 #[cfg(feature = "serde")]
1201 #[test]
1202 fn test_serialize_json_oid() -> Result<(), Box<dyn std::error::Error>> {
1203 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1204 let oid: ObjectIdentifier = "1.2.3.4".to_string().try_into().unwrap();
1205 let result = serde_json::to_string(&oid)?;
1206 assert_eq!(result, "\"1.2.3.4\"".to_string());
1207 Ok(())
1208 }
1209
1210 #[cfg(feature = "serde")]
1211 #[test]
1212 fn test_deserialize_json_oid() -> Result<(), Box<dyn std::error::Error>> {
1213 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1214 let expected: ObjectIdentifier = "1.2.3.4".to_string().try_into().unwrap();
1215 let result: ObjectIdentifier = serde_json::from_str("\"1.2.3.4\"")?;
1216 assert_eq!(result, expected);
1217 Ok(())
1218 }
1219
1220 #[cfg(feature = "serde")]
1221 #[test]
1222 fn test_serialize_json_keystring() -> Result<(), Box<dyn std::error::Error>> {
1223 let ks: KeyString = KeyString("foo".to_string());
1224 let result = serde_json::to_string(&ks)?;
1225 assert_eq!(result, "\"foo\"".to_string());
1226 Ok(())
1227 }
1228
1229 #[cfg(feature = "serde")]
1230 #[test]
1231 fn test_deserialize_json_keystring() -> Result<(), Box<dyn std::error::Error>> {
1232 let expected: KeyString = KeyString("foo".to_string());
1233 let result: KeyString = serde_json::from_str("\"foo\"")?;
1234 assert_eq!(result, expected);
1235 Ok(())
1236 }
1237
1238 #[cfg(feature = "serde")]
1239 #[test]
1240 fn test_serialize_json_keystring_or_oid_keystring() -> Result<(), Box<dyn std::error::Error>> {
1241 let ks: KeyStringOrOID = KeyStringOrOID::KeyString(KeyString("foo".to_string()));
1242 let result = serde_json::to_string(&ks)?;
1243 assert_eq!(result, "{\"key_string\":\"foo\"}".to_string());
1244 Ok(())
1245 }
1246
1247 #[cfg(feature = "serde")]
1248 #[test]
1249 fn test_deserialize_json_keystring_or_oid_keystring() -> Result<(), Box<dyn std::error::Error>>
1250 {
1251 let expected: KeyStringOrOID = KeyStringOrOID::KeyString(KeyString("foo".to_string()));
1252 let result: KeyStringOrOID = serde_json::from_str("{\"key_string\":\"foo\"}")?;
1253 assert_eq!(result, expected);
1254 Ok(())
1255 }
1256
1257 #[cfg(feature = "serde")]
1258 #[test]
1259 fn test_serialize_json_keystring_or_oid_oid() -> Result<(), Box<dyn std::error::Error>> {
1260 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1261 let ks: KeyStringOrOID = KeyStringOrOID::OID("1.2.3.4".to_string().try_into().unwrap());
1262 let result = serde_json::to_string(&ks)?;
1263 assert_eq!(result, "{\"oid\":\"1.2.3.4\"}".to_string());
1264 Ok(())
1265 }
1266
1267 #[cfg(feature = "serde")]
1268 #[test]
1269 fn test_deserialize_json_keystring_or_oid_oid() -> Result<(), Box<dyn std::error::Error>> {
1270 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1271 let expected: KeyStringOrOID =
1272 KeyStringOrOID::OID("1.2.3.4".to_string().try_into().unwrap());
1273 let result: KeyStringOrOID = serde_json::from_str("{\"oid\":\"1.2.3.4\"}")?;
1274 assert_eq!(result, expected);
1275 Ok(())
1276 }
1277
1278 #[cfg(feature = "serde")]
1279 #[test]
1280 fn test_serialize_json_rdn() -> Result<(), Box<dyn std::error::Error>> {
1281 let rdn: RelativeDistinguishedName = RelativeDistinguishedName {
1282 attributes: vec![(
1283 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1284 "Foobar".as_bytes().to_vec(),
1285 )],
1286 };
1287 let result = serde_json::to_string(&rdn)?;
1288 assert_eq!(
1289 result,
1290 "{\"attributes\":[[{\"key_string\":\"cn\"},\"Foobar\"]]}".to_string()
1291 );
1292 Ok(())
1293 }
1294
1295 #[cfg(feature = "serde")]
1296 #[test]
1297 fn test_deserialize_json_rdn_string() -> Result<(), Box<dyn std::error::Error>> {
1298 let expected: RelativeDistinguishedName = RelativeDistinguishedName {
1299 attributes: vec![(
1300 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1301 "Foobar".as_bytes().to_vec(),
1302 )],
1303 };
1304 let result: RelativeDistinguishedName =
1305 serde_json::from_str("{\"attributes\":[[{\"key_string\":\"cn\"},\"Foobar\"]]}")?;
1306 assert_eq!(result, expected);
1307 Ok(())
1308 }
1309
1310 #[cfg(feature = "serde")]
1311 #[test]
1312 fn test_deserialize_json_rdn_integers() -> Result<(), Box<dyn std::error::Error>> {
1313 let expected: RelativeDistinguishedName = RelativeDistinguishedName {
1314 attributes: vec![(
1315 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1316 "Foobar".as_bytes().to_vec(),
1317 )],
1318 };
1319 let result: RelativeDistinguishedName = serde_json::from_str(
1320 "{\"attributes\":[[{\"key_string\":\"cn\"},[70, 111, 111, 98, 97, 114]]]}",
1321 )?;
1322 assert_eq!(result, expected);
1323 Ok(())
1324 }
1325
1326 #[cfg(feature = "serde")]
1327 #[test]
1328 fn test_serialize_json_dn() -> Result<(), Box<dyn std::error::Error>> {
1329 let dn: DistinguishedName = DistinguishedName {
1330 rdns: vec![RelativeDistinguishedName {
1331 attributes: vec![
1332 (
1333 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1334 "Foo,bar".as_bytes().to_vec(),
1335 ),
1336 (
1337 KeyStringOrOID::KeyString(KeyString("uid".to_string())),
1338 "foobar".as_bytes().to_vec(),
1339 ),
1340 ],
1341 }],
1342 };
1343 let result = serde_json::to_string(&dn)?;
1344 assert_eq!(
1345 result,
1346 "{\"rdns\":[{\"attributes\":[[{\"key_string\":\"cn\"},\"Foo,bar\"],[{\"key_string\":\"uid\"},\"foobar\"]]}]}".to_string()
1347 );
1348 Ok(())
1349 }
1350
1351 #[cfg(feature = "serde")]
1352 #[test]
1353 fn test_deserialize_json_dn() -> Result<(), Box<dyn std::error::Error>> {
1354 let expected: DistinguishedName = DistinguishedName {
1355 rdns: vec![RelativeDistinguishedName {
1356 attributes: vec![
1357 (
1358 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1359 "Foo,bar".as_bytes().to_vec(),
1360 ),
1361 (
1362 KeyStringOrOID::KeyString(KeyString("uid".to_string())),
1363 "foobar".as_bytes().to_vec(),
1364 ),
1365 ],
1366 }],
1367 };
1368 let result : DistinguishedName = serde_json::from_str("{\"rdns\":[{\"attributes\":[[{\"key_string\":\"cn\"},\"Foo,bar\"],[{\"key_string\":\"uid\"},\"foobar\"]]}]}")?;
1369 assert_eq!(result, expected);
1370 Ok(())
1371 }
1372}