ldap-rs 0.4.0

LDAP client for Rust
Documentation
use std::borrow::Cow;

use bytes::Bytes;
use once_cell::sync::Lazy;
use pest::{
    iterators::{Pair, Pairs},
    Parser,
};
use pest_derive::Parser;
use rasn::prelude::*;
use rasn_ldap::{AttributeValueAssertion, Filter, MatchingRuleAssertion, SubstringChoice, SubstringFilter};
use regex::bytes::{Captures, Regex};

use crate::error::Error;

type RulePair<'a> = Pair<'a, Rule>;
type RulePairs<'a> = Pairs<'a, Rule>;

#[inline]
fn c2b(c: u8) -> u8 {
    match c {
        b'0'..=b'9' => c - b'0',
        b'a'..=b'f' => c - b'a' + 10,
        b'A'..=b'F' => c - b'A' + 10,
        _ => panic!("Unexpected value"),
    }
}

#[inline]
fn hex2b(data: &[u8]) -> u8 {
    (c2b(data[0]) << 4) | c2b(data[1])
}

fn unescape(s: &[u8]) -> Cow<[u8]> {
    static HEX_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r#"\\([\da-fA-F]{2})"#).unwrap());

    HEX_RE.replace_all(s, |caps: &Captures| [hex2b(&caps[1])])
}

#[derive(Parser)]
#[grammar = "filter.pest"]
pub(crate) struct FilterParser;

pub(crate) fn parse_filter<S: AsRef<str>>(filter: S) -> Result<Filter, Error> {
    let mut parsed = FilterParser::parse(Rule::rfc2254, filter.as_ref())?;
    Ok(parse_rule(parsed.next().expect("No top level rule")))
}

fn as_bytes(pair: &RulePair) -> Bytes {
    unescape(pair.as_str().as_bytes()).into_owned().into()
}

fn as_inner(pair: RulePair) -> RulePair {
    pair.into_inner().next().expect("No inner rule")
}

fn parse_rule(pair: RulePair) -> Filter {
    match pair.as_rule() {
        Rule::and => Filter::And(parse_set(pair.into_inner())),
        Rule::or => Filter::Or(parse_set(pair.into_inner())),
        Rule::not => Filter::Not(Box::new(parse_rule(as_inner(pair)))),
        Rule::simple => parse_simple(pair.into_inner()),
        Rule::present => Filter::Present(as_bytes(&as_inner(pair))),
        Rule::substring => substring_to_ldap(pair.into_inner()),
        Rule::extensible => parse_extensible(pair.into_inner()),
        _ => panic!("Unexpected rule"),
    }
}

fn parse_extensible(pairs: RulePairs) -> Filter {
    let mut assertion = MatchingRuleAssertion::new(None, None, Bytes::default(), false);
    for pair in pairs {
        match pair.as_rule() {
            Rule::ruleid => assertion.matching_rule = Some(as_bytes(&pair)),
            Rule::ident => assertion.r#type = Some(as_bytes(&pair)),
            Rule::string => assertion.match_value = as_bytes(&pair),
            Rule::dnattr => assertion.dn_attributes = true,
            _ => panic!("Unexpected rule"),
        }
    }
    Filter::ExtensibleMatch(assertion)
}

fn substring_to_ldap(mut pairs: RulePairs) -> Filter {
    let attr = as_bytes(&pairs.next().unwrap());
    let choices = pairs
        .map(|pair| match pair.as_rule() {
            Rule::initial => SubstringChoice::Initial(as_bytes(&pair)),
            Rule::any => SubstringChoice::Any(as_bytes(&pair)),
            Rule::final_ => SubstringChoice::Final(as_bytes(&pair)),
            _ => panic!("Unexpected rule"),
        })
        .collect();
    Filter::Substrings(SubstringFilter::new(attr, choices))
}

fn parse_simple(pairs: RulePairs) -> Filter {
    let pairs = pairs.collect::<Vec<_>>();
    let assertion = AttributeValueAssertion::new(as_bytes(&pairs[0]), as_bytes(&pairs[2]));
    match pairs[1].as_rule() {
        Rule::equal => Filter::EqualityMatch(assertion),
        Rule::approx => Filter::ApproxMatch(assertion),
        Rule::greater => Filter::GreaterOrEqual(assertion),
        Rule::less => Filter::LessOrEqual(assertion),
        _ => panic!("Unexpected rule"),
    }
}

