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 Deserialize, Deserializer, Serialize, Serializer, de::SeqAccess, ser::SerializeSeq as _,
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!(
657 formatter,
658 "an array of tuples of attribute name and attribute value (either a string or a sequence of integers)"
659 )
660 }
661
662 fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
663 where
664 A: SeqAccess<'de>,
665 {
666 let mut result = Vec::new();
667 while let Some(e) = seq.next_element()? {
668 result.push(e);
669 }
670 Ok(result)
671 }
672 }
673
674 let parse_result = d.deserialize_seq(RDNVisitor)?;
675 let mut results = Vec::new();
676 for (ref k, ref v) in parse_result {
677 match v {
678 StringOrBytes::String(s) => {
679 results.push((k.to_owned(), s.as_bytes().to_vec()));
680 }
681 StringOrBytes::Bytes(b) => results.push((k.to_owned(), b.to_vec())),
682 }
683 }
684 Ok(results)
685}
686
687#[cfg(feature = "chumsky")]
689#[expect(
690 clippy::missing_panics_doc,
691 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"
692)]
693#[must_use]
694pub fn hex_byte_parser<'src>() -> impl Parser<'src, &'src str, u8, extra::Err<Rich<'src, char>>> {
695 any()
696 .filter(|c: &char| c.is_ascii_hexdigit())
697 .repeated()
698 .exactly(2)
699 .collect::<String>()
700 .try_map(|ds, span| {
701 hex::decode(ds.as_bytes()).map_err(|e| Rich::custom(span, format!("{e:?}")))
702 })
703 .map(|v: Vec<u8>| {
704 #[expect(clippy::unwrap_used, reason = "since we collect exactly two hex digits the result of hex::decode should be exactly 1 u8 long")]
705 v.first().unwrap().to_owned()
706 })
707}
708
709#[cfg(feature = "chumsky")]
711#[must_use]
712pub fn rdn_attribute_binary_value_parser<'src>()
713-> impl Parser<'src, &'src str, Vec<u8>, extra::Err<Rich<'src, char>>> {
714 just('#').ignore_then(hex_byte_parser().repeated().collect())
715}
716
717#[cfg(feature = "chumsky")]
719#[must_use]
720pub fn rdn_attribute_string_value_parser<'src>()
721-> impl Parser<'src, &'src str, Vec<u8>, extra::Err<Rich<'src, char>>> {
722 none_of(",+\"\\<>;")
723 .or(just('\\').ignore_then(one_of(" ,+\"\\<>;")))
724 .or(just('\\').ignore_then(hex_byte_parser().map(char::from)))
725 .repeated()
726 .collect::<String>()
727 .map(|s| s.as_bytes().to_vec())
728}
729
730#[cfg(feature = "chumsky")]
732#[must_use]
733pub fn rdn_attribute_value_parser<'src>()
734-> impl Parser<'src, &'src str, Vec<u8>, extra::Err<Rich<'src, char>>> {
735 rdn_attribute_binary_value_parser().or(rdn_attribute_string_value_parser())
736}
737
738#[cfg(feature = "chumsky")]
740#[must_use]
741pub fn rdn_parser<'src>()
742-> impl Parser<'src, &'src str, RelativeDistinguishedName, extra::Err<Rich<'src, char>>> {
743 keystring_or_oid_parser()
744 .then(just('=').ignore_then(rdn_attribute_value_parser()))
745 .separated_by(just('+'))
746 .at_least(1)
747 .collect()
748 .map(|attributes| RelativeDistinguishedName { attributes })
749}
750
751#[derive(Debug, PartialEq, Eq, Clone, Hash)]
757#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
758pub struct DistinguishedName {
759 pub rdns: Vec<RelativeDistinguishedName>,
761}
762
763impl DistinguishedName {
764 #[must_use]
766 pub const fn is_empty(&self) -> bool {
767 self.rdns.is_empty()
768 }
769 #[must_use]
772 pub fn parent(&self) -> Option<Self> {
773 if self.is_empty() {
774 None
775 } else {
776 Some(Self {
777 rdns: self.rdns.iter().skip(1).cloned().collect(),
778 })
779 }
780 }
781
782 #[must_use]
787 pub fn is_ancestor_of(&self, other: &Self) -> bool {
788 let mut it = self.rdns.iter().rev();
789 let mut other_it = other.rdns.iter().rev();
790 loop {
791 let e = it.next();
792 let other_e = other_it.next();
793 match (e, other_e) {
794 (None | Some(_), None) => {
797 return false;
798 }
799 (None, Some(_)) => {
800 return true;
803 }
804 (Some(e), Some(other_e)) => {
805 if e != other_e {
806 return false;
809 }
810 }
813 }
814 }
815 }
816
817 #[must_use]
819 pub fn add_suffix(&self, other: &Self) -> Self {
820 Self {
821 rdns: [self.rdns.to_vec(), other.rdns.to_vec()].concat(),
822 }
823 }
824
825 #[must_use]
827 pub fn strip_suffix(&self, other: &Self) -> Option<Self> {
828 if !other.is_ancestor_of(self) {
829 None
830 } else {
831 let self_len = self.rdns.len();
832 let other_len = other.rdns.len();
833 Some(Self {
834 rdns: self
835 .rdns
836 .split_at(self_len.saturating_sub(other_len))
837 .0
838 .to_vec(),
839 })
840 }
841 }
842}
843
844#[cfg(feature = "chumsky")]
845impl TryFrom<&str> for DistinguishedName {
846 type Error = ChumskyError<chumsky::error::Rich<'static, char>>;
847
848 fn try_from(value: &str) -> Result<Self, Self::Error> {
849 (dn_parser().then_ignore(chumsky::primitive::end()))
850 .parse(value)
851 .into_result()
852 .map_err(|e| ChumskyError {
853 description: "distinguished name".to_string(),
854 source: value.to_string(),
855 errors: e.into_iter().map(|e| e.into_owned()).collect(),
856 })
857 }
858}
859
860#[cfg(feature = "chumsky")]
861impl TryFrom<String> for DistinguishedName {
862 type Error = ChumskyError<chumsky::error::Rich<'static, char>>;
863
864 fn try_from(value: String) -> Result<Self, Self::Error> {
865 (dn_parser().then_ignore(chumsky::primitive::end()))
866 .parse(&value)
867 .into_result()
868 .map_err(|e| ChumskyError {
869 description: "distinguished name".to_string(),
870 source: value.to_string(),
871 errors: e.into_iter().map(|e| e.into_owned()).collect(),
872 })
873 }
874}
875
876#[cfg(feature = "chumsky")]
877impl std::str::FromStr for DistinguishedName {
878 type Err = ChumskyError<chumsky::error::Rich<'static, char>>;
879
880 fn from_str(s: &str) -> Result<Self, Self::Err> {
881 (dn_parser().then_ignore(chumsky::primitive::end()))
882 .parse(s)
883 .into_result()
884 .map_err(|e| ChumskyError {
885 description: "distinguished name".to_string(),
886 source: s.to_string(),
887 errors: e.into_iter().map(|e| e.into_owned()).collect(),
888 })
889 }
890}
891
892impl From<DistinguishedName> for String {
893 fn from(dn: DistinguishedName) -> Self {
894 dn.to_string()
895 }
896}
897
898impl std::fmt::Display for DistinguishedName {
899 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
900 let mut first = true;
901 for rdn in &self.rdns {
902 if !first {
903 write!(f, ",")?;
904 } else {
905 first = false;
906 }
907 write!(f, "{rdn}")?;
908 }
909 Ok(())
910 }
911}
912
913impl PartialOrd for DistinguishedName {
914 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
915 Some(self.cmp(other))
916 }
917}
918
919impl Ord for DistinguishedName {
920 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
921 self.rdns
922 .iter()
923 .rev()
924 .zip(other.rdns.iter().rev())
925 .map(|(a, b)| a.cmp(b))
926 .fold(std::cmp::Ordering::Equal, |acc, e| acc.then(e))
927 .then(self.rdns.len().cmp(&other.rdns.len()))
928 }
929}
930
931#[cfg(feature = "chumsky")]
933#[must_use]
934pub fn dn_parser<'src>()
935-> impl Parser<'src, &'src str, DistinguishedName, extra::Err<Rich<'src, char>>> {
936 rdn_parser()
937 .separated_by(just(','))
938 .collect()
939 .map(|rdns| DistinguishedName { rdns })
940}
941
942#[derive(Debug, Clone, PartialEq, Eq)]
946#[cfg_attr(feature = "diff", derive(Diff))]
947#[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")])))]
948#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
949pub struct LDAPEntry {
950 pub dn: String,
952 pub attrs: HashMap<String, Vec<String>>,
954 pub bin_attrs: HashMap<String, Vec<Vec<u8>>>,
956}
957
958impl LDAPEntry {
959 #[must_use]
961 pub fn combined_attrs(&self) -> Vec<(Vec<u8>, HashSet<Vec<u8>>)> {
962 let mut result: HashMap<Vec<u8>, HashSet<Vec<u8>>> = HashMap::new();
963 for (attr_name, attr_values) in &self.attrs {
964 let attr_name = attr_name.as_bytes().to_vec();
965 let attr_values = attr_values.iter().map(|x| x.as_bytes().to_vec()).collect();
966 if let Some(values) = result.get_mut(&attr_name) {
967 values.extend(attr_values);
968 } else {
969 result.insert(attr_name, attr_values);
970 }
971 }
972 for (attr_name, attr_values) in &self.bin_attrs {
973 let attr_name = attr_name.as_bytes().to_vec();
974 let attr_values = attr_values.iter().map(|x| x.to_vec()).collect();
975 if let Some(values) = result.get_mut(&attr_name) {
976 values.extend(attr_values);
977 } else {
978 result.insert(attr_name, attr_values);
979 }
980 }
981 result.into_iter().collect()
982 }
983}
984
985#[cfg(feature = "ldap3")]
986impl From<ldap3::SearchEntry> for LDAPEntry {
987 fn from(entry: ldap3::SearchEntry) -> Self {
988 Self {
989 dn: entry.dn,
990 attrs: entry.attrs,
991 bin_attrs: entry.bin_attrs,
992 }
993 }
994}
995
996#[cfg(feature = "ldap3")]
997impl From<LDAPEntry> for ldap3::SearchEntry {
998 fn from(entry: LDAPEntry) -> Self {
999 Self {
1000 dn: entry.dn,
1001 attrs: entry.attrs,
1002 bin_attrs: entry.bin_attrs,
1003 }
1004 }
1005}
1006
1007#[derive(Debug, Clone)]
1011#[cfg(feature = "ldap3")]
1012pub enum LDAPOperation {
1013 Add(LDAPEntry),
1015 Delete {
1017 dn: String,
1019 },
1020 Modify {
1022 dn: String,
1024 mods: Vec<ldap3::Mod<String>>,
1026 bin_mods: Vec<ldap3::Mod<Vec<u8>>>,
1028 },
1029}
1030
1031#[cfg(feature = "ldap3")]
1032impl LDAPOperation {
1033 #[cfg(feature = "chumsky")]
1035 #[must_use]
1036 pub fn operation_apply_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1037 match (self, other) {
1038 (Self::Add(entry1 @ LDAPEntry { .. }), Self::Add(entry2 @ LDAPEntry { .. })) => {
1039 let parsed_dn1: Result<DistinguishedName, _> =
1040 dn_parser().parse(&entry1.dn).into_result();
1041 let parsed_dn2: Result<DistinguishedName, _> =
1042 dn_parser().parse(&entry2.dn).into_result();
1043 if let (Ok(parsed_dn1), Ok(parsed_dn2)) = (parsed_dn1, parsed_dn2) {
1044 Some(parsed_dn1.cmp(&parsed_dn2))
1045 } else {
1046 None
1047 }
1048 }
1049 (Self::Delete { dn: dn1 }, Self::Delete { dn: dn2 }) => {
1050 let parsed_dn1: Result<DistinguishedName, _> = dn_parser().parse(dn1).into_result();
1051 let parsed_dn2: Result<DistinguishedName, _> = dn_parser().parse(dn2).into_result();
1052 if let (Ok(parsed_dn1), Ok(parsed_dn2)) = (parsed_dn1, parsed_dn2) {
1053 Some(parsed_dn1.cmp(&parsed_dn2))
1054 } else {
1055 None
1056 }
1057 }
1058 _ => None,
1059 }
1060 }
1061}
1062
1063#[cfg(test)]
1064mod test {
1065 use super::*;
1066 use pretty_assertions::assert_eq;
1067
1068 #[cfg(feature = "chumsky")]
1069 #[test]
1070 fn test_parse_oid() {
1071 #[expect(clippy::unwrap_used, reason = "intentional for assertion")]
1072 oid_parser().parse("1.2.3.4").into_result().unwrap();
1073 }
1074
1075 #[cfg(feature = "chumsky")]
1076 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1077 #[test]
1078 fn test_parse_oid_value() {
1079 assert_eq!(
1080 oid_parser().parse("1.2.3.4").into_result(),
1081 Ok("1.2.3.4".to_string().try_into().unwrap())
1082 );
1083 }
1084
1085 #[cfg(feature = "chumsky")]
1086 #[test]
1087 fn test_dn_parser_empty_dn() {
1088 assert_eq!(
1089 dn_parser().parse("").into_result(),
1090 Ok(DistinguishedName { rdns: vec![] })
1091 );
1092 }
1093
1094 #[cfg(feature = "chumsky")]
1095 #[test]
1096 fn test_dn_parser_single_rdn_single_string_attribute() {
1097 assert_eq!(
1098 dn_parser().parse("cn=Foobar").into_result(),
1099 Ok(DistinguishedName {
1100 rdns: vec![RelativeDistinguishedName {
1101 attributes: vec![(
1102 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1103 "Foobar".as_bytes().to_vec()
1104 )]
1105 }]
1106 })
1107 );
1108 }
1109
1110 #[cfg(feature = "chumsky")]
1111 #[test]
1112 fn test_dn_parser_single_rdn_single_string_attribute_with_escaped_comma() {
1113 assert_eq!(
1114 dn_parser().parse("cn=Foo\\,bar").into_result(),
1115 Ok(DistinguishedName {
1116 rdns: vec![RelativeDistinguishedName {
1117 attributes: vec![(
1118 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1119 "Foo,bar".as_bytes().to_vec()
1120 )]
1121 }]
1122 })
1123 );
1124 }
1125
1126 #[cfg(feature = "chumsky")]
1127 #[test]
1128 fn test_dn_parser_single_rdn_single_binary_attribute() {
1129 assert_eq!(
1130 dn_parser().parse("cn=#466f6f626172").into_result(),
1131 Ok(DistinguishedName {
1132 rdns: vec![RelativeDistinguishedName {
1133 attributes: vec![(
1134 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1135 "Foobar".as_bytes().to_vec()
1136 )]
1137 }]
1138 })
1139 );
1140 }
1141
1142 #[cfg(feature = "chumsky")]
1143 #[test]
1144 fn test_dn_parser_single_rdn_multiple_string_attributes() {
1145 assert_eq!(
1146 dn_parser().parse("cn=Foo\\,bar+uid=foobar").into_result(),
1147 Ok(DistinguishedName {
1148 rdns: vec![RelativeDistinguishedName {
1149 attributes: vec![
1150 (
1151 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1152 "Foo,bar".as_bytes().to_vec()
1153 ),
1154 (
1155 KeyStringOrOID::KeyString(KeyString("uid".to_string())),
1156 "foobar".as_bytes().to_vec()
1157 ),
1158 ]
1159 }]
1160 })
1161 );
1162 }
1163
1164 #[cfg(feature = "chumsky")]
1165 #[test]
1166 fn test_dn_parser_multiple_rdns() {
1167 assert_eq!(
1168 dn_parser().parse("cn=Foo\\,bar,uid=foobar").into_result(),
1169 Ok(DistinguishedName {
1170 rdns: vec![
1171 RelativeDistinguishedName {
1172 attributes: vec![(
1173 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1174 "Foo,bar".as_bytes().to_vec()
1175 )]
1176 },
1177 RelativeDistinguishedName {
1178 attributes: vec![(
1179 KeyStringOrOID::KeyString(KeyString("uid".to_string())),
1180 "foobar".as_bytes().to_vec()
1181 )]
1182 },
1183 ]
1184 })
1185 );
1186 }
1187
1188 #[test]
1189 fn test_dn_cmp() {
1190 assert_eq!(
1191 DistinguishedName { rdns: vec![] }.cmp(&DistinguishedName {
1192 rdns: vec![RelativeDistinguishedName {
1193 attributes: vec![(
1194 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1195 "Foo,bar".as_bytes().to_vec()
1196 )]
1197 }]
1198 }),
1199 std::cmp::Ordering::Less
1200 );
1201 }
1202
1203 #[cfg(feature = "serde")]
1204 #[test]
1205 fn test_serialize_json_oid() -> Result<(), Box<dyn std::error::Error>> {
1206 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1207 let oid: ObjectIdentifier = "1.2.3.4".to_string().try_into().unwrap();
1208 let result = serde_json::to_string(&oid)?;
1209 assert_eq!(result, "\"1.2.3.4\"".to_string());
1210 Ok(())
1211 }
1212
1213 #[cfg(feature = "serde")]
1214 #[test]
1215 fn test_deserialize_json_oid() -> Result<(), Box<dyn std::error::Error>> {
1216 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1217 let expected: ObjectIdentifier = "1.2.3.4".to_string().try_into().unwrap();
1218 let result: ObjectIdentifier = serde_json::from_str("\"1.2.3.4\"")?;
1219 assert_eq!(result, expected);
1220 Ok(())
1221 }
1222
1223 #[cfg(feature = "serde")]
1224 #[test]
1225 fn test_serialize_json_keystring() -> Result<(), Box<dyn std::error::Error>> {
1226 let ks: KeyString = KeyString("foo".to_string());
1227 let result = serde_json::to_string(&ks)?;
1228 assert_eq!(result, "\"foo\"".to_string());
1229 Ok(())
1230 }
1231
1232 #[cfg(feature = "serde")]
1233 #[test]
1234 fn test_deserialize_json_keystring() -> Result<(), Box<dyn std::error::Error>> {
1235 let expected: KeyString = KeyString("foo".to_string());
1236 let result: KeyString = serde_json::from_str("\"foo\"")?;
1237 assert_eq!(result, expected);
1238 Ok(())
1239 }
1240
1241 #[cfg(feature = "serde")]
1242 #[test]
1243 fn test_serialize_json_keystring_or_oid_keystring() -> Result<(), Box<dyn std::error::Error>> {
1244 let ks: KeyStringOrOID = KeyStringOrOID::KeyString(KeyString("foo".to_string()));
1245 let result = serde_json::to_string(&ks)?;
1246 assert_eq!(result, "{\"key_string\":\"foo\"}".to_string());
1247 Ok(())
1248 }
1249
1250 #[cfg(feature = "serde")]
1251 #[test]
1252 fn test_deserialize_json_keystring_or_oid_keystring() -> Result<(), Box<dyn std::error::Error>>
1253 {
1254 let expected: KeyStringOrOID = KeyStringOrOID::KeyString(KeyString("foo".to_string()));
1255 let result: KeyStringOrOID = serde_json::from_str("{\"key_string\":\"foo\"}")?;
1256 assert_eq!(result, expected);
1257 Ok(())
1258 }
1259
1260 #[cfg(feature = "serde")]
1261 #[test]
1262 fn test_serialize_json_keystring_or_oid_oid() -> Result<(), Box<dyn std::error::Error>> {
1263 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1264 let ks: KeyStringOrOID = KeyStringOrOID::OID("1.2.3.4".to_string().try_into().unwrap());
1265 let result = serde_json::to_string(&ks)?;
1266 assert_eq!(result, "{\"oid\":\"1.2.3.4\"}".to_string());
1267 Ok(())
1268 }
1269
1270 #[cfg(feature = "serde")]
1271 #[test]
1272 fn test_deserialize_json_keystring_or_oid_oid() -> Result<(), Box<dyn std::error::Error>> {
1273 #[expect(clippy::unwrap_used, reason = "just a literal parse in a test")]
1274 let expected: KeyStringOrOID =
1275 KeyStringOrOID::OID("1.2.3.4".to_string().try_into().unwrap());
1276 let result: KeyStringOrOID = serde_json::from_str("{\"oid\":\"1.2.3.4\"}")?;
1277 assert_eq!(result, expected);
1278 Ok(())
1279 }
1280
1281 #[cfg(feature = "serde")]
1282 #[test]
1283 fn test_serialize_json_rdn() -> Result<(), Box<dyn std::error::Error>> {
1284 let rdn: RelativeDistinguishedName = RelativeDistinguishedName {
1285 attributes: vec![(
1286 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1287 "Foobar".as_bytes().to_vec(),
1288 )],
1289 };
1290 let result = serde_json::to_string(&rdn)?;
1291 assert_eq!(
1292 result,
1293 "{\"attributes\":[[{\"key_string\":\"cn\"},\"Foobar\"]]}".to_string()
1294 );
1295 Ok(())
1296 }
1297
1298 #[cfg(feature = "serde")]
1299 #[test]
1300 fn test_deserialize_json_rdn_string() -> Result<(), Box<dyn std::error::Error>> {
1301 let expected: RelativeDistinguishedName = RelativeDistinguishedName {
1302 attributes: vec![(
1303 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1304 "Foobar".as_bytes().to_vec(),
1305 )],
1306 };
1307 let result: RelativeDistinguishedName =
1308 serde_json::from_str("{\"attributes\":[[{\"key_string\":\"cn\"},\"Foobar\"]]}")?;
1309 assert_eq!(result, expected);
1310 Ok(())
1311 }
1312
1313 #[cfg(feature = "serde")]
1314 #[test]
1315 fn test_deserialize_json_rdn_integers() -> Result<(), Box<dyn std::error::Error>> {
1316 let expected: RelativeDistinguishedName = RelativeDistinguishedName {
1317 attributes: vec![(
1318 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1319 "Foobar".as_bytes().to_vec(),
1320 )],
1321 };
1322 let result: RelativeDistinguishedName = serde_json::from_str(
1323 "{\"attributes\":[[{\"key_string\":\"cn\"},[70, 111, 111, 98, 97, 114]]]}",
1324 )?;
1325 assert_eq!(result, expected);
1326 Ok(())
1327 }
1328
1329 #[cfg(feature = "serde")]
1330 #[test]
1331 fn test_serialize_json_dn() -> Result<(), Box<dyn std::error::Error>> {
1332 let dn: DistinguishedName = DistinguishedName {
1333 rdns: vec![RelativeDistinguishedName {
1334 attributes: vec![
1335 (
1336 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1337 "Foo,bar".as_bytes().to_vec(),
1338 ),
1339 (
1340 KeyStringOrOID::KeyString(KeyString("uid".to_string())),
1341 "foobar".as_bytes().to_vec(),
1342 ),
1343 ],
1344 }],
1345 };
1346 let result = serde_json::to_string(&dn)?;
1347 assert_eq!(
1348 result,
1349 "{\"rdns\":[{\"attributes\":[[{\"key_string\":\"cn\"},\"Foo,bar\"],[{\"key_string\":\"uid\"},\"foobar\"]]}]}".to_string()
1350 );
1351 Ok(())
1352 }
1353
1354 #[cfg(feature = "serde")]
1355 #[test]
1356 fn test_deserialize_json_dn() -> Result<(), Box<dyn std::error::Error>> {
1357 let expected: DistinguishedName = DistinguishedName {
1358 rdns: vec![RelativeDistinguishedName {
1359 attributes: vec![
1360 (
1361 KeyStringOrOID::KeyString(KeyString("cn".to_string())),
1362 "Foo,bar".as_bytes().to_vec(),
1363 ),
1364 (
1365 KeyStringOrOID::KeyString(KeyString("uid".to_string())),
1366 "foobar".as_bytes().to_vec(),
1367 ),
1368 ],
1369 }],
1370 };
1371 let result: DistinguishedName = serde_json::from_str(
1372 "{\"rdns\":[{\"attributes\":[[{\"key_string\":\"cn\"},\"Foo,bar\"],[{\"key_string\":\"uid\"},\"foobar\"]]}]}",
1373 )?;
1374 assert_eq!(result, expected);
1375 Ok(())
1376 }
1377}