UNPKG

10.9 kBJavaScriptView Raw
1// Copyright 2011 Mark Cavage, Inc. All rights reserved.
2
3
4var assert = require('assert-plus');
5
6
7///--- Helpers
8
9function invalidDN(name) {
10 var e = new Error();
11 e.name = 'InvalidDistinguishedNameError';
12 e.message = name;
13 return e;
14}
15
16function isAlphaNumeric(c) {
17 var re = /[A-Za-z0-9]/;
18 return re.test(c);
19}
20
21function isWhitespace(c) {
22 var re = /\s/;
23 return re.test(c);
24}
25
26function repeatChar(c, n) {
27 var out = '';
28 var max = n ? n : 0;
29 for (var i = 0; i < max; i++)
30 out += c;
31 return out;
32}
33
34///--- API
35
36function RDN(obj) {
37 var self = this;
38 this.attrs = {};
39
40 if (obj) {
41 Object.keys(obj).forEach(function (k) {
42 self.set(k, obj[k]);
43 });
44 }
45}
46
47RDN.prototype.set = function rdnSet(name, value, opts) {
48 assert.string(name, 'name (string) required');
49 assert.string(value, 'value (string) required');
50
51 var self = this;
52 var lname = name.toLowerCase();
53 this.attrs[lname] = {
54 value: value,
55 name: name
56 };
57 if (opts && typeof (opts) === 'object') {
58 Object.keys(opts).forEach(function (k) {
59 if (k !== 'value')
60 self.attrs[lname][k] = opts[k];
61 });
62 }
63};
64
65RDN.prototype.equals = function rdnEquals(rdn) {
66 if (typeof (rdn) !== 'object')
67 return false;
68
69 var ourKeys = Object.keys(this.attrs);
70 var theirKeys = Object.keys(rdn.attrs);
71 if (ourKeys.length !== theirKeys.length)
72 return false;
73
74 ourKeys.sort();
75 theirKeys.sort();
76
77 for (var i = 0; i < ourKeys.length; i++) {
78 if (ourKeys[i] !== theirKeys[i])
79 return false;
80 if (this.attrs[ourKeys[i]].value !== rdn.attrs[ourKeys[i]].value)
81 return false;
82 }
83 return true;
84};
85
86
87/**
88 * Convert RDN to string according to specified formatting options.
89 * (see: DN.format for option details)
90 */
91RDN.prototype.format = function rdnFormat(options) {
92 assert.optionalObject(options, 'options must be an object');
93 options = options || {};
94
95 var self = this;
96 var str = '';
97
98 function escapeValue(val, forceQuote) {
99 var out = '';
100 var cur = 0;
101 var len = val.length;
102 var quoted = false;
103 /* BEGIN JSSTYLED */
104 var escaped = /[\\\"]/;
105 var special = /[,=+<>#;]/;
106 /* END JSSTYLED */
107
108 if (len > 0) {
109 // Wrap strings with trailing or leading spaces in quotes
110 quoted = forceQuote || (val[0] == ' ' || val[len-1] == ' ');
111 }
112
113 while (cur < len) {
114 if (escaped.test(val[cur]) || (!quoted && special.test(val[cur]))) {
115 out += '\\';
116 }
117 out += val[cur++];
118 }
119 if (quoted)
120 out = '"' + out + '"';
121 return out;
122 }
123 function sortParsed(a, b) {
124 return self.attrs[a].order - self.attrs[b].order;
125 }
126 function sortStandard(a, b) {
127 var nameCompare = a.localeCompare(b);
128 if (nameCompare === 0) {
129 // TODO: Handle binary values
130 return self.attrs[a].value.localeCompare(self.attrs[b].value);
131 } else {
132 return nameCompare;
133 }
134 }
135
136 var keys = Object.keys(this.attrs);
137 if (options.keepOrder) {
138 keys.sort(sortParsed);
139 } else {
140 keys.sort(sortStandard);
141 }
142
143 keys.forEach(function (key) {
144 var attr = self.attrs[key];
145 if (str.length)
146 str += '+';
147
148 if (options.keepCase) {
149 str += attr.name;
150 } else {
151 if (options.upperName)
152 str += key.toUpperCase();
153 else
154 str += key;
155 }
156
157 str += '=' + escapeValue(attr.value, (options.keepQuote && attr.quoted));
158 });
159
160 return str;
161};
162
163RDN.prototype.toString = function rdnToString() {
164 return this.format();
165};
166
167
168// Thank you OpenJDK!
169function parse(name) {
170 if (typeof (name) !== 'string')
171 throw new TypeError('name (string) required');
172
173 var cur = 0;
174 var len = name.length;
175
176 function parseRdn() {
177 var rdn = new RDN();
178 var order = 0;
179 rdn.spLead = trim();
180 while (cur < len) {
181 var opts = {
182 order: order
183 };
184 var attr = parseAttrType();
185 trim();
186 if (cur >= len || name[cur++] !== '=')
187 throw invalidDN(name);
188
189 trim();
190 // Parameters about RDN value are set in 'opts' by parseAttrValue
191 var value = parseAttrValue(opts);
192 rdn.set(attr, value, opts);
193 rdn.spTrail = trim();
194 if (cur >= len || name[cur] !== '+')
195 break;
196 ++cur;
197 ++order;
198 }
199 return rdn;
200 }
201
202
203 function trim() {
204 var count = 0;
205 while ((cur < len) && isWhitespace(name[cur])) {
206 ++cur;
207 count++;
208 }
209 return count;
210 }
211
212 function parseAttrType() {
213 var beg = cur;
214 while (cur < len) {
215 var c = name[cur];
216 if (isAlphaNumeric(c) ||
217 c == '.' ||
218 c == '-' ||
219 c == ' ') {
220 ++cur;
221 } else {
222 break;
223 }
224 }
225 // Back out any trailing spaces.
226 while ((cur > beg) && (name[cur - 1] == ' '))
227 --cur;
228
229 if (beg == cur)
230 throw invalidDN(name);
231
232 return name.slice(beg, cur);
233 }
234
235 function parseAttrValue(opts) {
236 if (cur < len && name[cur] == '#') {
237 opts.binary = true;
238 return parseBinaryAttrValue();
239 } else if (cur < len && name[cur] == '"') {
240 opts.quoted = true;
241 return parseQuotedAttrValue();
242 } else {
243 return parseStringAttrValue();
244 }
245 }
246
247 function parseBinaryAttrValue() {
248 var beg = cur++;
249 while (cur < len && isAlphaNumeric(name[cur]))
250 ++cur;
251
252 return name.slice(beg, cur);
253 }
254
255 function parseQuotedAttrValue() {
256 var str = '';
257 ++cur; // Consume the first quote
258
259 while ((cur < len) && name[cur] != '"') {
260 if (name[cur] === '\\')
261 cur++;
262 str += name[cur++];
263 }
264 if (cur++ >= len) // no closing quote
265 throw invalidDN(name);
266
267 return str;
268 }
269
270 function parseStringAttrValue() {
271 var beg = cur;
272 var str = '';
273 var esc = -1;
274
275 while ((cur < len) && !atTerminator()) {
276 if (name[cur] === '\\') {
277 // Consume the backslash and mark its place just in case it's escaping
278 // whitespace which needs to be preserved.
279 esc = cur++;
280 }
281 if (cur === len) // backslash followed by nothing
282 throw invalidDN(name);
283 str += name[cur++];
284 }
285
286 // Trim off (unescaped) trailing whitespace and rewind cursor to the end of
287 // the AttrValue to record whitespace length.
288 for (; cur > beg; cur--) {
289 if (!isWhitespace(name[cur - 1]) || (esc === (cur - 1)))
290 break;
291 }
292 return str.slice(0, cur - beg);
293 }
294
295 function atTerminator() {
296 return (cur < len &&
297 (name[cur] === ',' ||
298 name[cur] === ';' ||
299 name[cur] === '+'));
300 }
301
302 var rdns = [];
303
304 // Short-circuit for empty DNs
305 if (len === 0)
306 return new DN(rdns);
307
308 rdns.push(parseRdn());
309 while (cur < len) {
310 if (name[cur] === ',' || name[cur] === ';') {
311 ++cur;
312 rdns.push(parseRdn());
313 } else {
314 throw invalidDN(name);
315 }
316 }
317
318 return new DN(rdns);
319}
320
321
322function DN(rdns) {
323 assert.optionalArrayOfObject(rdns, '[object] required');
324
325 this.rdns = rdns ? rdns.slice() : [];
326 this._format = {};
327}
328Object.defineProperties(DN.prototype, {
329 length: {
330 get: function getLength() { return this.rdns.length; },
331 configurable: false
332 }
333});
334
335/**
336 * Convert DN to string according to specified formatting options.
337 *
338 * Parameters:
339 * - options: formatting parameters (optional, details below)
340 *
341 * Options are divided into two types:
342 * - Preservation options: Using data recorded during parsing, details of the
343 * original DN are preserved when converting back into a string.
344 * - Modification options: Alter string formatting defaults.
345 *
346 * Preservation options _always_ take precedence over modification options.
347 *
348 * Preservation Options:
349 * - keepOrder: Order of multi-value RDNs.
350 * - keepQuote: RDN values which were quoted will remain so.
351 * - keepSpace: Leading/trailing spaces will be output.
352 * - keepCase: Parsed attr name will be output instead of lowercased version.
353 *
354 * Modification Options:
355 * - upperName: RDN names will be uppercased instead of lowercased.
356 * - skipSpace: Disable trailing space after RDN separators
357 */
358DN.prototype.format = function dnFormat(options) {
359 assert.optionalObject(options, 'options must be an object');
360 options = options || this._format;
361
362 var str = '';
363 this.rdns.forEach(function (rdn) {
364 var rdnString = rdn.format(options);
365 if (str.length !== 0) {
366 str += ',';
367 }
368 if (options.keepSpace) {
369 str += (repeatChar(' ', rdn.spLead) +
370 rdnString + repeatChar(' ', rdn.spTrail));
371 } else if (options.skipSpace === true || str.length === 0) {
372 str += rdnString;
373 } else {
374 str += ' ' + rdnString;
375 }
376 });
377 return str;
378};
379
380/**
381 * Set default string formatting options.
382 */
383DN.prototype.setFormat = function setFormat(options) {
384 assert.object(options, 'options must be an object');
385
386 this._format = options;
387};
388
389DN.prototype.toString = function dnToString() {
390 return this.format();
391};
392
393DN.prototype.parentOf = function parentOf(dn) {
394 if (typeof (dn) !== 'object')
395 dn = parse(dn);
396
397 if (this.rdns.length >= dn.rdns.length)
398 return false;
399
400 var diff = dn.rdns.length - this.rdns.length;
401 for (var i = this.rdns.length - 1; i >= 0; i--) {
402 var myRDN = this.rdns[i];
403 var theirRDN = dn.rdns[i + diff];
404
405 if (!myRDN.equals(theirRDN))
406 return false;
407 }
408
409 return true;
410};
411
412DN.prototype.childOf = function childOf(dn) {
413 if (typeof (dn) !== 'object')
414 dn = parse(dn);
415 return dn.parentOf(this);
416};
417
418DN.prototype.isEmpty = function isEmpty() {
419 return (this.rdns.length === 0);
420};
421
422DN.prototype.equals = function dnEquals(dn) {
423 if (typeof (dn) !== 'object')
424 dn = parse(dn);
425
426 if (this.rdns.length !== dn.rdns.length)
427 return false;
428
429 for (var i = 0; i < this.rdns.length; i++) {
430 if (!this.rdns[i].equals(dn.rdns[i]))
431 return false;
432 }
433
434 return true;
435};
436
437DN.prototype.parent = function dnParent() {
438 if (this.rdns.length !== 0) {
439 var save = this.rdns.shift();
440 var dn = new DN(this.rdns);
441 this.rdns.unshift(save);
442 return dn;
443 }
444
445 return null;
446};
447
448DN.prototype.clone = function dnClone() {
449 var dn = new DN(this.rdns);
450 dn._format = this._format;
451 return dn;
452};
453
454DN.prototype.reverse = function dnReverse() {
455 this.rdns.reverse();
456 return this;
457};
458
459DN.prototype.pop = function dnPop() {
460 return this.rdns.pop();
461};
462
463DN.prototype.push = function dnPush(rdn) {
464 assert.object(rdn, 'rdn (RDN) required');
465
466 return this.rdns.push(rdn);
467};
468
469DN.prototype.shift = function dnShift() {
470 return this.rdns.shift();
471};
472
473DN.prototype.unshift = function dnUnshift(rdn) {
474 assert.object(rdn, 'rdn (RDN) required');
475
476 return this.rdns.unshift(rdn);
477};
478
479DN.isDN = function isDN(dn) {
480 if (!dn || typeof (dn) !== 'object') {
481 return false;
482 }
483 if (dn instanceof DN) {
484 return true;
485 }
486 if (Array.isArray(dn.rdns)) {
487 // Really simple duck-typing for now
488 return true;
489 }
490 return false;
491};
492
493
494///--- Exports
495
496module.exports = {
497 parse: parse,
498 DN: DN,
499 RDN: RDN
500};