UNPKG

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