ldap_types/
basic.rs

1//! Contains al the basic LDAP types
2use 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/// a wrapped error in case parsing fails to get proper error output
28/// the chumsky errors themselves lack Display and std::error::Error
29/// implementations
30#[cfg(feature = "chumsky")]
31#[derive(Debug)]
32pub struct ChumskyError<E> {
33    /// description of the object we were trying to parse
34    pub description: String,
35    /// source string for parsing
36    pub source: String,
37    /// errors encountered during parsing
38    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/// represents the object to request from an LDAP server to figure out which
116/// features,... it supports
117///
118/// <https://ldapwiki.com/wiki/RootDSE>
119///
120/// <https://ldapwiki.com/wiki/LDAP%20Extensions%20and%20Controls%20Listing>
121#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
122pub struct RootDSE {
123    /// version of the LDAP protocol the server supports
124    pub supported_ldap_version: String,
125    /// LDAP controls the server supports
126    ///
127    /// <https://ldapwiki.com/wiki/SupportedControl>
128    pub supported_controls: Vec<ObjectIdentifier>,
129    /// LDAP extensions the server supports
130    ///
131    /// <https://ldapwiki.com/wiki/SupportedExtension>
132    pub supported_extensions: Vec<ObjectIdentifier>,
133    /// LDAP features the server supports
134    ///
135    /// <https://ldapwiki.com/wiki/SupportedFeatures>
136    pub supported_features: Vec<ObjectIdentifier>,
137    /// SASL mechanisms the server supports for authentication
138    ///
139    /// <https://ldapwiki.com/wiki/SupportedSASLMechanisms>
140    pub supported_sasl_mechanisms: Vec<String>,
141    /// the DN of the config context on this server
142    ///
143    /// this is where the LDAP server configuration lives
144    pub config_context: String,
145    /// the DNs of naming contexts on this server
146    ///
147    /// each of these is essentially the root of a tree where the actual data
148    /// on the server lives
149    ///
150    /// <https://ldapwiki.com/wiki/NamingContext>
151    pub naming_contexts: Vec<String>,
152    /// the DN of the subschema subentry
153    ///
154    /// this is essentially where the LDAP schema elements this server supports
155    /// can be retrieved
156    ///
157    /// <https://ldapwiki.com/wiki/SubschemaSubentry>
158    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/// chumsky parser for [oid::ObjectIdentifier]
198#[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/// a key string is a string limited to the characters that are safe to use
215/// in a key context, e.g. a relative distinguished name, without encoding
216#[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    /// this is a quick and dirty helper method to determine if this KeyString
229    /// describes one of the standard case insensitive matches
230    ///
231    /// not perfect but it is useful when trying to figure out how string LDAP
232    /// attributes need to be compared
233    #[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    /// converts the KeyString to lowercase
249    #[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/// parses a [KeyString]
279#[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/// parses a [KeyString] in locations where it is single-quoted
295#[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
302/// hash function for ObjectIdentifier based on string representation
303/// since ObjectIdentifier does not implement Hash
304pub fn hash_oid<H: Hasher>(s: &ObjectIdentifier, state: &mut H) {
305    Hash::hash(&format!("{s:?}"), state);
306}
307
308/// LDAP allows the use of either a keystring or an OID in many locations,
309/// e.g. in DNs or in the schema
310#[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    /// this represents a [KeyString]
315    #[cfg_attr(feature = "serde", serde(rename = "key_string"))]
316    KeyString(KeyString),
317    /// this represents an [ObjectIdentifier]
318    #[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/// parses either a [KeyString] or an [ObjectIdentifier]
473#[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/// in some locations LDAP allows OIDs with an optional length specifier
482/// to describe attribute types with a length limit
483#[derive(Clone, Educe)]
484#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
485#[educe(PartialEq, Eq, Hash)]
486pub struct OIDWithLength {
487    /// the [ObjectIdentifier]
488    #[educe(Hash(method = "hash_oid"))]
489    pub oid: ObjectIdentifier,
490    /// the optional maximum length of the value
491    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/// a relative distinguished name is one of the components of a distinguished name
517/// usually a single pair of a keystring or an OID along with its attribute value
518/// but it can also be a plus sign separated string of several such pairs
519///
520/// <https://ldapwiki.com/wiki/Relative%20Distinguished%20Name>
521#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
522#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
523pub struct RelativeDistinguishedName {
524    /// the attributes of the RDN
525    #[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/// serialize RDN attribute values as string if possible
608/// falling back to array of numbers of necessary
609///
610/// # Errors
611///
612/// fails if serialize_seq or serialize_element calls fail
613#[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/// parses an RDN with attribute values being represented either as a string or an array of integers
630///
631/// # Errors
632///
633/// fails if deserializing the elements fails
634#[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    /// untagged union to allow deserializing attribute values as either string or bytes
640    #[derive(Deserialize)]
641    #[serde(untagged)]
642    enum StringOrBytes {
643        /// string attribute value
644        String(String),
645        /// bytes attribute value
646        Bytes(Vec<u8>),
647    }
648
649    /// visitor to deserialize RDNs in deserialize_rdn
650    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/// parses a series of hex-encoded bytes (always even number of hex digits)
685#[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/// parses a hex-encoded binary attribute value in an RDN
707#[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/// parses a plain string attribute value in an RDN
715#[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/// parses either a binary or a plain attribute value in an RDN
728#[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/// parses a [RelativeDistinguishedName]
736#[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/// a distinguished name is a unique identifier for an entry within the LDAP tree,
749/// it is comprised of a comma-separated ordered list of [RelativeDistinguishedName]
750/// components
751///
752/// <https://ldapwiki.com/wiki/Distinguished%20Names>
753#[derive(Debug, PartialEq, Eq, Clone, Hash)]
754#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
755pub struct DistinguishedName {
756    /// the RDN components of the DN
757    pub rdns: Vec<RelativeDistinguishedName>,
758}
759
760impl DistinguishedName {
761    /// returns true if this is the empty DN
762    #[must_use]
763    pub const fn is_empty(&self) -> bool {
764        self.rdns.is_empty()
765    }
766    /// returns the DN for the parent object in the LDAP hierarchy unless this is
767    /// already the empty DN
768    #[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    /// checks if the current DN is an ancestor (parent, parent of parent,...)
780    /// of the given other DN
781    ///
782    /// it does return false if both DNs are identical
783    #[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                // both DNs are identical or
792                // self is longer, can not be an ancestor
793                (None | Some(_), None) => {
794                    return false;
795                }
796                (None, Some(_)) => {
797                    // so far we have not gotten a false and self is longer,
798                    // so other must be an ancestor
799                    return true;
800                }
801                (Some(e), Some(other_e)) => {
802                    if e != other_e {
803                        // different RDNs in the same position mean self
804                        // can not be an ancestor of other (or vice versa)
805                        return false;
806                    }
807                    // identical RDNs in this position mean we can advance
808                    // the loop
809                }
810            }
811        }
812    }
813
814    /// add suffix DN to this DN (e.g. the base DN)
815    #[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    /// remove a suffix DN from this DN (e.g. the base DN)
823    #[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/// parses a [DistinguishedName]
929#[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/// represents an object in the LDAP tree
940/// we would use ldap3::SearchEntry but then we would not be able to derive Diff
941/// easily
942#[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    /// the DN of the entry
948    pub dn: String,
949    /// the textual attributes of the entry
950    pub attrs: HashMap<String, Vec<String>>,
951    /// the binary attributes of the entry
952    pub bin_attrs: HashMap<String, Vec<Vec<u8>>>,
953}
954
955impl LDAPEntry {
956    /// return the combined attributes from attrs and bin_attrs for use in e.g. the [ldap3::Ldap::add] method
957    #[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/// an operation to perform to turn one LDAP object into another.
1005/// we purposefully only include operations here that operate without
1006/// moving the object to a different DN
1007#[derive(Debug, Clone)]
1008#[cfg(feature = "ldap3")]
1009pub enum LDAPOperation {
1010    /// add a new entry
1011    Add(LDAPEntry),
1012    /// delete an existing entry
1013    Delete {
1014        /// the DN of the entry to delete
1015        dn: String,
1016    },
1017    /// modify attributes of an existing entry
1018    Modify {
1019        /// the DN of the entry to modify
1020        dn: String,
1021        /// the modifications to textual attributes to perform
1022        mods: Vec<ldap3::Mod<String>>,
1023        /// the modifications to binary attributes to perform
1024        bin_mods: Vec<ldap3::Mod<Vec<u8>>>,
1025    },
1026}
1027
1028#[cfg(feature = "ldap3")]
1029impl LDAPOperation {
1030    /// Used to order operations so parents are added first and children deleted first
1031    #[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}