///|
/// A parsed RFC 4514 distinguished name. `rdns` is the ordered list of
/// relative distinguished names; each RDN is an ordered list of attribute
/// type/value pairs (a multi-valued RDN has more than one pair).
pub struct Dn {
rdns : Array[Array[(String, String)]]
} derive(Eq, @debug.Debug)
///|
pub fn Dn::empty() -> Dn {
{ rdns: [] }
}
///|
/// Parse an RFC 4514 DN string.
pub fn parse_dn(s : String) -> Result[Dn, LdapError] {
result_of_ldap(fn() raise LdapError {
let p = DnParser::new(s)
let dn = p.parse_dn()
if p.pos != p.data.length() {
raise invalid_dn("unexpected trailing characters at offset \{p.pos}")
}
dn
})
}
///|
pub fn Dn::parse(s : String) -> Result[Dn, LdapError] {
parse_dn(s)
}
///|
pub fn Dn::to_string(self : Dn) -> String {
let sb = StringBuilder()
for i in 0.. 0 {
sb.write_char(',')
}
let rdn = self.rdns[i]
for j in 0.. 0 {
sb.write_char('+')
}
let (attr_type, attr_value) = rdn[j]
sb.write_string(attr_type)
sb.write_char('=')
sb.write_string(escape_dn_value(attr_value))
}
}
sb.to_string()
}
///|
pub fn Dn::is_empty(self : Dn) -> Bool {
self.rdns.is_empty()
}
///|
/// The number of RDNs.
pub fn Dn::length(self : Dn) -> Int {
self.rdns.length()
}
///|
/// Escape a DN attribute value for serialization. Escapes the special
/// characters `, + " \ < > ; =`, NUL and leading `#`, plus leading and
/// trailing spaces, using the `\XX` hex form. Runs of non-special bytes are
/// decoded as UTF-8 so multi-byte characters survive the round trip.
pub fn escape_dn_value(value : String) -> String {
let bytes = @utf8.encode(value)
let n = bytes.length()
let sb = StringBuilder()
let run : Array[Byte] = []
for i in 0.. Result[String, LdapError] {
result_of_ldap(fn() raise LdapError {
let p = DnParser::new(s)
let bytes = p.read_value_bytes()
@utf8.decode_lossy(bytes[:])
})
}
///|
priv struct DnParser {
data : Bytes
mut pos : Int
}
///|
fn DnParser::new(s : String) -> DnParser {
{ data: @utf8.encode(s), pos: 0 }
}
///|
fn invalid_dn(msg : String) -> LdapError {
LdapError::InvalidDn(msg)
}
///|
fn DnParser::peek(self : DnParser) -> Int? {
if self.pos >= self.data.length() {
return None
}
Some(self.data.get(self.pos).unwrap().to_int())
}
///|
fn DnParser::advance(self : DnParser) -> Unit {
self.pos = self.pos + 1
}
///|
fn DnParser::parse_dn(self : DnParser) -> Dn raise LdapError {
let rdns : Array[Array[(String, String)]] = []
if self.pos >= self.data.length() {
return { rdns, }
}
while true {
let rdn = self.parse_rdn()
rdns.push(rdn)
match self.peek() {
Some(c) if c == ','.to_int() => {
self.advance()
if self.pos >= self.data.length() {
raise invalid_dn("trailing comma")
}
}
_ => break
}
}
{ rdns, }
}
///|
fn DnParser::parse_rdn(
self : DnParser,
) -> Array[(String, String)] raise LdapError {
let pairs : Array[(String, String)] = []
while true {
let pair = self.parse_attr()
pairs.push(pair)
match self.peek() {
Some(c) if c == '+'.to_int() => self.advance()
_ => break
}
}
pairs
}
///|
fn DnParser::parse_attr(self : DnParser) -> (String, String) raise LdapError {
let attr_type = self.read_attr_type()
if attr_type.is_empty() {
raise invalid_dn("missing attribute type at offset \{self.pos}")
}
match self.peek() {
Some(c) if c == '='.to_int() => self.advance()
_ => raise invalid_dn("expected '=' at offset \{self.pos}")
}
let value_bytes = self.read_value_bytes()
let value = @utf8.decode_lossy(value_bytes[:])
(attr_type, value)
}
///|
fn DnParser::read_attr_type(self : DnParser) -> String {
let sb = StringBuilder()
while true {
match self.peek() {
Some(c) if is_dn_attr_char(c) => {
sb.write_char(c.unsafe_to_char())
self.advance()
}
_ => break
}
}
sb.to_string()
}
///|
fn is_dn_attr_char(c : Int) -> Bool {
if c >= 'a'.to_int() && c <= 'z'.to_int() {
return true
}
if c >= 'A'.to_int() && c <= 'Z'.to_int() {
return true
}
if c >= '0'.to_int() && c <= '9'.to_int() {
return true
}
c == '-'.to_int() || c == '.'.to_int() || c == '_'.to_int()
}
///|
fn is_dn_special(c : Int) -> Bool {
c == ','.to_int() ||
c == '='.to_int() ||
c == '+'.to_int() ||
c == '<'.to_int() ||
c == '>'.to_int() ||
c == ';'.to_int() ||
c == '"'.to_int() ||
c == '\\'.to_int() ||
c == '#'.to_int() ||
c == '['.to_int() ||
c == ']'.to_int() ||
c == ' '.to_int()
}
///|
fn DnParser::read_value_bytes(self : DnParser) -> Bytes raise LdapError {
let out : Array[Byte] = []
while true {
match self.peek() {
Some(c) if c == ','.to_int() || c == '+'.to_int() => break
Some(c) if c == '\\'.to_int() => {
self.advance()
let h1 = self.peek()
match h1 {
None => raise invalid_dn("truncated escape at end of input")
Some(a) => {
self.advance()
let h2 = self.peek()
let hi = hex_nibble(a)
let lo = match h2 {
Some(b) => hex_nibble(b)
None => -1
}
if hi >= 0 && lo >= 0 {
self.advance()
out.push(((hi << 4) | lo).to_byte())
} else if is_dn_special(a) {
// RFC 4514 single-character escape, e.g. `\,` or `\\`.
out.push(a.to_byte())
} else {
raise invalid_dn("invalid escape sequence at offset \{self.pos}")
}
}
}
}
Some(c) => {
self.advance()
out.push(c.to_byte())
}
None => break
}
}
Bytes::from_array(out)
}