1use crate::ber::{self, Decoder, Encoder};
26use rustlavel_core::{Error, Result};
27
28pub const VERSION: i64 = 3;
31
32pub const START_TLS_OID: &str = "1.3.6.1.4.1.1466.20037";
34
35pub mod tags {
37 use crate::ber::application;
38
39 pub const BIND_REQUEST: u8 = application(0, true);
40 pub const BIND_RESPONSE: u8 = application(1, true);
41 pub const UNBIND_REQUEST: u8 = application(2, false);
43 pub const SEARCH_REQUEST: u8 = application(3, true);
44 pub const SEARCH_RESULT_ENTRY: u8 = application(4, true);
45 pub const SEARCH_RESULT_DONE: u8 = application(5, true);
46 pub const SEARCH_RESULT_REFERENCE: u8 = application(19, true);
47 pub const EXTENDED_REQUEST: u8 = application(23, true);
48 pub const EXTENDED_RESPONSE: u8 = application(24, true);
49 pub const INTERMEDIATE_RESPONSE: u8 = application(25, true);
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum ResultCode {
64 Success,
65 OperationsError,
66 ProtocolError,
67 TimeLimitExceeded,
68 SizeLimitExceeded,
69 CompareFalse,
70 CompareTrue,
71 AuthMethodNotSupported,
72 StrongerAuthRequired,
73 Referral,
74 AdminLimitExceeded,
75 UnavailableCriticalExtension,
76 ConfidentialityRequired,
77 SaslBindInProgress,
78 NoSuchAttribute,
79 UndefinedAttributeType,
80 InappropriateMatching,
81 ConstraintViolation,
82 AttributeOrValueExists,
83 InvalidAttributeSyntax,
84 NoSuchObject,
85 AliasProblem,
86 InvalidDnSyntax,
87 AliasDereferencingProblem,
88 InappropriateAuthentication,
89 InvalidCredentials,
90 InsufficientAccessRights,
91 Busy,
92 Unavailable,
93 UnwillingToPerform,
94 LoopDetect,
95 NamingViolation,
96 ObjectClassViolation,
97 NotAllowedOnNonLeaf,
98 NotAllowedOnRdn,
99 EntryAlreadyExists,
100 ObjectClassModsProhibited,
101 AffectsMultipleDsas,
102 Other,
103 Unknown(i64),
106}
107
108impl ResultCode {
109 pub fn from_i64(code: i64) -> ResultCode {
110 use ResultCode::*;
111 match code {
112 0 => Success,
113 1 => OperationsError,
114 2 => ProtocolError,
115 3 => TimeLimitExceeded,
116 4 => SizeLimitExceeded,
117 5 => CompareFalse,
118 6 => CompareTrue,
119 7 => AuthMethodNotSupported,
120 8 => StrongerAuthRequired,
121 10 => Referral,
122 11 => AdminLimitExceeded,
123 12 => UnavailableCriticalExtension,
124 13 => ConfidentialityRequired,
125 14 => SaslBindInProgress,
126 16 => NoSuchAttribute,
127 17 => UndefinedAttributeType,
128 18 => InappropriateMatching,
129 19 => ConstraintViolation,
130 20 => AttributeOrValueExists,
131 21 => InvalidAttributeSyntax,
132 32 => NoSuchObject,
133 33 => AliasProblem,
134 34 => InvalidDnSyntax,
135 36 => AliasDereferencingProblem,
136 48 => InappropriateAuthentication,
137 49 => InvalidCredentials,
138 50 => InsufficientAccessRights,
139 51 => Busy,
140 52 => Unavailable,
141 53 => UnwillingToPerform,
142 54 => LoopDetect,
143 64 => NamingViolation,
144 65 => ObjectClassViolation,
145 66 => NotAllowedOnNonLeaf,
146 67 => NotAllowedOnRdn,
147 68 => EntryAlreadyExists,
148 69 => ObjectClassModsProhibited,
149 71 => AffectsMultipleDsas,
150 80 => Other,
151 other => Unknown(other),
152 }
153 }
154
155 pub fn as_i64(self) -> i64 {
156 use ResultCode::*;
157 match self {
158 Success => 0,
159 OperationsError => 1,
160 ProtocolError => 2,
161 TimeLimitExceeded => 3,
162 SizeLimitExceeded => 4,
163 CompareFalse => 5,
164 CompareTrue => 6,
165 AuthMethodNotSupported => 7,
166 StrongerAuthRequired => 8,
167 Referral => 10,
168 AdminLimitExceeded => 11,
169 UnavailableCriticalExtension => 12,
170 ConfidentialityRequired => 13,
171 SaslBindInProgress => 14,
172 NoSuchAttribute => 16,
173 UndefinedAttributeType => 17,
174 InappropriateMatching => 18,
175 ConstraintViolation => 19,
176 AttributeOrValueExists => 20,
177 InvalidAttributeSyntax => 21,
178 NoSuchObject => 32,
179 AliasProblem => 33,
180 InvalidDnSyntax => 34,
181 AliasDereferencingProblem => 36,
182 InappropriateAuthentication => 48,
183 InvalidCredentials => 49,
184 InsufficientAccessRights => 50,
185 Busy => 51,
186 Unavailable => 52,
187 UnwillingToPerform => 53,
188 LoopDetect => 54,
189 NamingViolation => 64,
190 ObjectClassViolation => 65,
191 NotAllowedOnNonLeaf => 66,
192 NotAllowedOnRdn => 67,
193 EntryAlreadyExists => 68,
194 ObjectClassModsProhibited => 69,
195 AffectsMultipleDsas => 71,
196 Other => 80,
197 Unknown(code) => code,
198 }
199 }
200
201 pub fn name(self) -> &'static str {
203 use ResultCode::*;
204 match self {
205 Success => "success",
206 OperationsError => "operationsError",
207 ProtocolError => "protocolError",
208 TimeLimitExceeded => "timeLimitExceeded",
209 SizeLimitExceeded => "sizeLimitExceeded",
210 CompareFalse => "compareFalse",
211 CompareTrue => "compareTrue",
212 AuthMethodNotSupported => "authMethodNotSupported",
213 StrongerAuthRequired => "strongerAuthRequired",
214 Referral => "referral",
215 AdminLimitExceeded => "adminLimitExceeded",
216 UnavailableCriticalExtension => "unavailableCriticalExtension",
217 ConfidentialityRequired => "confidentialityRequired",
218 SaslBindInProgress => "saslBindInProgress",
219 NoSuchAttribute => "noSuchAttribute",
220 UndefinedAttributeType => "undefinedAttributeType",
221 InappropriateMatching => "inappropriateMatching",
222 ConstraintViolation => "constraintViolation",
223 AttributeOrValueExists => "attributeOrValueExists",
224 InvalidAttributeSyntax => "invalidAttributeSyntax",
225 NoSuchObject => "noSuchObject",
226 AliasProblem => "aliasProblem",
227 InvalidDnSyntax => "invalidDNSyntax",
228 AliasDereferencingProblem => "aliasDereferencingProblem",
229 InappropriateAuthentication => "inappropriateAuthentication",
230 InvalidCredentials => "invalidCredentials",
231 InsufficientAccessRights => "insufficientAccessRights",
232 Busy => "busy",
233 Unavailable => "unavailable",
234 UnwillingToPerform => "unwillingToPerform",
235 LoopDetect => "loopDetect",
236 NamingViolation => "namingViolation",
237 ObjectClassViolation => "objectClassViolation",
238 NotAllowedOnNonLeaf => "notAllowedOnNonLeaf",
239 NotAllowedOnRdn => "notAllowedOnRDN",
240 EntryAlreadyExists => "entryAlreadyExists",
241 ObjectClassModsProhibited => "objectClassModsProhibited",
242 AffectsMultipleDsas => "affectsMultipleDSAs",
243 Other => "other",
244 Unknown(_) => "an unrecognised result code",
245 }
246 }
247
248 pub fn is_success(self) -> bool {
249 matches!(self, ResultCode::Success)
250 }
251}
252
253impl std::fmt::Display for ResultCode {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 write!(f, "{} ({})", self.name(), self.as_i64())
256 }
257}
258
259#[derive(Debug, Clone)]
261pub struct LdapResult {
262 pub code: ResultCode,
263 pub matched_dn: String,
266 pub diagnostic: String,
269 pub referrals: Vec<String>,
270}
271
272impl LdapResult {
273 pub fn is_success(&self) -> bool {
274 self.code.is_success()
275 }
276
277 pub fn into_result(self, operation: &str) -> Result<LdapResult> {
279 if self.is_success() {
280 return Ok(self);
281 }
282
283 let mut message = format!("the directory refused the {operation}: {}", self.code);
284 if !self.diagnostic.is_empty() {
285 message.push_str(&format!(" — {}", self.diagnostic));
286 }
287 if !self.matched_dn.is_empty() {
288 message.push_str(&format!(" (it did find `{}`)", self.matched_dn));
289 }
290 Err(Error::msg(message))
291 }
292
293 fn decode(decoder: &mut Decoder<'_>) -> Result<LdapResult> {
297 let code = ResultCode::from_i64(decoder.enumerated()?);
298 let matched_dn = decoder.string()?;
299 let diagnostic = decoder.string()?;
300
301 let mut referrals = Vec::new();
302 if decoder.has_tag(ber::context(3, true)) {
303 let mut list = decoder.nested(ber::context(3, true))?;
304 while !list.is_empty() {
305 referrals.push(list.string()?);
306 }
307 }
308
309 Ok(LdapResult { code, matched_dn, diagnostic, referrals })
310 }
311}
312
313#[derive(Debug, Clone)]
322pub struct Control {
323 pub oid: String,
324 pub critical: bool,
327 pub value: Option<Vec<u8>>,
328}
329
330impl Control {
331 pub fn new(oid: impl Into<String>) -> Control {
332 Control { oid: oid.into(), critical: false, value: None }
333 }
334
335 pub fn critical(mut self, critical: bool) -> Control {
336 self.critical = critical;
337 self
338 }
339
340 pub fn value(mut self, value: impl Into<Vec<u8>>) -> Control {
341 self.value = Some(value.into());
342 self
343 }
344
345 fn encode(&self, encoder: &mut Encoder) {
346 encoder.sequence(|body| {
347 body.string(&self.oid);
348 if self.critical {
351 body.boolean(true);
352 }
353 if let Some(value) = &self.value {
354 body.octet_string(value);
355 }
356 });
357 }
358
359 fn decode(decoder: &mut Decoder<'_>) -> Result<Control> {
360 let mut fields = decoder.sequence()?;
361 let oid = fields.string()?;
362 let mut critical = false;
363 let mut value = None;
364
365 if fields.has_tag(ber::BOOLEAN) {
366 critical = fields.boolean()?;
367 }
368 if fields.has_tag(ber::OCTET_STRING) {
369 value = Some(fields.octet_string()?.to_vec());
370 }
371
372 Ok(Control { oid, critical, value })
373 }
374}
375
376#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
382pub enum Scope {
383 Base,
385 OneLevel,
387 #[default]
390 Subtree,
391}
392
393impl Scope {
394 pub fn as_i64(self) -> i64 {
395 match self {
396 Scope::Base => 0,
397 Scope::OneLevel => 1,
398 Scope::Subtree => 2,
399 }
400 }
401}
402
403#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
405pub enum DerefAliases {
406 #[default]
411 Never,
412 InSearching,
413 FindingBaseObject,
414 Always,
415}
416
417impl DerefAliases {
418 pub fn as_i64(self) -> i64 {
419 match self {
420 DerefAliases::Never => 0,
421 DerefAliases::InSearching => 1,
422 DerefAliases::FindingBaseObject => 2,
423 DerefAliases::Always => 3,
424 }
425 }
426}
427
428#[derive(Debug, Clone)]
430pub struct SearchRequest {
431 pub base: String,
432 pub scope: Scope,
433 pub deref: DerefAliases,
434 pub size_limit: i64,
436 pub time_limit: i64,
438 pub types_only: bool,
440 pub filter: Filter,
441 pub attributes: Vec<String>,
444}
445
446impl SearchRequest {
447 pub fn new(base: impl Into<String>, filter: Filter) -> SearchRequest {
448 SearchRequest {
449 base: base.into(),
450 scope: Scope::default(),
451 deref: DerefAliases::default(),
452 size_limit: 0,
453 time_limit: 0,
454 types_only: false,
455 filter,
456 attributes: Vec::new(),
457 }
458 }
459
460 pub fn scope(mut self, scope: Scope) -> SearchRequest {
461 self.scope = scope;
462 self
463 }
464
465 pub fn deref(mut self, deref: DerefAliases) -> SearchRequest {
466 self.deref = deref;
467 self
468 }
469
470 pub fn size_limit(mut self, limit: i64) -> SearchRequest {
471 self.size_limit = limit;
472 self
473 }
474
475 pub fn time_limit(mut self, seconds: i64) -> SearchRequest {
476 self.time_limit = seconds;
477 self
478 }
479
480 pub fn types_only(mut self, types_only: bool) -> SearchRequest {
481 self.types_only = types_only;
482 self
483 }
484
485 pub fn attributes<S: Into<String>>(
486 mut self,
487 attributes: impl IntoIterator<Item = S>,
488 ) -> SearchRequest {
489 self.attributes = attributes.into_iter().map(Into::into).collect();
490 self
491 }
492
493 fn encode(&self, encoder: &mut Encoder) {
494 encoder.constructed(tags::SEARCH_REQUEST, |body| {
495 body.string(&self.base);
496 body.enumerated(self.scope.as_i64());
497 body.enumerated(self.deref.as_i64());
498 body.integer(self.size_limit);
499 body.integer(self.time_limit);
500 body.boolean(self.types_only);
501 self.filter.encode(body);
502 body.sequence(|list| {
503 for attribute in &self.attributes {
504 list.string(attribute);
505 }
506 });
507 });
508 }
509}
510
511#[derive(Debug, Clone)]
516pub struct Attribute {
517 pub name: String,
518 pub values: Vec<Vec<u8>>,
519}
520
521impl Attribute {
522 pub fn first(&self) -> Option<&str> {
524 self.values.first().and_then(|value| std::str::from_utf8(value).ok())
525 }
526}
527
528#[derive(Debug, Clone)]
530pub struct SearchEntry {
531 pub dn: String,
532 pub attributes: Vec<Attribute>,
533}
534
535impl SearchEntry {
536 pub fn attribute(&self, name: &str) -> Option<&Attribute> {
543 self.attributes.iter().find(|attribute| attribute.name.eq_ignore_ascii_case(name))
544 }
545
546 pub fn value(&self, name: &str) -> Option<&str> {
548 self.attribute(name).and_then(Attribute::first)
549 }
550
551 pub fn values(&self, name: &str) -> Vec<&str> {
553 self.attribute(name)
554 .map(|attribute| {
555 attribute
556 .values
557 .iter()
558 .filter_map(|value| std::str::from_utf8(value).ok())
559 .collect()
560 })
561 .unwrap_or_default()
562 }
563
564 fn decode(decoder: &mut Decoder<'_>) -> Result<SearchEntry> {
565 let dn = decoder.string()?;
566 let mut attributes = Vec::new();
567
568 let mut list = decoder.sequence()?;
569 while !list.is_empty() {
570 let mut partial = list.sequence()?;
571 let name = partial.string()?;
572 let mut values = Vec::new();
573 let mut set = partial.set()?;
574 while !set.is_empty() {
575 values.push(set.octet_string()?.to_vec());
576 }
577 attributes.push(Attribute { name, values });
578 }
579
580 Ok(SearchEntry { dn, attributes })
581 }
582}
583
584#[derive(Debug, Clone)]
586pub struct SearchOutcome {
587 pub entries: Vec<SearchEntry>,
588 pub referrals: Vec<Vec<String>>,
592 pub result: LdapResult,
593}
594
595#[derive(Debug, Clone, PartialEq, Eq)]
616pub enum Filter {
617 And(Vec<Filter>),
620 Or(Vec<Filter>),
622 Not(Box<Filter>),
623 Equality { attribute: String, value: String },
625 GreaterOrEqual { attribute: String, value: String },
627 LessOrEqual { attribute: String, value: String },
629 Approximate { attribute: String, value: String },
631 Present { attribute: String },
633 Substrings {
636 attribute: String,
637 initial: Option<String>,
638 any: Vec<String>,
639 last: Option<String>,
640 },
641}
642
643impl Filter {
644 pub fn equals(attribute: impl Into<String>, value: impl Into<String>) -> Filter {
645 Filter::Equality { attribute: attribute.into(), value: value.into() }
646 }
647
648 pub fn greater_or_equal(attribute: impl Into<String>, value: impl Into<String>) -> Filter {
649 Filter::GreaterOrEqual { attribute: attribute.into(), value: value.into() }
650 }
651
652 pub fn less_or_equal(attribute: impl Into<String>, value: impl Into<String>) -> Filter {
653 Filter::LessOrEqual { attribute: attribute.into(), value: value.into() }
654 }
655
656 pub fn approximately(attribute: impl Into<String>, value: impl Into<String>) -> Filter {
657 Filter::Approximate { attribute: attribute.into(), value: value.into() }
658 }
659
660 pub fn present(attribute: impl Into<String>) -> Filter {
661 Filter::Present { attribute: attribute.into() }
662 }
663
664 pub fn starts_with(attribute: impl Into<String>, prefix: impl Into<String>) -> Filter {
665 Filter::Substrings {
666 attribute: attribute.into(),
667 initial: Some(prefix.into()),
668 any: Vec::new(),
669 last: None,
670 }
671 }
672
673 pub fn ends_with(attribute: impl Into<String>, suffix: impl Into<String>) -> Filter {
674 Filter::Substrings {
675 attribute: attribute.into(),
676 initial: None,
677 any: Vec::new(),
678 last: Some(suffix.into()),
679 }
680 }
681
682 pub fn contains(attribute: impl Into<String>, fragment: impl Into<String>) -> Filter {
683 Filter::Substrings {
684 attribute: attribute.into(),
685 initial: None,
686 any: vec![fragment.into()],
687 last: None,
688 }
689 }
690
691 pub fn and(filters: impl IntoIterator<Item = Filter>) -> Filter {
692 Filter::And(filters.into_iter().collect())
693 }
694
695 pub fn or(filters: impl IntoIterator<Item = Filter>) -> Filter {
696 Filter::Or(filters.into_iter().collect())
697 }
698
699 pub fn negate(self) -> Filter {
702 Filter::Not(Box::new(self))
703 }
704
705 pub fn encode(&self, encoder: &mut Encoder) {
707 match self {
708 Filter::And(filters) => {
709 encoder.constructed(ber::context(0, true), |body| {
710 for filter in filters {
711 filter.encode(body);
712 }
713 });
714 }
715 Filter::Or(filters) => {
716 encoder.constructed(ber::context(1, true), |body| {
717 for filter in filters {
718 filter.encode(body);
719 }
720 });
721 }
722 Filter::Not(inner) => {
725 encoder.constructed(ber::context(2, true), |body| inner.encode(body));
726 }
727 Filter::Equality { attribute, value } => {
728 assertion(encoder, ber::context(3, true), attribute, value);
729 }
730 Filter::Substrings { attribute, initial, any, last } => {
731 encoder.constructed(ber::context(4, true), |body| {
732 body.string(attribute);
733 body.sequence(|parts| {
734 if let Some(initial) = initial {
735 parts.tagged_string(ber::context(0, false), initial);
736 }
737 for fragment in any {
738 parts.tagged_string(ber::context(1, false), fragment);
739 }
740 if let Some(last) = last {
741 parts.tagged_string(ber::context(2, false), last);
742 }
743 });
744 });
745 }
746 Filter::GreaterOrEqual { attribute, value } => {
747 assertion(encoder, ber::context(5, true), attribute, value);
748 }
749 Filter::LessOrEqual { attribute, value } => {
750 assertion(encoder, ber::context(6, true), attribute, value);
751 }
752 Filter::Present { attribute } => {
754 encoder.tagged_string(ber::context(7, false), attribute);
755 }
756 Filter::Approximate { attribute, value } => {
757 assertion(encoder, ber::context(8, true), attribute, value);
758 }
759 }
760 }
761}
762
763impl std::ops::Not for Filter {
765 type Output = Filter;
766
767 fn not(self) -> Filter {
768 Filter::Not(Box::new(self))
769 }
770}
771
772fn assertion(encoder: &mut Encoder, tag: u8, attribute: &str, value: &str) {
773 encoder.constructed(tag, |body| {
774 body.string(attribute);
775 body.string(value);
776 });
777}
778
779impl std::fmt::Display for Filter {
780 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
782 match self {
783 Filter::And(filters) => {
784 f.write_str("(&")?;
785 for filter in filters {
786 write!(f, "{filter}")?;
787 }
788 f.write_str(")")
789 }
790 Filter::Or(filters) => {
791 f.write_str("(|")?;
792 for filter in filters {
793 write!(f, "{filter}")?;
794 }
795 f.write_str(")")
796 }
797 Filter::Not(inner) => write!(f, "(!{inner})"),
798 Filter::Equality { attribute, value } => {
799 write!(f, "({}={})", escape_filter_value(attribute), escape_filter_value(value))
800 }
801 Filter::GreaterOrEqual { attribute, value } => {
802 write!(f, "({}>={})", escape_filter_value(attribute), escape_filter_value(value))
803 }
804 Filter::LessOrEqual { attribute, value } => {
805 write!(f, "({}<={})", escape_filter_value(attribute), escape_filter_value(value))
806 }
807 Filter::Approximate { attribute, value } => {
808 write!(f, "({}~={})", escape_filter_value(attribute), escape_filter_value(value))
809 }
810 Filter::Present { attribute } => write!(f, "({}=*)", escape_filter_value(attribute)),
811 Filter::Substrings { attribute, initial, any, last } => {
812 write!(f, "({}=", escape_filter_value(attribute))?;
813 if let Some(initial) = initial {
814 f.write_str(&escape_filter_value(initial))?;
815 }
816 f.write_str("*")?;
817 for fragment in any {
818 f.write_str(&escape_filter_value(fragment))?;
819 f.write_str("*")?;
820 }
821 if let Some(last) = last {
822 f.write_str(&escape_filter_value(last))?;
823 }
824 f.write_str(")")
825 }
826 }
827 }
828}
829
830pub fn escape_filter_value(value: &str) -> String {
844 let mut escaped = String::with_capacity(value.len());
845 for character in value.chars() {
846 match character {
847 '\\' => escaped.push_str("\\5c"),
848 '*' => escaped.push_str("\\2a"),
849 '(' => escaped.push_str("\\28"),
850 ')' => escaped.push_str("\\29"),
851 '\0' => escaped.push_str("\\00"),
852 other => escaped.push(other),
853 }
854 }
855 escaped
856}
857
858pub fn escape_dn_value(value: &str) -> String {
868 let mut escaped = String::with_capacity(value.len());
869 let characters: Vec<char> = value.chars().collect();
870
871 for (index, &character) in characters.iter().enumerate() {
872 let first = index == 0;
873 let last = index + 1 == characters.len();
874
875 match character {
876 '\0' => escaped.push_str("\\00"),
877 '"' | '+' | ',' | ';' | '<' | '>' | '\\' | '=' => {
878 escaped.push('\\');
879 escaped.push(character);
880 }
881 '#' if first => escaped.push_str("\\#"),
882 ' ' if first || last => escaped.push_str("\\ "),
883 other => escaped.push(other),
884 }
885 }
886
887 escaped
888}
889
890pub struct Operation {
900 kind: &'static str,
901 op: Vec<u8>,
902 controls: Vec<Control>,
903 carries_password: bool,
907}
908
909impl Operation {
910 pub fn simple_bind(dn: &str, password: &str) -> Result<Operation> {
924 if password.is_empty() {
925 return Err(Error::msg(
926 "refusing to send a simple bind with an empty password. LDAP treats that as an \
927 anonymous bind and answers `success`, so a blank password field would report a \
928 successful login. If an anonymous bind is what you meant, ask for one by name \
929 with `bind_anonymous`.",
930 ));
931 }
932
933 Ok(Operation {
934 kind: "bind",
935 op: bind_bytes(dn, password),
936 controls: Vec::new(),
937 carries_password: true,
938 })
939 }
940
941 pub fn anonymous_bind() -> Operation {
946 Operation {
947 kind: "anonymous bind",
948 op: bind_bytes("", ""),
949 controls: Vec::new(),
950 carries_password: false,
951 }
952 }
953
954 pub fn search(request: &SearchRequest) -> Operation {
955 let mut encoder = Encoder::new();
956 request.encode(&mut encoder);
957 Operation {
958 kind: "search",
959 op: encoder.into_bytes(),
960 controls: Vec::new(),
961 carries_password: false,
962 }
963 }
964
965 pub fn unbind() -> Operation {
968 let mut encoder = Encoder::new();
969 encoder.element(tags::UNBIND_REQUEST, &[]);
970 Operation {
971 kind: "unbind",
972 op: encoder.into_bytes(),
973 controls: Vec::new(),
974 carries_password: false,
975 }
976 }
977
978 pub fn extended(oid: &str, value: Option<&[u8]>) -> Operation {
979 let mut encoder = Encoder::new();
980 encoder.constructed(tags::EXTENDED_REQUEST, |body| {
981 body.tagged_string(ber::context(0, false), oid);
982 if let Some(value) = value {
983 body.element(ber::context(1, false), value);
984 }
985 });
986 Operation {
987 kind: "extended request",
988 op: encoder.into_bytes(),
989 controls: Vec::new(),
990 carries_password: false,
991 }
992 }
993
994 pub fn start_tls() -> Operation {
995 let mut operation = Operation::extended(START_TLS_OID, None);
996 operation.kind = "StartTLS request";
997 operation
998 }
999
1000 pub fn with_control(mut self, control: Control) -> Operation {
1001 self.controls.push(control);
1002 self
1003 }
1004
1005 pub fn kind(&self) -> &'static str {
1006 self.kind
1007 }
1008
1009 pub fn carries_password(&self) -> bool {
1010 self.carries_password
1011 }
1012
1013 pub fn encode(&self, message_id: i64) -> Vec<u8> {
1015 let mut encoder = Encoder::new();
1016 encoder.sequence(|body| {
1017 body.integer(message_id);
1018 body.raw(&self.op);
1019 if !self.controls.is_empty() {
1020 body.constructed(ber::context(0, true), |list| {
1021 for control in &self.controls {
1022 control.encode(list);
1023 }
1024 });
1025 }
1026 });
1027 encoder.into_bytes()
1028 }
1029}
1030
1031fn bind_bytes(dn: &str, password: &str) -> Vec<u8> {
1032 let mut encoder = Encoder::new();
1033 encoder.constructed(tags::BIND_REQUEST, |body| {
1034 body.integer(VERSION);
1035 body.string(dn);
1036 body.tagged_string(ber::context(0, false), password);
1038 });
1039 encoder.into_bytes()
1040}
1041
1042#[derive(Debug, Clone)]
1048pub enum ProtocolOp {
1049 BindResponse { result: LdapResult, server_sasl_credentials: Option<Vec<u8>> },
1050 SearchResultEntry(SearchEntry),
1051 SearchResultReference(Vec<String>),
1052 SearchResultDone(LdapResult),
1053 ExtendedResponse { result: LdapResult, name: Option<String>, value: Option<Vec<u8>> },
1054 IntermediateResponse,
1055 Unrecognised { tag: u8 },
1059}
1060
1061impl ProtocolOp {
1062 pub fn result(&self) -> Option<&LdapResult> {
1064 match self {
1065 ProtocolOp::BindResponse { result, .. }
1066 | ProtocolOp::SearchResultDone(result)
1067 | ProtocolOp::ExtendedResponse { result, .. } => Some(result),
1068 _ => None,
1069 }
1070 }
1071
1072 pub fn name(&self) -> &'static str {
1073 match self {
1074 ProtocolOp::BindResponse { .. } => "a bind response",
1075 ProtocolOp::SearchResultEntry(_) => "a search result entry",
1076 ProtocolOp::SearchResultReference(_) => "a search result reference",
1077 ProtocolOp::SearchResultDone(_) => "a search result",
1078 ProtocolOp::ExtendedResponse { .. } => "an extended response",
1079 ProtocolOp::IntermediateResponse => "an intermediate response",
1080 ProtocolOp::Unrecognised { .. } => "an operation this client does not implement",
1081 }
1082 }
1083}
1084
1085#[derive(Debug, Clone)]
1087pub struct LdapMessage {
1088 pub id: i64,
1092 pub op: ProtocolOp,
1093 pub controls: Vec<Control>,
1094}
1095
1096impl LdapMessage {
1097 pub fn parse(bytes: &[u8]) -> Result<LdapMessage> {
1100 let mut outer = Decoder::new(bytes);
1101 let mut message = outer.sequence()?;
1102 outer.finish("an LDAP message")?;
1103
1104 let id = message.integer()?;
1105 if id < 0 {
1106 return Err(Error::msg(format!(
1107 "the directory sent message id {id}; RFC 4511 says a message id is a positive \
1108 integer or zero"
1109 )));
1110 }
1111
1112 let tag = message
1113 .peek_tag()
1114 .ok_or_else(|| Error::msg("an LDAP message carried an id but no operation"))?;
1115
1116 let op = match tag {
1117 tags::BIND_RESPONSE => {
1118 let mut fields = message.nested(tag)?;
1119 let result = LdapResult::decode(&mut fields)?;
1120 let credentials = optional_octets(&mut fields, ber::context(7, false))?;
1121 ProtocolOp::BindResponse { result, server_sasl_credentials: credentials }
1122 }
1123 tags::SEARCH_RESULT_ENTRY => {
1124 let mut fields = message.nested(tag)?;
1125 ProtocolOp::SearchResultEntry(SearchEntry::decode(&mut fields)?)
1126 }
1127 tags::SEARCH_RESULT_DONE => {
1128 let mut fields = message.nested(tag)?;
1129 ProtocolOp::SearchResultDone(LdapResult::decode(&mut fields)?)
1130 }
1131 tags::SEARCH_RESULT_REFERENCE => {
1132 let mut fields = message.nested(tag)?;
1133 let mut uris = Vec::new();
1134 while !fields.is_empty() {
1135 uris.push(fields.string()?);
1136 }
1137 ProtocolOp::SearchResultReference(uris)
1138 }
1139 tags::EXTENDED_RESPONSE => {
1140 let mut fields = message.nested(tag)?;
1141 let result = LdapResult::decode(&mut fields)?;
1142 let name = match fields.has_tag(ber::context(10, false)) {
1143 true => Some(fields.tagged_string(ber::context(10, false))?),
1144 false => None,
1145 };
1146 let value = optional_octets(&mut fields, ber::context(11, false))?;
1147 ProtocolOp::ExtendedResponse { result, name, value }
1148 }
1149 tags::INTERMEDIATE_RESPONSE => {
1150 message.skip()?;
1151 ProtocolOp::IntermediateResponse
1152 }
1153 other => {
1154 message.skip()?;
1155 ProtocolOp::Unrecognised { tag: other }
1156 }
1157 };
1158
1159 let mut controls = Vec::new();
1160 if message.has_tag(ber::context(0, true)) {
1161 let mut list = message.nested(ber::context(0, true))?;
1162 while !list.is_empty() {
1163 controls.push(Control::decode(&mut list)?);
1164 }
1165 }
1166
1167 message.finish("an LDAP message")?;
1168 Ok(LdapMessage { id, op, controls })
1169 }
1170}
1171
1172fn optional_octets(decoder: &mut Decoder<'_>, tag: u8) -> Result<Option<Vec<u8>>> {
1173 if decoder.has_tag(tag) {
1174 return Ok(Some(decoder.expect(tag)?.to_vec()));
1175 }
1176 Ok(None)
1177}
1178
1179#[cfg(test)]
1180mod tests {
1181 use super::*;
1182
1183 fn filter_bytes(filter: &Filter) -> Vec<u8> {
1184 let mut encoder = Encoder::new();
1185 filter.encode(&mut encoder);
1186 encoder.into_bytes()
1187 }
1188
1189 #[test]
1190 fn an_anonymous_bind_is_the_message_every_reference_shows() {
1191 assert_eq!(
1196 Operation::anonymous_bind().encode(1),
1197 vec![
1198 0x30, 0x0c, 0x02, 0x01, 0x01, 0x60, 0x07, 0x02, 0x01, 0x03, 0x04, 0x00, 0x80, 0x00, ]
1205 );
1206 }
1207
1208 #[test]
1209 fn a_simple_bind_carries_the_dn_and_the_password_unwrapped() {
1210 let bytes = Operation::simple_bind("cn=root", "hunter2").unwrap().encode(2);
1211
1212 assert_eq!(
1213 bytes,
1214 vec![
1215 0x30, 0x1a, 0x02, 0x01, 0x02, 0x60, 0x15, 0x02, 0x01, 0x03, 0x04, 0x07, b'c', b'n', b'=', b'r', b'o', b'o', b't', 0x80, 0x07, b'h', b'u', b'n', b't', b'e', b'r', b'2',
1221 ]
1222 );
1223
1224 assert!(bytes.windows(7).any(|window| window == b"hunter2"));
1227 assert!(Operation::simple_bind("cn=root", "hunter2").unwrap().carries_password());
1228 assert!(!Operation::anonymous_bind().carries_password());
1229 }
1230
1231 #[test]
1232 fn an_empty_password_is_refused_before_it_reaches_the_wire() {
1233 let error = match Operation::simple_bind("uid=alice,dc=example,dc=test", "") {
1244 Ok(_) => panic!("an empty password must never build a bind"),
1245 Err(error) => error.to_string(),
1246 };
1247
1248 assert!(error.contains("anonymous"), "the error must say what an empty password becomes");
1249 assert!(error.contains("bind_anonymous"), "and name the deliberate alternative: {error}");
1250
1251 assert!(Operation::simple_bind("uid=alice,dc=example,dc=test", " ").is_ok());
1253 }
1254
1255 #[test]
1256 fn unbind_is_a_primitive_null_with_no_response() {
1257 assert_eq!(Operation::unbind().encode(3), vec![0x30, 0x05, 0x02, 0x01, 0x03, 0x42, 0x00]);
1258 }
1259
1260 #[test]
1261 fn start_tls_names_the_oid_from_rfc_4511() {
1262 let bytes = Operation::start_tls().encode(1);
1263 let oid = START_TLS_OID.as_bytes();
1264
1265 assert_eq!(&bytes[..5], &[0x30, 0x1d, 0x02, 0x01, 0x01]);
1266 assert_eq!(bytes[5], 0x77, "[APPLICATION 23] ExtendedRequest");
1267 assert_eq!(bytes[7], 0x80, "requestName [0], primitive");
1268 assert_eq!(bytes[8] as usize, oid.len());
1269 assert_eq!(&bytes[9..], oid);
1270 }
1271
1272 #[test]
1273 fn an_equality_filter_puts_the_value_in_a_length_delimited_string() {
1274 assert_eq!(
1276 filter_bytes(&Filter::equals("uid", "alice")),
1277 vec![
1278 0xa3, 0x0c, 0x04, 0x03, b'u', b'i', b'd', 0x04, 0x05, b'a', b'l', b'i', b'c', b'e',
1281 ]
1282 );
1283 }
1284
1285 #[test]
1286 fn an_asterisk_in_an_equality_value_stays_a_literal_asterisk() {
1287 assert_eq!(
1293 filter_bytes(&Filter::equals("uid", "*")),
1294 vec![0xa3, 0x08, 0x04, 0x03, b'u', b'i', b'd', 0x04, 0x01, 0x2a]
1295 );
1296
1297 assert_eq!(
1300 filter_bytes(&Filter::present("uid")),
1301 vec![0x87, 0x03, b'u', b'i', b'd']
1302 );
1303
1304 let injected = Filter::equals("uid", "alice)(uid=admin");
1306 let bytes = filter_bytes(&injected);
1307 assert_eq!(bytes[0], 0xa3, "still one equality assertion");
1308 assert_eq!(bytes.iter().filter(|&&byte| byte == 0xa3).count(), 1);
1309 }
1310
1311 #[test]
1312 fn a_not_filter_wraps_a_whole_inner_filter_because_the_tag_is_explicit() {
1313 assert_eq!(
1316 filter_bytes(&!Filter::equals("cn", "x")),
1317 vec![0xa2, 0x09, 0xa3, 0x07, 0x04, 0x02, b'c', b'n', 0x04, 0x01, b'x']
1318 );
1319 }
1320
1321 #[test]
1322 fn and_and_or_replace_the_set_tag_rather_than_wrapping_it() {
1323 let bytes = filter_bytes(&Filter::and([Filter::present("a"), Filter::present("b")]));
1325 assert_eq!(bytes, vec![0xa0, 0x06, 0x87, 0x01, b'a', 0x87, 0x01, b'b']);
1326
1327 let bytes = filter_bytes(&Filter::or([Filter::present("a")]));
1328 assert_eq!(bytes, vec![0xa1, 0x03, 0x87, 0x01, b'a']);
1329 }
1330
1331 #[test]
1332 fn a_substring_filter_tags_each_part_by_position() {
1333 let filter = Filter::Substrings {
1335 attribute: "o".into(),
1336 initial: Some("univ".into()),
1337 any: vec!["of".into(), "mich".into()],
1338 last: None,
1339 };
1340
1341 assert_eq!(
1342 filter_bytes(&filter),
1343 vec![
1344 0xa4, 0x15, 0x04, 0x01, b'o', 0x30, 0x10, 0x80, 0x04, b'u', b'n', b'i', b'v', 0x81, 0x02, b'o', b'f', 0x81, 0x04, b'm', b'i', b'c', b'h', ]
1351 );
1352 assert_eq!(filter.to_string(), "(o=univ*of*mich*)");
1353 }
1354
1355 #[test]
1356 fn filters_print_the_examples_from_rfc_4515() {
1357 assert_eq!(Filter::equals("cn", "Babs Jensen").to_string(), "(cn=Babs Jensen)");
1360 assert_eq!(Filter::equals("cn", "Tim Howes").negate().to_string(), "(!(cn=Tim Howes))");
1361 assert_eq!(!Filter::equals("cn", "Tim Howes"), Filter::equals("cn", "Tim Howes").negate());
1363 assert_eq!(
1364 Filter::and([
1365 Filter::equals("objectClass", "Person"),
1366 Filter::or([
1367 Filter::equals("sn", "Jensen"),
1368 Filter::starts_with("cn", "Babs J"),
1369 ]),
1370 ])
1371 .to_string(),
1372 "(&(objectClass=Person)(|(sn=Jensen)(cn=Babs J*)))"
1373 );
1374 assert_eq!(Filter::equals("seeAlso", "").to_string(), "(seeAlso=)");
1375 assert_eq!(Filter::present("objectClass").to_string(), "(objectClass=*)");
1376
1377 assert_eq!(Filter::and([]).to_string(), "(&)");
1379 assert_eq!(Filter::or([]).to_string(), "(|)");
1380 }
1381
1382 #[test]
1383 fn escaping_matches_rfc_4515_s_worked_examples() {
1384 assert_eq!(
1386 Filter::equals("o", "Parens R Us (for all your parenthetical needs)").to_string(),
1387 "(o=Parens R Us \\28for all your parenthetical needs\\29)"
1388 );
1389 assert_eq!(Filter::contains("cn", "*").to_string(), "(cn=*\\2a*)");
1390 assert_eq!(
1391 Filter::equals("filename", "C:\\MyFile").to_string(),
1392 "(filename=C:\\5cMyFile)"
1393 );
1394 assert_eq!(escape_filter_value("\0\0\0\u{4}"), "\\00\\00\\00\u{4}");
1397 }
1398
1399 #[test]
1400 fn an_unescaped_asterisk_would_have_matched_everybody() {
1401 let username = "*";
1404 let naive = format!("(uid={username})");
1405 assert_eq!(naive, "(uid=*)", "which is a presence filter — every account matches");
1406
1407 let escaped = format!("(uid={})", escape_filter_value(username));
1408 assert_eq!(escaped, "(uid=\\2a)", "which matches an account literally named *");
1409
1410 assert_eq!(
1412 escape_filter_value("alice)(uid=admin"),
1413 "alice\\29\\28uid=admin",
1414 "the parentheses have to go, or the filter grows a second clause"
1415 );
1416 }
1417
1418 #[test]
1419 fn dn_escaping_covers_what_rfc_4514_requires() {
1420 assert_eq!(escape_dn_value("Smith, John"), "Smith\\, John");
1421 assert_eq!(escape_dn_value("a+b"), "a\\+b");
1422 assert_eq!(escape_dn_value("back\\slash"), "back\\\\slash");
1423 assert_eq!(escape_dn_value(" alice "), "\\ alice\\ ");
1425 assert_eq!(escape_dn_value("#alice"), "\\#alice");
1426 assert_eq!(escape_dn_value("mid#dle"), "mid#dle");
1427 assert_eq!(
1430 escape_dn_value("alice,ou=admins"),
1431 "alice\\,ou\\=admins"
1432 );
1433 }
1434
1435 #[test]
1436 fn a_search_request_encodes_every_field_in_order() {
1437 let request = SearchRequest::new("dc=example,dc=test", Filter::equals("uid", "alice"))
1438 .scope(Scope::Subtree)
1439 .size_limit(2)
1440 .attributes(["cn"]);
1441
1442 assert_eq!(
1443 Operation::search(&request).encode(4),
1444 vec![
1445 0x30, 0x3c, 0x02, 0x01, 0x04, 0x63, 0x37, 0x04, 0x12, b'd', b'c', b'=', b'e', b'x', b'a', b'm', b'p', b'l', b'e', b',',
1449 b'd', b'c', b'=', b't', b'e', b's', b't', 0x0a, 0x01, 0x02, 0x0a, 0x01, 0x00, 0x02, 0x01, 0x02, 0x02, 0x01, 0x00, 0x01, 0x01, 0x00, 0xa3, 0x0c, 0x04, 0x03, b'u', b'i', b'd', 0x04, 0x05, b'a', b'l', b'i', b'c',
1456 b'e', 0x30, 0x04, 0x04, 0x02, b'c', b'n', ]
1459 );
1460 }
1461
1462 #[test]
1463 fn a_bind_response_decodes_its_code_and_the_directory_s_own_words() {
1464 let mut encoder = Encoder::new();
1466 encoder.sequence(|body| {
1467 body.integer(2);
1468 body.constructed(tags::BIND_RESPONSE, |fields| {
1469 fields.enumerated(49);
1470 fields.string("");
1471 fields.string("80090308: LdapErr: DSID-0C0903A9, comment: ..., data 533, v2580");
1472 });
1473 });
1474
1475 let message = LdapMessage::parse(encoder.as_bytes()).unwrap();
1476 assert_eq!(message.id, 2);
1477
1478 let ProtocolOp::BindResponse { result, .. } = &message.op else {
1479 panic!("expected a bind response, got {}", message.op.name());
1480 };
1481 assert_eq!(result.code, ResultCode::InvalidCredentials);
1482 assert!(result.diagnostic.contains("data 533"));
1483 assert!(!result.is_success());
1484 }
1485
1486 #[test]
1487 fn a_search_result_entry_decodes_its_dn_and_multi_valued_attributes() {
1488 let mut encoder = Encoder::new();
1489 encoder.sequence(|body| {
1490 body.integer(5);
1491 body.constructed(tags::SEARCH_RESULT_ENTRY, |entry| {
1492 entry.string("uid=alice,ou=people,dc=example,dc=test");
1493 entry.sequence(|attributes| {
1494 attributes.sequence(|attribute| {
1495 attribute.string("cn");
1496 attribute.set(|values| {
1497 values.octet_string(b"Alice Liddell");
1498 });
1499 });
1500 attributes.sequence(|attribute| {
1501 attribute.string("objectClass");
1502 attribute.set(|values| {
1503 values.octet_string(b"top");
1504 values.octet_string(b"inetOrgPerson");
1505 });
1506 });
1507 });
1508 });
1509 });
1510
1511 let message = LdapMessage::parse(encoder.as_bytes()).unwrap();
1512 let ProtocolOp::SearchResultEntry(entry) = &message.op else {
1513 panic!("expected an entry, got {}", message.op.name());
1514 };
1515
1516 assert_eq!(entry.dn, "uid=alice,ou=people,dc=example,dc=test");
1517 assert_eq!(entry.value("cn"), Some("Alice Liddell"));
1518 assert_eq!(entry.value("CN"), Some("Alice Liddell"));
1521 assert_eq!(entry.values("objectclass"), vec!["top", "inetOrgPerson"]);
1522 assert_eq!(entry.value("mail"), None);
1523 }
1524
1525 #[test]
1526 fn a_referral_in_a_result_is_read_and_not_followed() {
1527 let mut encoder = Encoder::new();
1528 encoder.sequence(|body| {
1529 body.integer(6);
1530 body.constructed(tags::SEARCH_RESULT_DONE, |fields| {
1531 fields.enumerated(10);
1532 fields.string("dc=example,dc=test");
1533 fields.string("");
1534 fields.constructed(ber::context(3, true), |referral| {
1535 referral.string("ldap://other.example.test/dc=example,dc=test");
1536 });
1537 });
1538 });
1539
1540 let message = LdapMessage::parse(encoder.as_bytes()).unwrap();
1541 let ProtocolOp::SearchResultDone(result) = &message.op else {
1542 panic!("expected a search result");
1543 };
1544 assert_eq!(result.code, ResultCode::Referral);
1545 assert_eq!(result.referrals, vec!["ldap://other.example.test/dc=example,dc=test"]);
1546 assert_eq!(result.matched_dn, "dc=example,dc=test");
1547 }
1548
1549 #[test]
1550 fn controls_round_trip_on_a_request_and_a_response() {
1551 let operation = Operation::search(&SearchRequest::new("", Filter::present("objectClass")))
1552 .with_control(Control::new("1.2.840.113556.1.4.319").critical(true).value(vec![1, 2]));
1553
1554 let bytes = operation.encode(9);
1559 let parsed = LdapMessage::parse(&bytes).unwrap();
1560 assert_eq!(parsed.id, 9);
1561 assert!(matches!(parsed.op, ProtocolOp::Unrecognised { .. }));
1562 assert_eq!(parsed.controls.len(), 1);
1563 assert_eq!(parsed.controls[0].oid, "1.2.840.113556.1.4.319");
1564 assert!(parsed.controls[0].critical);
1565 assert_eq!(parsed.controls[0].value, Some(vec![1, 2]));
1566
1567 let mut encoder = Encoder::new();
1568 encoder.sequence(|body| {
1569 body.integer(9);
1570 body.constructed(tags::SEARCH_RESULT_DONE, |fields| {
1571 fields.enumerated(0);
1572 fields.string("");
1573 fields.string("");
1574 });
1575 body.constructed(ber::context(0, true), |list| {
1576 list.sequence(|control| {
1577 control.string("1.2.840.113556.1.4.319");
1578 control.octet_string(&[7]);
1579 });
1580 });
1581 });
1582
1583 let message = LdapMessage::parse(encoder.as_bytes()).unwrap();
1584 assert_eq!(message.controls.len(), 1);
1585 assert_eq!(message.controls[0].oid, "1.2.840.113556.1.4.319");
1586 assert!(!message.controls[0].critical, "criticality DEFAULT FALSE when absent");
1587 assert_eq!(message.controls[0].value, Some(vec![7]));
1588 }
1589
1590 #[test]
1591 fn an_operation_this_client_never_asked_for_is_named_not_guessed_at() {
1592 let mut encoder = Encoder::new();
1593 encoder.sequence(|body| {
1594 body.integer(1);
1595 body.constructed(ber::application(7, true), |fields| {
1597 fields.enumerated(0);
1598 fields.string("");
1599 fields.string("");
1600 });
1601 });
1602
1603 let message = LdapMessage::parse(encoder.as_bytes()).unwrap();
1604 assert!(matches!(message.op, ProtocolOp::Unrecognised { tag: 0x67 }));
1605 assert!(message.op.result().is_none());
1606 }
1607
1608 #[test]
1609 fn a_truncated_or_corrupt_message_is_an_error_and_never_a_panic() {
1610 let mut encoder = Encoder::new();
1611 encoder.sequence(|body| {
1612 body.integer(1);
1613 body.constructed(tags::BIND_RESPONSE, |fields| {
1614 fields.enumerated(0);
1615 fields.string("");
1616 fields.string("");
1617 });
1618 });
1619 let complete = encoder.into_bytes();
1620
1621 for length in 0..complete.len() {
1622 assert!(
1623 LdapMessage::parse(&complete[..length]).is_err(),
1624 "a {length} byte prefix should not parse"
1625 );
1626 }
1627 assert!(LdapMessage::parse(&complete).is_ok());
1628
1629 let mut extra = complete.clone();
1632 extra.push(0x00);
1633 assert!(LdapMessage::parse(&extra).is_err());
1634 }
1635
1636 #[test]
1637 fn result_codes_round_trip_and_name_themselves() {
1638 for code in [0i64, 1, 32, 49, 50, 53, 80] {
1639 assert_eq!(ResultCode::from_i64(code).as_i64(), code);
1640 }
1641 assert_eq!(ResultCode::from_i64(49), ResultCode::InvalidCredentials);
1642 assert_eq!(ResultCode::from_i64(32), ResultCode::NoSuchObject);
1643 assert_eq!(ResultCode::from_i64(4242), ResultCode::Unknown(4242));
1644 assert_eq!(ResultCode::InvalidCredentials.to_string(), "invalidCredentials (49)");
1645 assert_eq!(ResultCode::Unknown(4242).as_i64(), 4242);
1646 assert!(ResultCode::Success.is_success());
1647 }
1648
1649 #[test]
1650 fn a_failed_result_explains_itself_in_words() {
1651 let result = LdapResult {
1652 code: ResultCode::NoSuchObject,
1653 matched_dn: "dc=example,dc=test".into(),
1654 diagnostic: "no such object".into(),
1655 referrals: Vec::new(),
1656 };
1657
1658 let error = result.into_result("search").unwrap_err().to_string();
1659 assert!(error.contains("noSuchObject (32)"), "got {error}");
1660 assert!(error.contains("no such object"), "got {error}");
1661 assert!(error.contains("dc=example,dc=test"), "the matched DN is the clue: {error}");
1662 }
1663}