#[allow(clippy::mutable_key_type)]
fn parse_set(pairs: RulePairs) -> SetOf<Filter> {
    pairs.map(parse_rule).collect()
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeSet;

    use super::*;

    #[test]
    fn test_parser() {
        let test_filters = vec![
            (
                r#"(cn=Babs Jensen\2a\30T\30\01)"#,
                Filter::EqualityMatch(AttributeValueAssertion::new("cn".into(), "Babs Jensen*0T0\x01".into())),
            ),
            (
                r#"(objectSid=\01\05\00\00\00\00\00\05\15\00\00\00B\c9\b5+\b7\a79\87\16\0c\d4\a5\01\02\00\00)"#,
                Filter::EqualityMatch(AttributeValueAssertion::new(
                    "objectSid".into(),
                    b"\x01\x05\0\0\0\0\0\x05\x15\0\0\0B\xc9\xb5+\xb7\xa79\x87\x16\x0c\xd4\xa5\x01\x02\0\0"
                        .to_vec()
                        .into(),
                )),
            ),
            ("(cn=*)", Filter::Present("cn".into())),
            (
                "(!(cn=Tim Howes))",
                Filter::Not(Box::new(Filter::EqualityMatch(AttributeValueAssertion::new(
                    "cn".into(),
                    "Tim Howes".into(),
                )))),
            ),
            (
                "(&(objectClass=Person)(|(sn=Jensen)(cn=Babs J*)))",
                Filter::And(BTreeSet::from([
                    Filter::EqualityMatch(AttributeValueAssertion::new("objectClass".into(), "Person".into())),
                    Filter::Or(BTreeSet::from([
                        Filter::EqualityMatch(AttributeValueAssertion::new("sn".into(), "Jensen".into())),
                        Filter::Substrings(SubstringFilter::new(
                            "cn".into(),
                            vec![SubstringChoice::Initial("Babs J".into())],
                        )),
                    ])),
                ])),
            ),
            (
                "(o=univ*of*mich*end)",
                Filter::Substrings(SubstringFilter::new(
                    "o".into(),
                    vec![
                        SubstringChoice::Initial("univ".into()),
                        SubstringChoice::Any("of".into()),
                        SubstringChoice::Any("mich".into()),
                        SubstringChoice::Final("end".into()),
                    ],
                )),
            ),
            (
                "(cn:1.2.3.4.5:=Fred Flintstone)",
                Filter::ExtensibleMatch(MatchingRuleAssertion::new(
                    Some("1.2.3.4.5".into()),
                    Some("cn".into()),
                    "Fred Flintstone".into(),
                    false,
                )),
            ),
            (
                "(sn:dn:2.4.6.8.10:=Barney Rubble)",
                Filter::ExtensibleMatch(MatchingRuleAssertion::new(
                    Some("2.4.6.8.10".into()),
                    Some("sn".into()),
                    "Barney Rubble".into(),
                    true,
                )),
            ),
            (
                "(o:dn:=Ace Industry)",
                Filter::ExtensibleMatch(MatchingRuleAssertion::new(
                    None,
                    Some("o".into()),
                    "Ace Industry".into(),
                    true,
                )),
            ),
            (
                "(:dn:2.4.6.8.10:=Dino)",
                Filter::ExtensibleMatch(MatchingRuleAssertion::new(
                    Some("2.4.6.8.10".into()),
                    None,
                    "Dino".into(),
                    true,
                )),
            ),
            (
                "(!(userAccountControl:1.2.840.113556.1.4.803:=2))",
                Filter::Not(Box::new(Filter::ExtensibleMatch(MatchingRuleAssertion::new(
                    Some("1.2.840.113556.1.4.803".into()),
                    Some("userAccountControl".into()),
                    "2".into(),
                    false,
                )))),
            ),
        ];

        for f in test_filters {
            assert_eq!(parse_filter(f.0).unwrap(), f.1);
        }
    }

    #[test]
    fn test_bad_filter_1() {
        let filter = r#"(objectClass=a\00test\bx\dd\\12)"#;
        assert!(parse_filter(filter).is_err());
    }

    #[test]
    fn test_bad_filter_2() {
        let filter = r#"(objectClass=\\CC)"#;
        assert!(parse_filter(filter).is_err());
    }

    #[test]
    fn test_bad_filter_3() {
        let filter = r#"(objectClass=\CC\\)"#;
        assert!(parse_filter(filter).is_err());
    }

    #[test]
    fn test_bad_filter_4() {
        let filter = r#"(objectClass=\aav\bb\0n)"#;
        assert!(parse_filter(filter).is_err());
    }

    #[test]
    fn test_unescape() {
        let hex = br#"hello\20\77\6f\72\6c\64\00\01"#;
        let decoded = unescape(hex);
        assert_eq!(decoded.as_ref(), b"hello world\x00\x01");
    }

    #[test]
    fn test_unescape_bad_pattern() {
        let hex = br#"hello\\gg"#;
        let decoded = unescape(hex);
        assert_eq!(decoded.as_ref(), b"hello\\\\gg");
    }
}