Skip to main content

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    Deserialize, Deserializer, Serialize, Serializer, de::SeqAccess, ser::SerializeSeq as _,
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!(
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/// parses a series of hex-encoded bytes (always even number of hex digits)
688#[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/// parses a hex-encoded binary attribute value in an RDN
710#[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/// parses a plain string attribute value in an RDN
718#[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/// parses either a binary or a plain attribute value in an RDN
731#[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/// parses a [RelativeDistinguishedName]
739#[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/// a distinguished name is a unique identifier for an entry within the LDAP tree,
752/// it is comprised of a comma-separated ordered list of [RelativeDistinguishedName]
753/// components
754///
755/// <https://ldapwiki.com/wiki/Distinguished%20Names>
756#[derive(Debug, PartialEq, Eq, Clone, Hash)]
757#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
758pub struct DistinguishedName {
759    /// the RDN components of the DN
760    pub rdns: Vec<RelativeDistinguishedName>,
761}
762
763impl DistinguishedName {
764    /// returns true if this is the empty DN
765    #[must_use]
766    pub const fn is_empty(&self) -> bool {
767        self.rdns.is_empty()
768    }
769    /// returns the DN for the parent object in the LDAP hierarchy unless this is
770    /// already the empty DN
771    #[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    /// checks if the current DN is an ancestor (parent, parent of parent,...)
783    /// of the given other DN
784    ///
785    /// it does return false if both DNs are identical
786    #[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                // both DNs are identical or
795                // self is longer, can not be an ancestor
796                (None | Some(_), None) => {
797                    return false;
798                }
799                (None, Some(_)) => {
800                    // so far we have not gotten a false and self is longer,
801                    // so other must be an ancestor
802                    return true;
803                }
804                (Some(e), Some(other_e)) => {
805                    if e != other_e {
806                        // different RDNs in the same position mean self
807                        // can not be an ancestor of other (or vice versa)
808                        return false;
809                    }
810                    // identical RDNs in this position mean we can advance
811                    // the loop
812                }
813            }
814        }
815    }
816
817    /// add suffix DN to this DN (e.g. the base DN)
818    #[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    /// remove a suffix DN from this DN (e.g. the base DN)
826    #[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/// parses a [DistinguishedName]
932#[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/// represents an object in the LDAP tree
943/// we would use ldap3::SearchEntry but then we would not be able to derive Diff
944/// easily
945#[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    /// the DN of the entry
951    pub dn: String,
952    /// the textual attributes of the entry
953    pub attrs: HashMap<String, Vec<String>>,
954    /// the binary attributes of the entry
955    pub bin_attrs: HashMap<String, Vec<Vec<u8>>>,
956}
957
958impl LDAPEntry {
959    /// return the combined attributes from attrs and bin_attrs for use in e.g. the [ldap3::Ldap::add] method
960    #[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/// an operation to perform to turn one LDAP object into another.
1008/// we purposefully only include operations here that operate without
1009/// moving the object to a different DN
1010#[derive(Debug, Clone)]
1011#[cfg(feature = "ldap3")]
1012pub enum LDAPOperation {
1013    /// add a new entry
1014    Add(LDAPEntry),
1015    /// delete an existing entry
1016    Delete {
1017        /// the DN of the entry to delete
1018        dn: String,
1019    },
1020    /// modify attributes of an existing entry
1021    Modify {
1022        /// the DN of the entry to modify
1023        dn: String,
1024        /// the modifications to textual attributes to perform
1025        mods: Vec<ldap3::Mod<String>>,
1026        /// the modifications to binary attributes to perform
1027        bin_mods: Vec<ldap3::Mod<Vec<u8>>>,
1028    },
1029}
1030
1031#[cfg(feature = "ldap3")]
1032impl LDAPOperation {
1033    /// Used to order operations so parents are added first and children deleted first
1034    #[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}