node-ldapjs/lib/dn.js

501 lines
11 KiB
JavaScript

// Copyright 2011 Mark Cavage, Inc. All rights reserved.
var assert = require('assert-plus');
///--- Helpers
function invalidDN(name) {
var e = new Error();
e.name = 'InvalidDistinguishedNameError';
e.message = name;
return e;
}
function isAlphaNumeric(c) {
var re = /[A-Za-z0-9]/;
return re.test(c);
}
function isWhitespace(c) {
var re = /\s/;
return re.test(c);
}
function repeatChar(c, n) {
var out = '';
var max = n ? n : 0;
for (var i = 0; i < max; i++)
out += c;
return out;
}
///--- API
function RDN(obj) {
var self = this;
this.attrs = {};
if (obj) {
Object.keys(obj).forEach(function (k) {
self.set(k, obj[k]);
});
}
}
RDN.prototype.set = function rdnSet(name, value, opts) {
assert.string(name, 'name (string) required');
assert.string(value, 'value (string) required');
var self = this;
var lname = name.toLowerCase();
this.attrs[lname] = {
value: value,
name: name
};
if (opts && typeof (opts) === 'object') {
Object.keys(opts).forEach(function (k) {
if (k !== 'value')
self.attrs[lname][k] = opts[k];
});
}
};
RDN.prototype.equals = function rdnEquals(rdn) {
if (typeof (rdn) !== 'object')
return false;
var ourKeys = Object.keys(this.attrs);
var theirKeys = Object.keys(rdn.attrs);
if (ourKeys.length !== theirKeys.length)
return false;
ourKeys.sort();
theirKeys.sort();
for (var i = 0; i < ourKeys.length; i++) {
if (ourKeys[i] !== theirKeys[i])
return false;
if (this.attrs[ourKeys[i]].value !== rdn.attrs[ourKeys[i]].value)
return false;
}
return true;
};
/**
* Convert RDN to string according to specified formatting options.
* (see: DN.format for option details)
*/
RDN.prototype.format = function rdnFormat(options) {
assert.optionalObject(options, 'options must be an object');
options = options || {};
var self = this;
var str = '';
function escapeValue(val, forceQuote) {
var out = '';
var cur = 0;
var len = val.length;
var quoted = false;
/* BEGIN JSSTYLED */
var escaped = /[\\\"]/;
var special = /[,=+<>#;]/;
/* END JSSTYLED */
if (len > 0) {
// Wrap strings with trailing or leading spaces in quotes
quoted = forceQuote || (val[0] == ' ' || val[len-1] == ' ');
}
while (cur < len) {
if (escaped.test(val[cur]) || (!quoted && special.test(val[cur]))) {
out += '\\';
}
out += val[cur++];
}
if (quoted)
out = '"' + out + '"';
return out;
}
function sortParsed(a, b) {
return self.attrs[a].order - self.attrs[b].order;
}
function sortStandard(a, b) {
var nameCompare = a.localeCompare(b);
if (nameCompare === 0) {
// TODO: Handle binary values
return self.attrs[a].value.localeCompare(self.attrs[b].value);
} else {
return nameCompare;
}
}
var keys = Object.keys(this.attrs);
if (options.keepOrder) {
keys.sort(sortParsed);
} else {
keys.sort(sortStandard);
}
keys.forEach(function (key) {
var attr = self.attrs[key];
if (str.length)
str += '+';
if (options.keepCase) {
str += attr.name;
} else {
if (options.upperName)
str += key.toUpperCase();
else
str += key;
}
str += '=' + escapeValue(attr.value, (options.keepQuote && attr.quoted));
});
return str;
};
RDN.prototype.toString = function rdnToString() {
return this.format();
};
// Thank you OpenJDK!
function parse(name) {
if (typeof (name) !== 'string')
throw new TypeError('name (string) required');
var cur = 0;
var len = name.length;
function parseRdn() {
var rdn = new RDN();
var order = 0;
rdn.spLead = trim();
while (cur < len) {
var opts = {
order: order
};
var attr = parseAttrType();
trim();
if (cur >= len || name[cur++] !== '=')
throw invalidDN(name);
trim();
// Parameters about RDN value are set in 'opts' by parseAttrValue
var value = parseAttrValue(opts);
rdn.set(attr, value, opts);
rdn.spTrail = trim();
if (cur >= len || name[cur] !== '+')
break;
++cur;
++order;
}
return rdn;
}
function trim() {
var count = 0;
while ((cur < len) && isWhitespace(name[cur])) {
++cur;
count++;
}
return count;
}
function parseAttrType() {
var beg = cur;
while (cur < len) {
var c = name[cur];
if (isAlphaNumeric(c) ||
c == '.' ||
c == '-' ||
c == ' ') {
++cur;
} else {
break;
}
}
// Back out any trailing spaces.
while ((cur > beg) && (name[cur - 1] == ' '))
--cur;
if (beg == cur)
throw invalidDN(name);
return name.slice(beg, cur);
}
function parseAttrValue(opts) {
if (cur < len && name[cur] == '#') {
opts.binary = true;
return parseBinaryAttrValue();
} else if (cur < len && name[cur] == '"') {
opts.quoted = true;
return parseQuotedAttrValue();
} else {
return parseStringAttrValue();
}
}
function parseBinaryAttrValue() {
var beg = cur++;
while (cur < len && isAlphaNumeric(name[cur]))
++cur;
return name.slice(beg, cur);
}
function parseQuotedAttrValue() {
var str = '';
++cur; // Consume the first quote
while ((cur < len) && name[cur] != '"') {
if (name[cur] === '\\')
cur++;
str += name[cur++];
}
if (cur++ >= len) // no closing quote
throw invalidDN(name);
return str;
}
function parseStringAttrValue() {
var beg = cur;
var str = '';
var esc = -1;
while ((cur < len) && !atTerminator()) {
if (name[cur] === '\\') {
// Consume the backslash and mark its place just in case it's escaping
// whitespace which needs to be preserved.
esc = cur++;
}
if (cur === len) // backslash followed by nothing
throw invalidDN(name);
str += name[cur++];
}
// Trim off (unescaped) trailing whitespace and rewind cursor to the end of
// the AttrValue to record whitespace length.
for (; cur > beg; cur--) {
if (!isWhitespace(name[cur - 1]) || (esc === (cur - 1)))
break;
}
return str.slice(0, cur - beg);
}
function atTerminator() {
return (cur < len &&
(name[cur] === ',' ||
name[cur] === ';' ||
name[cur] === '+'));
}
var rdns = [];
// Short-circuit for empty DNs
if (len === 0)
return new DN(rdns);
rdns.push(parseRdn());
while (cur < len) {
if (name[cur] === ',' || name[cur] === ';') {
++cur;
rdns.push(parseRdn());
} else {
throw invalidDN(name);
}
}
return new DN(rdns);
}
function DN(rdns) {
assert.optionalArrayOfObject(rdns, '[object] required');
this.rdns = rdns ? rdns.slice() : [];
this._format = {};
}
Object.defineProperties(DN.prototype, {
length: {
get: function getLength() { return this.rdns.length; },
configurable: false
}
});
/**
* Convert DN to string according to specified formatting options.
*
* Parameters:
* - options: formatting parameters (optional, details below)
*
* Options are divided into two types:
* - Preservation options: Using data recorded during parsing, details of the
* original DN are preserved when converting back into a string.
* - Modification options: Alter string formatting defaults.
*
* Preservation options _always_ take precedence over modification options.
*
* Preservation Options:
* - keepOrder: Order of multi-value RDNs.
* - keepQuote: RDN values which were quoted will remain so.
* - keepSpace: Leading/trailing spaces will be output.
* - keepCase: Parsed attr name will be output instead of lowercased version.
*
* Modification Options:
* - upperName: RDN names will be uppercased instead of lowercased.
* - skipSpace: Disable trailing space after RDN separators
*/
DN.prototype.format = function dnFormat(options) {
assert.optionalObject(options, 'options must be an object');
options = options || this._format;
var str = '';
this.rdns.forEach(function (rdn) {
var rdnString = rdn.format(options);
if (str.length !== 0) {
str += ',';
}
if (options.keepSpace) {
str += (repeatChar(' ', rdn.spLead) +
rdnString + repeatChar(' ', rdn.spTrail));
} else if (options.skipSpace === true || str.length === 0) {
str += rdnString;
} else {
str += ' ' + rdnString;
}
});
return str;
};
/**
* Set default string formatting options.
*/
DN.prototype.setFormat = function setFormat(options) {
assert.object(options, 'options must be an object');
this._format = options;
};
DN.prototype.toString = function dnToString() {
return this.format();
};
DN.prototype.parentOf = function parentOf(dn) {
if (typeof (dn) !== 'object')
dn = parse(dn);
if (this.rdns.length >= dn.rdns.length)
return false;
var diff = dn.rdns.length - this.rdns.length;
for (var i = this.rdns.length - 1; i >= 0; i--) {
var myRDN = this.rdns[i];
var theirRDN = dn.rdns[i + diff];
if (!myRDN.equals(theirRDN))
return false;
}
return true;
};
DN.prototype.childOf = function childOf(dn) {
if (typeof (dn) !== 'object')
dn = parse(dn);
return dn.parentOf(this);
};
DN.prototype.isEmpty = function isEmpty() {
return (this.rdns.length === 0);
};
DN.prototype.equals = function dnEquals(dn) {
if (typeof (dn) !== 'object')
dn = parse(dn);
if (this.rdns.length !== dn.rdns.length)
return false;
for (var i = 0; i < this.rdns.length; i++) {
if (!this.rdns[i].equals(dn.rdns[i]))
return false;
}
return true;
};
DN.prototype.parent = function dnParent() {
if (this.rdns.length !== 0) {
var save = this.rdns.shift();
var dn = new DN(this.rdns);
this.rdns.unshift(save);
return dn;
}
return null;
};
DN.prototype.clone = function dnClone() {
var dn = new DN(this.rdns);
dn._format = this._format;
return dn;
};
DN.prototype.reverse = function dnReverse() {
this.rdns.reverse();
return this;
};
DN.prototype.pop = function dnPop() {
return this.rdns.pop();
};
DN.prototype.push = function dnPush(rdn) {
assert.object(rdn, 'rdn (RDN) required');
return this.rdns.push(rdn);
};
DN.prototype.shift = function dnShift() {
return this.rdns.shift();
};
DN.prototype.unshift = function dnUnshift(rdn) {
assert.object(rdn, 'rdn (RDN) required');
return this.rdns.unshift(rdn);
};
DN.isDN = function isDN(dn) {
if (!dn || typeof (dn) !== 'object') {
return false;
}
if (dn instanceof DN) {
return true;
}
if (Array.isArray(dn.rdns)) {
// Really simple duck-typing for now
return true;
}
return false;
};
///--- Exports
module.exports = {
parse: parse,
DN: DN,
RDN: RDN
};