123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500 |
- // 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
- };
|