pub fn matches_dn_pattern(dn: &str, pattern: &str) -> bool {
let pattern = pattern.replace("$SUFFIX", "*");
if pattern.contains('*') {
let parts: Vec<&str> = pattern.split('*').collect();
let dn_lower = dn.to_lowercase();
let mut current_pos = 0;
for (i, part) in parts.iter().enumerate() {
if part.is_empty() {
continue;
}
let part_lower = part.to_lowercase();
let part_lower = part_lower.trim_matches(',');
if i == 0 {
if !part_lower.is_empty() {
if !dn_lower.starts_with(part_lower) {
return false;
}
current_pos = part_lower.len();
}
} else if i == parts.len() - 1 {
if !part_lower.is_empty() && !dn_lower.ends_with(part_lower) {
return false;
}
} else if let Some(pos) = dn_lower[current_pos..].find(part_lower) {
current_pos += pos + part_lower.len();
} else {
return false;
}
}
true
} else {
dn.eq_ignore_ascii_case(&pattern)
}
}
pub fn is_direct_child(dn: &str, base_dn: &str) -> bool {
if base_dn.contains('*') {
return matches_dn_pattern(dn, base_dn);
}
if dn.eq_ignore_ascii_case(base_dn) {
return false;
}
let dn_lower = dn.to_lowercase();
let base_lower = base_dn.to_lowercase();
if !dn_lower.ends_with(&base_lower) {
return false;
}
let prefix = &dn_lower[..dn_lower.len() - base_lower.len()];
if !prefix.ends_with(',') {
return false;
}
let prefix_trimmed = prefix.trim_end_matches(',');
!prefix_trimmed.contains(',')
}
pub fn is_descendant_or_self(dn: &str, base_dn: &str) -> bool {
let dn_lower = dn.to_lowercase();
let base_lower = base_dn.to_lowercase();
if dn_lower == base_lower {
return true;
}
if dn_lower.ends_with(&base_lower) {
let prefix = &dn_lower[..dn_lower.len() - base_lower.len()];
return prefix.ends_with(',');
}
false
}
pub fn has_wildcard(dn: &str) -> bool {
dn.contains('*') || dn.contains("$SUFFIX")
}
#[derive(Debug, Clone)]
pub struct ParsedDn {
pub components: smallvec::SmallVec<[String; 8]>,
}
impl ParsedDn {
pub fn parse(dn: &str) -> Self {
let mut components: smallvec::SmallVec<[String; 8]> =
dn.split(',').map(|c| c.trim().to_lowercase()).collect();
components.reverse();
Self { components }
}
pub fn depth(&self) -> usize {
self.components.len()
}
pub fn is_ancestor_of(&self, other: &ParsedDn) -> bool {
if self.components.len() >= other.components.len() {
return false;
}
self.components
.iter()
.zip(other.components.iter())
.all(|(a, b)| a == b)
}
pub fn is_parent_of(&self, other: &ParsedDn) -> bool {
self.components.len() + 1 == other.components.len() && self.is_ancestor_of(other)
}
pub fn is_ancestor_or_self(&self, other: &ParsedDn) -> bool {
if self.components.len() > other.components.len() {
return false;
}
self.components
.iter()
.zip(other.components.iter())
.all(|(a, b)| a == b)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parsed_dn_basic() {
let dn = ParsedDn::parse("uid=alice,ou=people,dc=example,dc=com");
assert_eq!(dn.depth(), 4);
assert_eq!(dn.components[0], "dc=com");
assert_eq!(dn.components[1], "dc=example");
assert_eq!(dn.components[2], "ou=people");
assert_eq!(dn.components[3], "uid=alice");
}
#[test]
fn parsed_dn_case_insensitive() {
let dn = ParsedDn::parse("UID=Alice,OU=People,DC=Example,DC=Com");
assert_eq!(dn.components[0], "dc=com");
assert_eq!(dn.components[3], "uid=alice");
}
#[test]
fn ancestor_checks() {
let root = ParsedDn::parse("dc=example,dc=com");
let ou = ParsedDn::parse("ou=people,dc=example,dc=com");
let user = ParsedDn::parse("uid=alice,ou=people,dc=example,dc=com");
assert!(root.is_ancestor_of(&ou));
assert!(root.is_ancestor_of(&user));
assert!(ou.is_ancestor_of(&user));
assert!(!user.is_ancestor_of(&ou));
assert!(!ou.is_ancestor_of(&root));
assert!(!root.is_ancestor_of(&root));
}
#[test]
fn parent_checks() {
let root = ParsedDn::parse("dc=example,dc=com");
let ou = ParsedDn::parse("ou=people,dc=example,dc=com");
let user = ParsedDn::parse("uid=alice,ou=people,dc=example,dc=com");
assert!(root.is_parent_of(&ou));
assert!(ou.is_parent_of(&user));
assert!(!root.is_parent_of(&user));
}
#[test]
fn ancestor_or_self() {
let dn = ParsedDn::parse("ou=people,dc=example,dc=com");
assert!(dn.is_ancestor_or_self(&dn.clone()));
}
#[test]
fn is_direct_child_basic() {
assert!(is_direct_child(
"uid=alice,ou=people,dc=example,dc=com",
"ou=people,dc=example,dc=com"
));
assert!(!is_direct_child(
"ou=people,dc=example,dc=com",
"ou=people,dc=example,dc=com"
));
assert!(!is_direct_child(
"uid=alice,ou=sub,ou=people,dc=example,dc=com",
"ou=people,dc=example,dc=com"
));
}
#[test]
fn is_descendant_or_self_basic() {
assert!(is_descendant_or_self(
"uid=alice,ou=people,dc=example,dc=com",
"ou=people,dc=example,dc=com"
));
assert!(is_descendant_or_self(
"ou=people,dc=example,dc=com",
"ou=people,dc=example,dc=com"
));
assert!(is_descendant_or_self(
"uid=alice,ou=sub,ou=people,dc=example,dc=com",
"ou=people,dc=example,dc=com"
));
assert!(!is_descendant_or_self(
"uid=alice,ou=groups,dc=example,dc=com",
"ou=people,dc=example,dc=com"
));
}
#[test]
fn matches_dn_pattern_basic() {
assert!(matches_dn_pattern(
"uid=alice,ou=people,dc=example,dc=com",
"uid=*,ou=people,dc=example,dc=com"
));
assert!(matches_dn_pattern(
"uid=alice,ou=people,dc=example,dc=com",
"uid=alice,ou=people,dc=example,dc=com"
));
assert!(!matches_dn_pattern(
"uid=alice,ou=people,dc=example,dc=com",
"uid=bob,ou=people,dc=example,dc=com"
));
}
#[test]
fn has_wildcard_check() {
assert!(has_wildcard("uid=*,dc=example,dc=com"));
assert!(has_wildcard("uid=$SUFFIX,dc=test"));
assert!(!has_wildcard("uid=alice,dc=example,dc=com"));
}
}