First commit

This commit is contained in:
2025-10-08 11:12:59 -04:00
commit b0605a28a9
820 changed files with 100317 additions and 0 deletions

13
node_modules/@ldapjs/dn/.eslintrc generated vendored Normal file
View File

@ -0,0 +1,13 @@
{
"parserOptions": {
"ecmaVersion": "latest"
},
"extends": [
"standard"
],
"rules": {
"no-labels": ["error", {"allowLoop": true}]
}
}

10
node_modules/@ldapjs/dn/.github/workflows/main.yml generated vendored Normal file
View File

@ -0,0 +1,10 @@
name: "CI"
on:
pull_request:
push:
branches:
- master
jobs:
call-core-ci:
uses: ldapjs/.github/.github/workflows/node-ci.yml@main

5
node_modules/@ldapjs/dn/.taprc.yaml generated vendored Normal file
View File

@ -0,0 +1,5 @@
reporter: terse
coverage-map: coverage-map.js
files:
- 'lib/**/*.test.js'

21
node_modules/@ldapjs/dn/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
Copyright (c) 2014 Patrick Mooney. All rights reserved.
Copyright (c) 2014 Mark Cavage, Inc. All rights reserved.
Copyright (c) 2022 The LDAPJS Collaborators.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE

8
node_modules/@ldapjs/dn/README.md generated vendored Normal file
View File

@ -0,0 +1,8 @@
# dn
Provides objects for representing and working with LDAP distinguished name
strings as defined by [RFC 4514](https://www.rfc-editor.org/rfc/rfc4514).
## License
MIT.

3
node_modules/@ldapjs/dn/coverage-map.js generated vendored Normal file
View File

@ -0,0 +1,3 @@
'use strict'
module.exports = testFile => testFile.replace(/\.test\.js$/, '.js')

6
node_modules/@ldapjs/dn/index.js generated vendored Normal file
View File

@ -0,0 +1,6 @@
'use strict'
module.exports = {
DN: require('./lib/dn'),
RDN: require('./lib/rdn')
}

11
node_modules/@ldapjs/dn/lib/deprecations.js generated vendored Normal file
View File

@ -0,0 +1,11 @@
'use strict'
const warning = require('process-warning')()
const clazz = 'LdapjsDnWarning'
warning.create(clazz, 'LDAP_DN_DEP_001', 'attribute options is deprecated and are ignored')
warning.create(clazz, 'LDAP_DN_DEP_002', '.format() is deprecated. Use .toString() instead')
warning.create(clazz, 'LDAP_DN_DEP_003', '.set() is deprecated. Use .setAttribute() instead')
warning.create(clazz, 'LDAP_DN_DEP_004', '.setFormat() is deprecated. Options will be ignored')
module.exports = warning

336
node_modules/@ldapjs/dn/lib/dn.js generated vendored Normal file
View File

@ -0,0 +1,336 @@
'use strict'
const warning = require('./deprecations')
const RDN = require('./rdn')
const parseString = require('./utils/parse-string')
/**
* Implements distinguished name strings as described in
* https://www.rfc-editor.org/rfc/rfc4514 as an object.
* This is the primary implementation for parsing and generating DN strings.
*
* @example
* const dn = new DN({rdns: [{cn: 'jdoe', givenName: 'John'}] })
* dn.toString() // 'cn=jdoe+givenName=John'
*/
class DN {
#rdns = []
/**
* @param {object} input
* @param {RDN[]} [input.rdns=[]] A set of RDN objects that define the DN.
* Remember that DNs are in reverse domain order. Thus, the target RDN must
* be the first item and the top-level RDN the last item.
*
* @throws When the provided `rdns` array is invalid.
*/
constructor ({ rdns = [] } = {}) {
if (Array.isArray(rdns) === false) {
throw Error('rdns must be an array')
}
const hasNonRdn = rdns.some(
r => RDN.isRdn(r) === false
)
if (hasNonRdn === true) {
throw Error('rdns must be an array of RDN objects')
}
Array.prototype.push.apply(
this.#rdns,
rdns.map(r => {
if (Object.prototype.toString.call(r) === '[object LdapRdn]') {
return r
}
return new RDN(r)
})
)
}
get [Symbol.toStringTag] () {
return 'LdapDn'
}
/**
* The number of RDNs that make up the DN.
*
* @returns {number}
*/
get length () {
return this.#rdns.length
}
/**
* Determine if the current instance is the child of another DN instance or
* DN string.
*
* @param {DN|string} dn
*
* @returns {boolean}
*/
childOf (dn) {
if (typeof dn === 'string') {
const parsedDn = DN.fromString(dn)
return parsedDn.parentOf(this)
}
return dn.parentOf(this)
}
/**
* Get a new instance that is a replica of the current instance.
*
* @returns {DN}
*/
clone () {
return new DN({ rdns: this.#rdns })
}
/**
* Determine if the instance is equal to another DN.
*
* @param {DN|string} dn
*
* @returns {boolean}
*/
equals (dn) {
if (typeof dn === 'string') {
const parsedDn = DN.fromString(dn)
return parsedDn.equals(this)
}
if (this.length !== dn.length) return false
for (let i = 0; i < this.length; i += 1) {
if (this.#rdns[i].equals(dn.rdnAt(i)) === false) {
return false
}
}
return true
}
/**
* @deprecated Use .toString() instead.
*
* @returns {string}
*/
format () {
warning.emit('LDAP_DN_DEP_002')
return this.toString()
}
/**
* Determine if the instance has any RDNs defined.
*
* @returns {boolean}
*/
isEmpty () {
return this.#rdns.length === 0
}
/**
* Get a DN representation of the parent of this instance.
*
* @returns {DN|undefined}
*/
parent () {
if (this.length === 0) return undefined
const save = this.shift()
const dn = new DN({ rdns: this.#rdns })
this.unshift(save)
return dn
}
/**
* Determine if the instance is the parent of a given DN instance or DN
* string.
*
* @param {DN|string} dn
*
* @returns {boolean}
*/
parentOf (dn) {
if (typeof dn === 'string') {
const parsedDn = DN.fromString(dn)
return this.parentOf(parsedDn)
}
if (this.length >= dn.length) {
// If we have more RDNs in our set then we must be a descendent at least.
return false
}
const numberOfElementsDifferent = dn.length - this.length
for (let i = this.length - 1; i >= 0; i -= 1) {
const myRdn = this.#rdns[i]
const theirRdn = dn.rdnAt(i + numberOfElementsDifferent)
if (myRdn.equals(theirRdn) === false) {
return false
}
}
return true
}
/**
* Removes the last RDN from the list and returns it. This alters the
* instance.
*
* @returns {RDN}
*/
pop () {
return this.#rdns.pop()
}
/**
* Adds a new RDN to the end of the list (i.e. the "top most" RDN in the
* directory path) and returns the new RDN count.
*
* @param {RDN} rdn
*
* @returns {number}
*
* @throws When the input is not a valid RDN.
*/
push (rdn) {
if (Object.prototype.toString.call(rdn) !== '[object LdapRdn]') {
throw Error('rdn must be a RDN instance')
}
return this.#rdns.push(rdn)
}
/**
* Return the RDN at the provided index in the list of RDNs associated with
* this instance.
*
* @param {number} index
*
* @returns {RDN}
*/
rdnAt (index) {
return this.#rdns[index]
}
/**
* Reverse the RDNs list such that the first element becomes the last, and
* the last becomes the first. This is useful when the RDNs were added in the
* opposite order of how they should have been.
*
* This is an in-place operation. The instance is changed as a result of
* this operation.
*
* @returns {DN} The current instance (i.e. this method is chainable).
*/
reverse () {
this.#rdns.reverse()
return this
}
/**
* @deprecated Formatting options are not supported.
*/
setFormat () {
warning.emit('LDAP_DN_DEP_004')
}
/**
* Remove the first RDN from the set of RDNs and return it.
*
* @returns {RDN}
*/
shift () {
return this.#rdns.shift()
}
/**
* Render the DN instance as a spec compliant DN string.
*
* @returns {string}
*/
toString () {
let result = ''
for (const rdn of this.#rdns) {
const rdnString = rdn.toString()
result += `,${rdnString}`
}
return result.substring(1)
}
/**
* Adds an RDN to the beginning of the RDN list and returns the new length.
*
* @param {RDN} rdn
*
* @returns {number}
*
* @throws When the RDN is invalid.
*/
unshift (rdn) {
if (Object.prototype.toString.call(rdn) !== '[object LdapRdn]') {
throw Error('rdn must be a RDN instance')
}
return this.#rdns.unshift(rdn)
}
/**
* Determine if an object is an instance of {@link DN} or is at least
* a DN-like object. It is safer to perform a `toString` check.
*
* @example Valid Instance
* const dn = new DN()
* DN.isDn(dn) // true
*
* @example DN-like Instance
* let dn = { rdns: [{name: 'cn', value: 'foo'}] }
* DN.isDn(dn) // true
*
* dn = { rdns: [{cn: 'foo', sn: 'bar'}, {dc: 'example'}, {dc: 'com'}]}
* DN.isDn(dn) // true
*
* @example Preferred Check
* let dn = new DN()
* Object.prototype.toString.call(dn) === '[object LdapDn]' // true
*
* dn = { rdns: [{name: 'cn', value: 'foo'}] }
* Object.prototype.toString.call(dn) === '[object LdapDn]' // false
*
* @param {object} dn
* @returns {boolean}
*/
static isDn (dn) {
if (Object.prototype.toString.call(dn) === '[object LdapDn]') {
return true
}
if (
Object.prototype.toString.call(dn) !== '[object Object]' ||
Array.isArray(dn.rdns) === false
) {
return false
}
if (dn.rdns.some(dn => RDN.isRdn(dn) === false) === true) {
return false
}
return true
}
/**
* Parses a DN string and returns a new {@link DN} instance.
*
* @example
* const dn = DN.fromString('cn=foo,dc=example,dc=com')
* DN.isDn(dn) // true
*
* @param {string} dnString
*
* @returns {DN}
*
* @throws If the string is not parseable.
*/
static fromString (dnString) {
const rdns = parseString(dnString)
return new DN({ rdns })
}
}
module.exports = DN

527
node_modules/@ldapjs/dn/lib/dn.test.js generated vendored Normal file
View File

@ -0,0 +1,527 @@
'use strict'
const tap = require('tap')
const warning = require('./deprecations')
const RDN = require('./rdn')
const DN = require('./dn')
// Silence the standard warning logs. We will test the messages explicitly.
process.removeAllListeners('warning')
tap.test('constructor', t => {
t.test('throws for non-array', async t => {
t.throws(
() => new DN({ rdns: 42 }),
Error('rdns must be an array')
)
})
t.test('throws for non-rdn in array', async t => {
const rdns = [
new RDN(),
{ 'non-string-value': 42 },
new RDN()
]
t.throws(
() => new DN({ rdns })
)
})
t.test('handles mixed array', async t => {
const rdns = [
{ cn: 'foo' },
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
const dn = new DN({ rdns })
t.equal(dn.length, 3)
t.equal(dn.toString(), 'cn=foo,dc=example,dc=com')
})
t.end()
})
tap.test('childOf', t => {
t.test('false if we are shallower', async t => {
const dn = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const target = new DN({
rdns: [
new RDN({ cn: 'foo' }),
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.childOf(target), false)
})
t.test('false if differing path', async t => {
const dn = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const target = new DN({
rdns: [
new RDN({ dc: 'ldapjs' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.childOf(target), false)
})
t.test('true if we are a child', async t => {
const dn = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const target = new DN({
rdns: [
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.childOf(target), true)
})
t.test('handles string input', async t => {
const dn = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const target = 'dc=example,dc=com'
t.equal(dn.childOf(target), true)
})
t.end()
})
tap.test('clone', t => {
t.test('returns a copy', async t => {
const rdns = [new RDN({ cn: 'foo' })]
const src = new DN({ rdns })
const clone = src.clone()
t.equal(src.length, clone.length)
t.equal(src.toString(), clone.toString())
})
t.end()
})
tap.test('equals', t => {
t.test('false for non-equal length', async t => {
const dn = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const target = new DN({
rdns: [
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.equals(target), false)
})
t.test('false for non-equal paths', async t => {
const dn = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const target = new DN({
rdns: [
new RDN({ ou: 'computers' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.equals(target), false)
})
t.test('true for equal paths', async t => {
const dn = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const target = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.equals(target), true)
})
t.test('handles string input', async t => {
const dn = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const target = 'ou=people,dc=example,dc=com'
t.equal(dn.equals(target), true)
})
t.end()
})
tap.test('format', t => {
t.test('emits warning', t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_DN_DEP_002', false)
})
const rdns = [{ cn: 'foo' }]
const dnString = (new DN({ rdns })).format()
t.equal(dnString, 'cn=foo')
function handler (error) {
t.equal(error.message, '.format() is deprecated. Use .toString() instead')
t.end()
}
})
t.end()
})
tap.test('isEmpty', t => {
t.test('returns correct result', async t => {
let dn = new DN()
t.equal(dn.isEmpty(), true)
dn = new DN({
rdns: [new RDN({ cn: 'foo' })]
})
t.equal(dn.isEmpty(), false)
})
t.end()
})
tap.test('parent', t => {
t.test('undefined for an empty DN', async t => {
const dn = new DN()
const parent = dn.parent()
t.equal(parent, undefined)
})
t.test('returns correct DN', async t => {
const dn = new DN({
rdns: [
new RDN({ cn: 'jdoe', givenName: 'John' }),
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const parent = dn.parent()
t.equal(parent.toString(), 'ou=people,dc=example,dc=com')
})
t.end()
})
tap.test('parentOf', t => {
t.test('false if we are deeper', async t => {
const target = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const dn = new DN({
rdns: [
new RDN({ cn: 'foo' }),
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.parentOf(target), false)
})
t.test('false if differing path', async t => {
const target = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const dn = new DN({
rdns: [
new RDN({ dc: 'ldapjs' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.parentOf(target), false)
})
t.test('true if we are a parent', async t => {
const target = new DN({
rdns: [
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const dn = new DN({
rdns: [
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.parentOf(target), true)
})
t.test('handles string input', async t => {
const dn = new DN({
rdns: [
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const target = 'ou=people,dc=example,dc=com'
t.equal(dn.parentOf(target), true)
})
t.end()
})
tap.test('pop', t => {
t.test('returns the last element and shortens the list', async t => {
const dn = new DN({
rdns: [
new RDN({ cn: 'foo' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.toString(), 'cn=foo,dc=example,dc=com')
const rdn = dn.pop()
t.equal(rdn.toString(), 'dc=com')
t.equal(dn.toString(), 'cn=foo,dc=example')
})
t.end()
})
tap.test('push', t => {
t.test('throws for bad input', async t => {
const dn = new DN()
t.throws(
() => dn.push({ cn: 'foo' }),
Error('rdn must be a RDN instance')
)
})
t.test('adds to the front of the list', async t => {
const dn = new DN({
rdns: [
new RDN({ cn: 'foo' }),
new RDN({ dc: 'example' })
]
})
t.equal(dn.toString(), 'cn=foo,dc=example')
const newLength = dn.push(new RDN({ dc: 'com' }))
t.equal(newLength, 3)
t.equal(dn.toString(), 'cn=foo,dc=example,dc=com')
})
t.end()
})
tap.test('rdnAt', t => {
t.test('returns correct RDN', async t => {
const dn = new DN({
rdns: [
new RDN({ cn: 'jdoe', givenName: 'John' }),
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
const rdn = dn.rdnAt(1)
t.equal(rdn.toString(), 'ou=people')
})
t.end()
})
tap.test('reverse', t => {
t.test('reverses the list', async t => {
const dn = new DN({
rdns: [
new RDN({ dc: 'com' }),
new RDN({ dc: 'example' }),
new RDN({ cn: 'foo' })
]
})
t.equal(dn.toString(), 'dc=com,dc=example,cn=foo')
const result = dn.reverse()
t.equal(dn, result)
t.equal(dn.toString(), 'cn=foo,dc=example,dc=com')
})
t.end()
})
tap.test('setFormat', t => {
t.test('emits warning', t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_DN_DEP_004', false)
})
const rdns = [{ cn: 'foo' }]
new DN({ rdns }).setFormat()
function handler (error) {
t.equal(error.message, '.setFormat() is deprecated. Options will be ignored')
t.end()
}
})
t.end()
})
tap.test('shift', t => {
t.test('returns the first element and shortens the list', async t => {
const dn = new DN({
rdns: [
new RDN({ cn: 'foo' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.toString(), 'cn=foo,dc=example,dc=com')
const rdn = dn.shift()
t.equal(rdn.toString(), 'cn=foo')
t.equal(dn.toString(), 'dc=example,dc=com')
})
t.end()
})
tap.test('toString', t => {
t.test('renders correctly', async t => {
const dn = new DN({
rdns: [
new RDN({ cn: 'jdoe', givenName: 'John' }),
new RDN({ ou: 'people' }),
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.toString(), 'cn=jdoe+givenName=John,ou=people,dc=example,dc=com')
})
t.test('empty string for empty DN', async t => {
const dn = new DN()
t.equal(dn.toString(), '')
})
t.end()
})
tap.test('unshift', t => {
t.test('throws for bad input', async t => {
const dn = new DN()
t.throws(
() => dn.unshift({ cn: 'foo' }),
Error('rdn must be a RDN instance')
)
})
t.test('adds to the front of the list', async t => {
const dn = new DN({
rdns: [
new RDN({ dc: 'example' }),
new RDN({ dc: 'com' })
]
})
t.equal(dn.toString(), 'dc=example,dc=com')
const newLength = dn.unshift(new RDN({ cn: 'foo' }))
t.equal(newLength, 3)
t.equal(dn.toString(), 'cn=foo,dc=example,dc=com')
})
t.end()
})
tap.test('#isDn', t => {
t.test('true for instance', async t => {
const dn = new DN()
t.equal(DN.isDn(dn), true)
})
t.test('false for non-object', async t => {
t.equal(DN.isDn(42), false)
})
t.test('false for non-array rdns', async t => {
const input = { rdns: 42 }
t.equal(DN.isDn(input), false)
})
t.test('false for bad rdn', async t => {
const input = { rdns: [{ bad: 'rdn', answer: 42 }] }
t.equal(DN.isDn(input), false)
})
t.test('true for dn-like', async t => {
const input = { rdns: [{ name: 'cn', value: 'foo' }] }
t.equal(DN.isDn(input), true)
})
t.end()
})
tap.test('#fromString', t => {
t.test('parses a basic string into an instance', async t => {
const input = 'cn=foo+sn=bar,dc=example,dc=com'
const dn = DN.fromString(input)
t.equal(DN.isDn(dn), true)
t.equal(dn.length, 3)
t.equal(dn.rdnAt(0).toString(), 'cn=foo+sn=bar')
})
t.end()
})

257
node_modules/@ldapjs/dn/lib/rdn.js generated vendored Normal file
View File

@ -0,0 +1,257 @@
'use strict'
const warning = require('./deprecations')
const escapeValue = require('./utils/escape-value')
const isDottedDecimal = require('./utils/is-dotted-decimal')
/**
* Implements a relative distinguished name as described in
* https://www.rfc-editor.org/rfc/rfc4514.
*
* @example
* const rdn = new RDN({cn: 'jdoe', givenName: 'John'})
* rdn.toString() // 'cn=jdoe+givenName=John'
*/
class RDN {
#attributes = new Map()
/**
* @param {object} rdn An object of key-values to use as RDN attribute
* types and attribute values. Attribute values should be strings.
*/
constructor (rdn = {}) {
for (const [key, val] of Object.entries(rdn)) {
this.setAttribute({ name: key, value: val })
}
}
get [Symbol.toStringTag] () {
return 'LdapRdn'
}
/**
* The number attributes associated with the RDN.
*
* @returns {number}
*/
get size () {
return this.#attributes.size
}
/**
* Very naive equality check against another RDN instance. In short, if they
* do not have the exact same key names with the exact same values, then
* this check will return `false`.
*
* @param {RDN} rdn
*
* @returns {boolean}
*
* @todo Should implement support for the attribute types listed in https://www.rfc-editor.org/rfc/rfc4514#section-3
*/
equals (rdn) {
if (Object.prototype.toString.call(rdn) !== '[object LdapRdn]') {
return false
}
if (this.size !== rdn.size) {
return false
}
for (const key of this.keys()) {
if (rdn.has(key) === false) return false
if (this.getValue(key) !== rdn.getValue(key)) return false
}
return true
}
/**
* The value associated with the given attribute name.
*
* @param {string} name An attribute name associated with the RDN.
*
* @returns {*}
*/
getValue (name) {
return this.#attributes.get(name)?.value
}
/**
* Determine if the RDN has a specific attribute assigned.
*
* @param {string} name The name of the attribute.
*
* @returns {boolean}
*/
has (name) {
return this.#attributes.has(name)
}
/**
* All attribute names associated with the RDN.
*
* @returns {IterableIterator<string>}
*/
keys () {
return this.#attributes.keys()
}
/**
* Define an attribute type and value on the RDN.
*
* @param {string} name
* @param {string | import('@ldapjs/asn1').BerReader} value
* @param {object} options Deprecated. All options will be ignored.
*
* @throws If any parameter is invalid.
*/
setAttribute ({ name, value, options = {} }) {
if (typeof name !== 'string') {
throw Error('name must be a string')
}
const valType = Object.prototype.toString.call(value)
if (typeof value !== 'string' && valType !== '[object BerReader]') {
throw Error('value must be a string or BerReader')
}
if (Object.prototype.toString.call(options) !== '[object Object]') {
throw Error('options must be an object')
}
const startsWithAlpha = str => /^[a-zA-Z]/.test(str) === true
if (startsWithAlpha(name) === false && isDottedDecimal(name) === false) {
throw Error('attribute name must start with an ASCII alpha character or be a numeric OID')
}
const attr = { value, name }
for (const [key, val] of Object.entries(options)) {
warning.emit('LDAP_DN_DEP_001')
if (key === 'value') continue
attr[key] = val
}
this.#attributes.set(name, attr)
}
/**
* Convert the RDN to a string representation. If an attribute value is
* an instance of `BerReader`, the value will be encoded appropriately.
*
* @example Dotted Decimal Type
* const rdn = new RDN({
* cn: '#foo',
* '1.3.6.1.4.1.1466.0': '#04024869'
* })
* rnd.toString()
* // => 'cn=\23foo+1.3.6.1.4.1.1466.0=#04024869'
*
* @example Unescaped Value
* const rdn = new RDN({
* cn: '#foo'
* })
* rdn.toString({ unescaped: true })
* // => 'cn=#foo'
*
* @param {object} [options]
* @param {boolean} [options.unescaped=false] Return the unescaped version
* of the RDN string.
*
* @returns {string}
*/
toString ({ unescaped = false } = {}) {
let result = ''
const isHexEncodedValue = val => /^#([0-9a-fA-F]{2})+$/.test(val) === true
for (const entry of this.#attributes.values()) {
result += entry.name + '='
if (isHexEncodedValue(entry.value)) {
result += entry.value
} else if (Object.prototype.toString.call(entry.value) === '[object BerReader]') {
let encoded = '#'
for (const byte of entry.value.buffer) {
encoded += Number(byte).toString(16).padStart(2, '0')
}
result += encoded
} else {
result += unescaped === false ? escapeValue(entry.value) : entry.value
}
result += '+'
}
return result.substring(0, result.length - 1)
}
/**
* @returns {string}
*
* @deprecated Use {@link toString}.
*/
format () {
// If we decide to add back support for this, we should do it as
// `.toStringWithFormatting(options)`.
warning.emit('LDAP_DN_DEP_002')
return this.toString()
}
/**
* @param {string} name
* @param {string} value
* @param {object} options
*
* @deprecated Use {@link setAttribute}.
*/
set (name, value, options) {
warning.emit('LDAP_DN_DEP_003')
this.setAttribute({ name, value, options })
}
/**
* Determine if an object is an instance of {@link RDN} or is at least
* a RDN-like object. It is safer to perform a `toString` check.
*
* @example Valid Instance
* const Rdn = new RDN()
* RDN.isRdn(rdn) // true
*
* @example RDN-like Instance
* const rdn = { name: 'cn', value: 'foo' }
* RDN.isRdn(rdn) // true
*
* @example Preferred Check
* let rdn = new RDN()
* Object.prototype.toString.call(rdn) === '[object LdapRdn]' // true
*
* dn = { name: 'cn', value: 'foo' }
* Object.prototype.toString.call(dn) === '[object LdapRdn]' // false
*
* @param {object} rdn
* @returns {boolean}
*/
static isRdn (rdn) {
if (Object.prototype.toString.call(rdn) === '[object LdapRdn]') {
return true
}
const isObject = Object.prototype.toString.call(rdn) === '[object Object]'
if (isObject === false) {
return false
}
if (typeof rdn.name === 'string' && typeof rdn.value === 'string') {
return true
}
for (const value of Object.values(rdn)) {
if (
typeof value !== 'string' &&
Object.prototype.toString.call(value) !== '[object BerReader]'
) return false
}
return true
}
}
module.exports = RDN

214
node_modules/@ldapjs/dn/lib/rdn.test.js generated vendored Normal file
View File

@ -0,0 +1,214 @@
'use strict'
const tap = require('tap')
const warning = require('./deprecations')
const { BerReader } = require('@ldapjs/asn1')
const RDN = require('./rdn')
// Silence the standard warning logs. We will test the messages explicitly.
process.removeAllListeners('warning')
tap.test('equals', t => {
t.test('false for non-rdn object', async t => {
const rdn = new RDN()
t.equal(rdn.equals({}), false)
})
t.test('false for size mis-match', async t => {
const rdn1 = new RDN({ cn: 'foo' })
const rdn2 = new RDN({ cn: 'foo', sn: 'bar' })
t.equal(rdn1.equals(rdn2), false)
})
t.test('false for keys mis-match', async t => {
const rdn1 = new RDN({ cn: 'foo' })
const rdn2 = new RDN({ sn: 'bar' })
t.equal(rdn1.equals(rdn2), false)
})
t.test('false for value mis-match', async t => {
const rdn1 = new RDN({ cn: 'foo' })
const rdn2 = new RDN({ cn: 'bar' })
t.equal(rdn1.equals(rdn2), false)
})
t.test('true for match', async t => {
const rdn1 = new RDN({ cn: 'foo' })
const rdn2 = new RDN({ cn: 'foo' })
t.equal(rdn1.equals(rdn2), true)
})
t.end()
})
tap.test('setAttribute', async t => {
t.test('throws for bad name', async t => {
const rdn = new RDN()
t.throws(
() => rdn.setAttribute({ name: 42 }),
Error('name must be a string')
)
t.throws(
() => rdn.setAttribute({ name: '3cn', value: 'foo' }),
Error('attribute name must start with an ASCII alpha character or be a numeric OID')
)
})
t.test('throws for bad value', async t => {
const rdn = new RDN()
t.throws(
() => rdn.setAttribute({ name: 'cn', value: 42 }),
Error('value must be a string')
)
})
t.test('throws for options', async t => {
const rdn = new RDN()
t.throws(
() => rdn.setAttribute({ name: 'cn', value: 'foo', options: 42 }),
Error('options must be an object')
)
})
t.test('sets an attribute with value', async t => {
const rdn = new RDN()
rdn.setAttribute({ name: 'cn', value: 'foo' })
t.equal(rdn.getValue('cn'), 'foo')
})
t.test('options generates warning', t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_DN_DEP_001', false)
})
const rdn = new RDN()
rdn.setAttribute({ name: 'cn', value: 'foo', options: { foo: 'bar' } })
function handler (error) {
t.equal(error.message, 'attribute options is deprecated and are ignored')
t.end()
}
})
t.end()
})
tap.test('toString', t => {
t.test('basic single value', async t => {
const rdn = new RDN({ cn: 'foo' })
t.equal(rdn.toString(), 'cn=foo')
})
t.test('escaped single value', async t => {
const rdn = new RDN({ cn: ' foo, bar\n' })
t.equal(rdn.toString(), 'cn=\\20foo\\2c bar\\0a')
})
t.test('basic multi-value', async t => {
const rdn = new RDN({ cn: 'foo', sn: 'bar' })
t.equal(rdn.toString(), 'cn=foo+sn=bar')
})
t.test('escaped multi-value', async t => {
const rdn = new RDN({ cn: '#foo', sn: 'bar' })
t.equal(rdn.toString(), 'cn=\\23foo+sn=bar')
})
t.test('recognizes encoded string values', async t => {
const rdn = new RDN({
cn: '#foo',
'1.3.6.1.4.1.1466.0': '#04024869'
})
t.equal(rdn.toString(), 'cn=\\23foo+1.3.6.1.4.1.1466.0=#04024869')
})
t.test('encodes BerReader instances', async t => {
const rdn = new RDN({
cn: new BerReader(Buffer.from([0x04, 0x03, 0x66, 0x6f, 0x6f]))
})
t.equal(rdn.toString(), 'cn=#0403666f6f')
})
t.test('honors unescaped options', async t => {
const rdn = new RDN({
ou: '研发二组'
})
t.equal(rdn.toString({ unescaped: true }), 'ou=研发二组')
})
t.end()
})
tap.test('deprecations', t => {
t.test('format', t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_DN_DEP_002', false)
})
const rdn = new RDN({ cn: 'foo' })
t.equal(rdn.format(), 'cn=foo')
function handler (error) {
t.equal(error.message, '.format() is deprecated. Use .toString() instead')
t.end()
}
})
t.test('set', t => {
process.on('warning', handler)
t.teardown(async () => {
process.removeListener('warning', handler)
warning.emitted.set('LDAP_DN_DEP_002', false)
})
const rdn = new RDN()
rdn.set('cn', 'foo', { value: 'ignored' })
function handler (error) {
t.equal(error.message, '.set() is deprecated. Use .setAttribute() instead')
t.end()
}
})
t.end()
})
tap.test('#isRdn', t => {
t.test('true for instance', async t => {
const rdn = new RDN()
t.equal(RDN.isRdn(rdn), true)
})
t.test('false for non-object', async t => {
t.equal(RDN.isRdn(42), false)
})
t.test('false for bad object', async t => {
const input = { bad: 'rdn', 'non-string-value': 42 }
t.equal(RDN.isRdn(input), false)
})
t.test('true for rdn-like with name+value keys', async t => {
const input = { name: 'cn', value: 'foo' }
t.equal(RDN.isRdn(input), true)
})
t.test('true for pojo representation', async t => {
const input = { cn: 'foo', sn: 'bar' }
t.equal(RDN.isRdn(input), true)
})
t.test('true for pojo with BerReader', async t => {
const input = {
foo: new BerReader(Buffer.from([0x04, 0x03, 0x66, 0x6f, 0x6f]))
}
t.equal(RDN.isRdn(input), true)
})
t.end()
})

104
node_modules/@ldapjs/dn/lib/utils/escape-value.js generated vendored Normal file
View File

@ -0,0 +1,104 @@
'use strict'
/**
* Converts an attribute value into an escaped string as described in
* https://www.rfc-editor.org/rfc/rfc4514#section-2.4.
*
* This function supports up to 4 byte unicode characters.
*
* @param {string} value
* @returns {string} The escaped string.
*/
module.exports = function escapeValue (value) {
if (typeof value !== 'string') {
throw Error('value must be a string')
}
const toEscape = Buffer.from(value, 'utf8')
const escaped = []
// We will handle the reverse solidus ('\') on its own.
const embeddedReservedChars = [
0x22, // '"'
0x2b, // '+'
0x2c, // ','
0x3b, // ';'
0x3c, // '<'
0x3e // '>'
]
for (let i = 0; i < toEscape.byteLength;) {
const charHex = toEscape[i]
// Handle leading space or #.
if (i === 0 && (charHex === 0x20 || charHex === 0x23)) {
escaped.push(toEscapedHexString(charHex))
i += 1
continue
}
// Handle trailing space.
if (i === toEscape.byteLength - 1 && charHex === 0x20) {
escaped.push(toEscapedHexString(charHex))
i += 1
continue
}
if (embeddedReservedChars.includes(charHex) === true) {
escaped.push(toEscapedHexString(charHex))
i += 1
continue
}
if (charHex >= 0xc0 && charHex <= 0xdf) {
// Represents the first byte in a 2-byte UTF-8 character.
escaped.push(toEscapedHexString(charHex))
escaped.push(toEscapedHexString(toEscape[i + 1]))
i += 2
continue
}
if (charHex >= 0xe0 && charHex <= 0xef) {
// Represents the first byte in a 3-byte UTF-8 character.
escaped.push(toEscapedHexString(charHex))
escaped.push(toEscapedHexString(toEscape[i + 1]))
escaped.push(toEscapedHexString(toEscape[i + 2]))
i += 3
continue
}
if (charHex >= 0xf0 && charHex <= 0xf7) {
// Represents the first byte in a 4-byte UTF-8 character.
escaped.push(toEscapedHexString(charHex))
escaped.push(toEscapedHexString(toEscape[i + 1]))
escaped.push(toEscapedHexString(toEscape[i + 2]))
escaped.push(toEscapedHexString(toEscape[i + 3]))
i += 4
continue
}
if (charHex <= 31) {
// Represents an ASCII control character.
escaped.push(toEscapedHexString(charHex))
i += 1
continue
}
escaped.push(String.fromCharCode(charHex))
i += 1
continue
}
return escaped.join('')
}
/**
* Given a byte, convert it to an escaped hex string.
*
* @example
* toEscapedHexString(0x20) // '\20'
*
* @param {number} char
* @returns {string}
*/
function toEscapedHexString (char) {
return '\\' + char.toString(16).padStart(2, '0')
}

62
node_modules/@ldapjs/dn/lib/utils/escape-value.test.js generated vendored Normal file
View File

@ -0,0 +1,62 @@
'use strict'
const tap = require('tap')
const escapeValue = require('./escape-value')
tap.test('throws for bad input', async t => {
t.throws(
() => escapeValue(42),
Error('value must be a string')
)
})
tap.test('reserved chars', t => {
t.test('space', async t => {
const input = ' has a leading and trailing space '
const expected = '\\20has a leading and trailing space\\20'
const result = escapeValue(input)
t.equal(result, expected)
})
t.test('leading #', async t => {
t.equal(escapeValue('#hashtag'), '\\23hashtag')
})
t.test('pompous name', async t => {
t.equal(
escapeValue('James "Jim" Smith, III'),
'James \\22Jim\\22 Smith\\2c III'
)
})
t.test('carriage return', async t => {
t.equal(escapeValue('Before\rAfter'), 'Before\\0dAfter')
})
t.end()
})
tap.test('2-byte utf-8', t => {
t.test('Lučić', async t => {
const expected = 'Lu\\c4\\8di\\c4\\87'
t.equal(escapeValue('Lučić'), expected)
})
t.end()
})
tap.test('3-byte utf-8', t => {
t.test('₠', async t => {
t.equal(escapeValue('₠'), '\\e2\\82\\a0')
})
t.end()
})
tap.test('4-byte utf-8', t => {
t.test('😀', async t => {
t.equal(escapeValue('😀'), '\\f0\\9f\\98\\80')
})
t.end()
})

19
node_modules/@ldapjs/dn/lib/utils/is-dotted-decimal.js generated vendored Normal file
View File

@ -0,0 +1,19 @@
'use strict'
const partIsNotNumeric = part => /^\d+$/.test(part) === false
/**
* Determines if a passed in string is a dotted decimal string.
*
* @param {string} value
*
* @returns {boolean}
*/
module.exports = function isDottedDecimal (value) {
if (typeof value !== 'string') return false
const parts = value.split('.')
const nonNumericParts = parts.filter(partIsNotNumeric)
return nonNumericParts.length === 0
}

View File

@ -0,0 +1,24 @@
'use strict'
const tap = require('tap')
const isDottedDecimal = require('./is-dotted-decimal')
tap.test('false for non-string', async t => {
t.equal(isDottedDecimal(), false)
})
tap.test('false for empty string', async t => {
t.equal(isDottedDecimal(''), false)
})
tap.test('false for alpha string', async t => {
t.equal(isDottedDecimal('foo'), false)
})
tap.test('false for alpha-num string', async t => {
t.equal(isDottedDecimal('foo.123'), false)
})
tap.test('true for valid string', async t => {
t.equal(isDottedDecimal('1.2.3'), true)
})

View File

@ -0,0 +1,58 @@
'use strict'
/**
* Find the ending position of the attribute type name portion of an RDN.
* This function does not verify if the name is a valid description string
* or numeric OID. It merely reads a string from the given starting position
* to the spec defined end of an attribute type string.
*
* @param {Buffer} searchBuffer A buffer representing the RDN.
* @param {number} startPos The position in the `searchBuffer` to start
* searching from.
*
* @returns {number} The position of the end of the RDN's attribute type name,
* or `-1` if an invalid character has been encountered.
*/
module.exports = function findNameEnd ({ searchBuffer, startPos }) {
let pos = startPos
while (pos < searchBuffer.byteLength) {
const char = searchBuffer[pos]
if (char === 0x20 || char === 0x3d) {
// Name ends with a space or an '=' character.
break
}
if (isValidNameChar(char) === true) {
pos += 1
continue
}
return -1
}
return pos
}
/**
* Determine if a character is a valid `attributeType` character as defined
* in RFC 4514 §3.
*
* @param {number} c The character to verify. Should be the byte representation
* of the character from a {@link Buffer} instance.
*
* @returns {boolean}
*/
function isValidNameChar (c) {
if (c >= 0x41 && c <= 0x5a) { // A - Z
return true
}
if (c >= 0x61 && c <= 0x7a) { // a - z
return true
}
if (c >= 0x30 && c <= 0x39) { // 0 - 9
return true
}
if (c === 0x2d || c === 0x2e) { // - or .
return true
}
return false
}

View File

@ -0,0 +1,28 @@
'use strict'
const tap = require('tap')
const findNameEnd = require('./find-name-end')
tap.test('stops on a space', async t => {
const input = Buffer.from('foo = bar')
const pos = findNameEnd({ searchBuffer: input, startPos: 0 })
t.equal(pos, 3)
})
tap.test('stops on an equals', async t => {
const input = Buffer.from('foo=bar')
const pos = findNameEnd({ searchBuffer: input, startPos: 0 })
t.equal(pos, 3)
})
tap.test('returns -1 for bad character', async t => {
const input = Buffer.from('føø=bar')
const pos = findNameEnd({ searchBuffer: input, startPos: 0 })
t.equal(pos, -1)
})
tap.test('recognizes all valid characters', async t => {
const input = Buffer.from('Foo.0-bar=baz')
const pos = findNameEnd({ searchBuffer: input, startPos: 0 })
t.equal(pos, 9)
})

View File

@ -0,0 +1,34 @@
'use strict'
// Attribute types must start with an ASCII alphanum character.
// https://www.rfc-editor.org/rfc/rfc4514#section-3
// https://www.rfc-editor.org/rfc/rfc4512#section-1.4
const isLeadChar = (c) => /[a-zA-Z0-9]/.test(c) === true
/**
* Find the starting position of an attribute type (name). Leading spaces and
* commas are ignored. If an invalid leading character is encountered, an
* invalid position will be returned.
*
* @param {Buffer} searchBuffer
* @param {number} startPos
*
* @returns {number} The position in the buffer where the name starts, or `-1`
* if an invalid name starting character is encountered.
*/
module.exports = function findNameStart ({ searchBuffer, startPos }) {
let pos = startPos
while (pos < searchBuffer.byteLength) {
if (searchBuffer[pos] === 0x20 || searchBuffer[pos] === 0x2c) {
// Skip leading space and comma.
pos += 1
continue
}
const char = String.fromCharCode(searchBuffer[pos])
if (isLeadChar(char) === true) {
return pos
}
break
}
return -1
}

View File

@ -0,0 +1,28 @@
'use strict'
const tap = require('tap')
const findNameStart = require('./find-name-start')
tap.test('returns correct position', async t => {
const input = Buffer.from(' foo')
t.equal(
findNameStart({ searchBuffer: input, startPos: 0 }),
3
)
})
tap.test('skips leading comma', async t => {
const input = Buffer.from(' , foo=bar')
t.equal(
findNameStart({ searchBuffer: input, startPos: 0 }),
3
)
})
tap.test('returns -1 for invalid lead char', async t => {
const input = Buffer.from(' øfoo')
t.equal(
findNameStart({ searchBuffer: input, startPos: 0 }),
-1
)
})

105
node_modules/@ldapjs/dn/lib/utils/parse-string/index.js generated vendored Normal file
View File

@ -0,0 +1,105 @@
'use strict'
const readAttributePair = require('./read-attribute-pair')
/**
* @typedef {object} ParsedPojoRdn
* @property {string} name Either the name of an RDN attribute, or the
* equivalent numeric OID.
* @property {string | import('@ldapjs/asn1').BerReader} value The attribute
* value as a plain string, or a `BerReader` if the string value was an encoded
* hex string.
*/
/**
* Parse a string into a set of plain JavaScript object representations of
* RDNs.
*
* @example A plain string with multiple RDNs and multiple attribute assertions.
* const input = 'cn=foo+sn=bar,dc=example,dc=com
* const result = parseString(input)
* // [
* // { cn: 'foo', sn: 'bar' },
* // { dc: 'example' }
* // { dc: 'com' }
* // ]
*
* @param {string} input The RDN string to parse.
*
* @returns {ParsedPojoRdn[]}
*
* @throws When there is some problem parsing the RDN string.
*/
module.exports = function parseString (input) {
if (typeof input !== 'string') {
throw Error('input must be a string')
}
if (input.length === 0) {
// Short circuit because the input is an empty DN (i.e. "root DSE").
return []
}
const searchBuffer = Buffer.from(input, 'utf8')
const length = searchBuffer.byteLength
const rdns = []
let pos = 0
let rdn = {}
readRdnLoop:
while (pos <= length) {
if (pos === length) {
const char = searchBuffer[pos - 1]
/* istanbul ignore else */
if (char === 0x2b || char === 0x2c || char === 0x3b) {
throw Error('rdn string ends abruptly with character: ' + String.fromCharCode(char))
}
}
// Find the start of significant characters by skipping over any leading
// whitespace.
while (pos < length && searchBuffer[pos] === 0x20) {
pos += 1
}
const readAttrPairResult = readAttributePair({ searchBuffer, startPos: pos })
pos = readAttrPairResult.endPos
rdn = { ...rdn, ...readAttrPairResult.pair }
if (pos >= length) {
// We've reached the end of the string. So push the current RDN and stop.
rdns.push(rdn)
break
}
// Next, we need to determine if the next set of significant characters
// denotes another attribute pair for the current RDN, or is the indication
// of another RDN.
while (pos < length) {
const char = searchBuffer[pos]
// We don't need to skip whitespace before the separator because the
// attribute pair function has already advanced us past any such
// whitespace.
if (char === 0x2b) { // +
// We need to continue adding attribute pairs to the current RDN.
pos += 1
continue readRdnLoop
}
/* istanbul ignore else */
if (char === 0x2c || char === 0x3b) { // , or ;
// The current RDN has been fully parsed, so push it to the list,
// reset the collector, and start parsing the next RDN.
rdns.push(rdn)
rdn = {}
pos += 1
continue readRdnLoop
}
}
}
return rdns
}

View File

@ -0,0 +1,214 @@
'use strict'
const tap = require('tap')
const { BerReader } = require('@ldapjs/asn1')
const parseString = require('./index')
tap.test('throws for non-string input', async t => {
const input = ['cn=foo']
t.throws(
() => parseString(input),
'input must be a string'
)
})
tap.test('short circuits for root dse', async t => {
t.same(parseString(''), [])
})
tap.test('parses basic single rdn', async t => {
const input = 'cn=foo'
const result = parseString(input)
t.same(result, [{ cn: 'foo' }])
})
tap.test('skips leading whitespace', async t => {
const input = ' cn=foo'
const result = parseString(input)
t.same(result, [{ cn: 'foo' }])
})
tap.test('parses basic multiple rdns', async t => {
let input = 'dc=example,dc=com'
let result = parseString(input)
t.same(
result,
[
{ dc: 'example' },
{ dc: 'com' }
]
)
// RFC 2253 §4 separator is supported.
input = 'dc=example;dc=com'
result = parseString(input)
t.same(
result,
[
{ dc: 'example' },
{ dc: 'com' }
]
)
})
tap.test('handles multivalued rdn', async t => {
const input = 'foo=bar+baz=bif'
const result = parseString(input)
t.same(result, [{ foo: 'bar', baz: 'bif' }])
})
tap.test('abruptly ending strings throw', async t => {
const baseError = 'rdn string ends abruptly with character: '
const tests = [
{ input: 'foo=bar+', expected: baseError + '+' },
{ input: 'foo=bar,', expected: baseError + ',' },
{ input: 'foo=bar;', expected: baseError + ';' }
]
for (const test of tests) {
t.throws(() => parseString(test.input), test.expected)
}
})
tap.test('adds rdn with trailing whitespace', async t => {
const input = 'foo=bar '
const result = parseString(input)
t.same(result, [{ foo: 'bar' }])
})
tap.test('parses rdn with attribute name in OID form', async t => {
const input = '0.9.2342.19200300.100.1.25=Example'
const result = parseString(input)
t.same(result, [{ '0.9.2342.19200300.100.1.25': 'Example' }])
})
tap.test('throws for invalid attribute type name', async t => {
let input = '3foo=bar'
t.throws(
() => parseString(input),
'invalid attribute type name: 3foo'
)
input = '1.2.3.abc=bar'
t.throws(
() => parseString(input),
'invalid attribute type name: 1.2.3.abc=bar'
)
input = 'føø=bar'
t.throws(
() => parseString(input),
'invalid attribute type name: føø'
)
})
tap.test('throws for abrupt end', async t => {
const input = 'foo=bar,'
t.throws(
() => parseString(input),
'rdn string ends abruptly with character: ,'
)
})
tap.test('rfc 4514 §4 examples', async t => {
const tests = [
{
input: 'UID=jsmith,DC=example,DC=net',
expected: [{ UID: 'jsmith' }, { DC: 'example' }, { DC: 'net' }]
},
{
input: 'OU=Sales+CN=J. Smith,DC=example,DC=net',
expected: [
{ OU: 'Sales', CN: 'J. Smith' },
{ DC: 'example' },
{ DC: 'net' }
]
},
{
input: 'CN=James \\"Jim\\" Smith\\, III,DC=example,DC=net',
expected: [{ CN: 'James "Jim" Smith, III' }, { DC: 'example' }, { DC: 'net' }]
},
{
input: 'CN=Before\\0dAfter,DC=example,DC=net',
expected: [{ CN: 'Before\rAfter' }, { DC: 'example' }, { DC: 'net' }]
},
{
checkBuffer: true,
input: '1.3.6.1.4.1.1466.0=#04024869',
expected: [{ '1.3.6.1.4.1.1466.0': new BerReader(Buffer.from([0x04, 0x02, 0x48, 0x69])) }]
},
{
input: 'CN=Lu\\C4\\8Di\\C4\\87',
expected: [{ CN: 'Lučić' }]
}
]
for (const test of tests) {
const result = parseString(test.input)
if (test.checkBuffer) {
for (const [i, rdn] of test.expected.entries()) {
for (const key of Object.keys(rdn)) {
t.equal(
rdn[key].buffer.compare(result[i][key].buffer),
0
)
}
}
} else {
t.same(result, test.expected)
}
}
})
tap.test('rfc 2253 §5 examples', async t => {
const tests = [
{
input: 'CN=Steve Kille,O=Isode Limited,C=GB',
expected: [{ CN: 'Steve Kille' }, { O: 'Isode Limited' }, { C: 'GB' }]
},
{
input: 'OU=Sales+CN=J. Smith,O=Widget Inc.,C=US',
expected: [{ OU: 'Sales', CN: 'J. Smith' }, { O: 'Widget Inc.' }, { C: 'US' }]
},
{
input: 'CN=L. Eagle,O=Sue\\, Grabbit and Runn,C=GB',
expected: [{ CN: 'L. Eagle' }, { O: 'Sue, Grabbit and Runn' }, { C: 'GB' }]
},
{
input: 'CN=Before\\0DAfter,O=Test,C=GB',
expected: [{ CN: 'Before\rAfter' }, { O: 'Test' }, { C: 'GB' }]
},
{
checkBuffer: true,
input: '1.3.6.1.4.1.1466.0=#04024869,O=Test,C=GB',
expected: [
{ '1.3.6.1.4.1.1466.0': new BerReader(Buffer.from([0x04, 0x02, 0x48, 0x69])) },
{ O: 'Test' },
{ C: 'GB' }
]
},
{
input: 'SN=Lu\\C4\\8Di\\C4\\87',
expected: [{ SN: 'Lučić' }]
}
]
for (const test of tests) {
const result = parseString(test.input)
if (test.checkBuffer) {
for (const [i, rdn] of test.expected.entries()) {
for (const key of Object.keys(rdn)) {
if (typeof rdn[key] !== 'string') {
t.equal(
rdn[key].buffer.compare(result[i][key].buffer),
0
)
} else {
t.equal(rdn[key], result[i][key])
}
}
}
} else {
t.same(result, test.expected)
}
}
})

View File

@ -0,0 +1,28 @@
'use strict'
const isDigit = c => /[0-9]/.test(c) === true
const hasKeyChars = input => /[a-zA-Z-]/.test(input) === true
const isValidLeadChar = c => /[a-zA-Z]/.test(c) === true
const hasInvalidChars = input => /[^a-zA-Z0-9-]/.test(input) === true
/**
* An attribute type name is defined by RFC 4514 §3 as a "descr" or
* "numericoid". These are defined by RFC 4512 §1.4. This function validates
* the given name as matching the spec.
*
* @param {string} name
*
* @returns {boolean}
*/
module.exports = function isValidAttributeTypeName (name) {
if (isDigit(name[0]) === true) {
// A leading digit indicates that the name should be a numericoid.
return hasKeyChars(name) === false
}
if (isValidLeadChar(name[0]) === false) {
return false
}
return hasInvalidChars(name) === false
}

View File

@ -0,0 +1,36 @@
'use strict'
const tap = require('tap')
const isValidAttributeTypeName = require('./is-valid-attribute-type-name')
tap.test('validates numericoids', async t => {
let input = '1.2.3.4'
let result = isValidAttributeTypeName(input)
t.equal(result, true)
input = '1.2.3.4.abc'
result = isValidAttributeTypeName(input)
t.equal(result, false)
})
tap.test('validates descrs', async t => {
let input = 'foo'
let result = isValidAttributeTypeName(input)
t.equal(result, true)
input = '3foo'
result = isValidAttributeTypeName(input)
t.equal(result, false)
input = 'foo-3'
result = isValidAttributeTypeName(input)
t.equal(result, true)
input = 'føø3'
result = isValidAttributeTypeName(input)
t.equal(result, false)
input = 'ƒ00'
result = isValidAttributeTypeName(input)
t.equal(result, false)
})

View File

@ -0,0 +1,73 @@
'use strict'
const findNameStart = require('./find-name-start')
const findNameEnd = require('./find-name-end')
const isValidAttributeTypeName = require('./is-valid-attribute-type-name')
const readAttributeValue = require('./read-attribute-value')
/**
* @typedef {object} AttributePair
* @property {string | import('@ldapjs/asn1').BerReader} name Property name is
* actually the property name of the attribute pair. The value will be a string,
* or, in the case of the value being a hex encoded string, an instance of
* `BerReader`.
*
* @example
* const input = 'foo=bar'
* const pair = { foo: 'bar' }
*/
/**
* @typedef {object} ReadAttributePairResult
* @property {number} endPos The ending position in the input search buffer that
* is the end of the read attribute pair.
* @property {AttributePair} pair The parsed attribute pair.
*/
/**
* Read an RDN attribute type and attribute value pair from the provided
* search buffer at the given starting position.
*
* @param {Buffer} searchBuffer
* @param {number} startPos
*
* @returns {ReadAttributePairResult}
*
* @throws When there is some problem with the input string.
*/
module.exports = function readAttributePair ({ searchBuffer, startPos }) {
let pos = startPos
const nameStartPos = findNameStart({
searchBuffer,
startPos: pos
})
if (nameStartPos < 0) {
throw Error('invalid attribute name leading character encountered')
}
const nameEndPos = findNameEnd({
searchBuffer,
startPos: nameStartPos
})
if (nameStartPos < 0) {
throw Error('invalid character in attribute name encountered')
}
const attributeName = searchBuffer.subarray(nameStartPos, nameEndPos).toString('utf8')
if (isValidAttributeTypeName(attributeName) === false) {
throw Error('invalid attribute type name: ' + attributeName)
}
const valueReadResult = readAttributeValue({
searchBuffer,
startPos: nameEndPos
})
pos = valueReadResult.endPos
const attributeValue = valueReadResult.value
return {
endPos: pos,
pair: { [attributeName]: attributeValue }
}
}

View File

@ -0,0 +1,165 @@
'use strict'
const readHexString = require('./read-hex-string')
const readEscapeSequence = require('./read-escape-sequence')
/**
* @typedef {object} ReadAttributeValueResult
* @property {number} endPos The position in the buffer that marks the end of
* the value.
* @property {string | import('@ldapjs/asn1').BerReader} value
*/
/**
* Read an attribute value string from a given {@link Buffer} and return it.
* If the value is an encoded octet string, it will be decoded and returned
* as a {@link Buffer}.
*
* @param {Buffer} searchBuffer
* @param {number} startPos
*
* @returns {ReadAttributeValueResult}
*
* @throws When there is a syntax error in the attribute value string.
*/
module.exports = function readAttributeValue ({ searchBuffer, startPos }) {
let pos = startPos
while (pos < searchBuffer.byteLength && searchBuffer[pos] === 0x20) {
// Skip over any leading whitespace before the '='.
pos += 1
}
if (pos >= searchBuffer.byteLength || searchBuffer[pos] !== 0x3d) {
throw Error('attribute value does not start with equals sign')
}
// Advance past the equals sign.
pos += 1
while (pos <= searchBuffer.byteLength && searchBuffer[pos] === 0x20) {
// Advance past any leading whitespace.
pos += 1
}
if (pos >= searchBuffer.byteLength) {
return { endPos: pos, value: '' }
}
if (searchBuffer[pos] === 0x23) {
const result = readHexString({ searchBuffer, startPos: pos + 1 })
pos = result.endPos
return { endPos: pos, value: result.berReader }
}
const readValueResult = readValueString({ searchBuffer, startPos: pos })
pos = readValueResult.endPos
return {
endPos: pos,
value: readValueResult.value.toString('utf8').trim()
}
}
/**
* @typedef {object} ReadValueStringResult
* @property {number} endPos
* @property {Buffer} value
* @private
*/
/**
* Read a series of bytes from the buffer as a plain string.
*
* @param {Buffer} searchBuffer
* @param {number} startPos
*
* @returns {ReadValueStringResult}
*
* @throws When the attribute value is malformed.
*
* @private
*/
function readValueString ({ searchBuffer, startPos }) {
let pos = startPos
let inQuotes = false
let endQuotePresent = false
const bytes = []
while (pos <= searchBuffer.byteLength) {
const char = searchBuffer[pos]
if (pos === searchBuffer.byteLength) {
if (inQuotes === true && endQuotePresent === false) {
throw Error('missing ending double quote for attribute value')
}
break
}
if (char === 0x22) {
// Handle the double quote (") character.
// RFC 2253 §4 allows for attribute values to be wrapped in double
// quotes in order to allow certain characters to be unescaped.
// We are not enforcing escaping of characters in this parser, so we only
// need to recognize that the quotes are present. Our RDN string encoder
// will escape characters as necessary.
if (inQuotes === true) {
pos += 1
endQuotePresent = true
// We should be at the end of the value.
while (pos < searchBuffer.byteLength) {
const nextChar = searchBuffer[pos]
if (isEndChar(nextChar) === true) {
break
}
if (nextChar !== 0x20) {
throw Error('significant rdn character found outside of quotes at position ' + pos)
}
pos += 1
}
break
}
if (pos !== startPos) {
throw Error('unexpected quote (") in rdn string at position ' + pos)
}
inQuotes = true
pos += 1
continue
}
if (isEndChar(char) === true && inQuotes === false) {
break
}
if (char === 0x5c) {
// We have encountered the start of an escape sequence.
const seqResult = readEscapeSequence({
searchBuffer,
startPos: pos
})
pos = seqResult.endPos
Array.prototype.push.apply(bytes, seqResult.parsed)
continue
}
bytes.push(char)
pos += 1
}
return {
endPos: pos,
value: Buffer.from(bytes)
}
}
function isEndChar (c) {
switch (c) {
case 0x2b: // +
case 0x2c: // ,
case 0x3b: // ; -- Allowed by RFC 2253 §4 in place of a comma.
return true
default:
return false
}
}

View File

@ -0,0 +1,119 @@
'use strict'
const tap = require('tap')
const readAttributeValue = require('./read-attribute-value')
const { BerReader } = require('@ldapjs/asn1')
const missingError = Error('attribute value does not start with equals sign')
tap.test('throws if missing equals sign', async t => {
let input = Buffer.from('foo')
t.throws(
() => readAttributeValue({ searchBuffer: input, startPos: 3 }),
missingError
)
input = Buffer.from('foo ≠')
t.throws(
() => readAttributeValue({ searchBuffer: input, startPos: 3 }),
missingError
)
})
tap.test('handles empty attribute value', async t => {
const input = Buffer.from('foo=')
const result = readAttributeValue({ searchBuffer: input, startPos: 3 })
t.same(result, { endPos: 4, value: '' })
})
tap.test('returns attribute value', async t => {
const input = Buffer.from('foo=bar')
const result = readAttributeValue({ searchBuffer: input, startPos: 3 })
t.same(result, { endPos: 7, value: 'bar' })
})
tap.test('quoted values', t => {
t.test('throws if quote is unexpected', async t => {
const input = Buffer.from('=foo"bar')
t.throws(
() => readAttributeValue({ searchBuffer: input, startPos: 0 }),
'unexpected quote (") in rdn string at position 4'
)
})
t.test('handles a basic quoted value', async t => {
const input = Buffer.from('="bar"')
const result = readAttributeValue({ searchBuffer: input, startPos: 0 })
t.same(result, { endPos: 6, value: 'bar' })
})
t.test('handles quote followed by end char', async t => {
const input = Buffer.from('="bar",another=rdn')
const result = readAttributeValue({ searchBuffer: input, startPos: 0 })
t.same(result, { endPos: 6, value: 'bar' })
})
t.test('significant spaces in quoted values are part of the value', async t => {
const input = Buffer.from('="foo bar "')
const result = readAttributeValue({ searchBuffer: input, startPos: 0 })
t.same(result, { endPos: 13, value: 'foo bar' })
})
t.test('throws if next significant char is not an end char', async t => {
const input = Buffer.from('="foo bar" baz')
t.throws(
() => readAttributeValue({ searchBuffer: input, startPos: 0 }),
'significant rdn character found outside of quotes at position 7'
)
})
t.test('throws if ending quote not found', async t => {
const input = Buffer.from('="foo')
t.throws(
() => readAttributeValue({ searchBuffer: input, startPos: 0 }),
'missing ending double quote for attribute value'
)
})
t.end()
})
tap.test('leading and trailing spaces are omitted', async t => {
const input = Buffer.from('= foo ')
const result = readAttributeValue({ searchBuffer: input, startPos: 0 })
t.same(result, { endPos: 9, value: 'foo' })
})
tap.test('parses escaped attribute values', async t => {
const input = Buffer.from('foo=foo\\#bar')
const result = readAttributeValue({ searchBuffer: input, startPos: 3 })
t.same(result, { endPos: 12, value: 'foo#bar' })
})
tap.test('stops reading at all ending characters', async t => {
const tests = [
{ input: '=foo,bar', expected: { endPos: 4, value: 'foo' } },
{ input: '=foo+bar', expected: { endPos: 4, value: 'foo' } },
{ input: '=foo;bar', expected: { endPos: 4, value: 'foo' } }
]
for (const test of tests) {
const result = readAttributeValue({
searchBuffer: Buffer.from(test.input),
startPos: 0
})
t.same(result, test.expected)
}
})
tap.test('reads hex encoded string', async t => {
const input = Buffer.from('=#0403666f6f')
const result = readAttributeValue({ searchBuffer: input, startPos: 0 })
const expected = {
endPos: 12,
value: new BerReader(Buffer.from([0x04, 0x03, 0x66, 0x6f, 0x6f]))
}
t.same(result, expected)
t.equal(result.value.buffer.compare(expected.value.buffer), 0)
})

View File

@ -0,0 +1,137 @@
'use strict'
/**
* @typedef ReadEscapeSequenceResult
* @property {number} endPos The position in the buffer that marks the end of
* the escape sequence.
* @property {Buffer} parsed The parsed escape sequence as a buffer of bytes.
*/
/**
* Read an escape sequence from a buffer. It reads until no escape sequences
* are found. Thus, a sequence of escape sequences will all be parsed at once
* and returned as a single result.
*
* @example A Single ASCII Sequence
* const toParse = Buffer.from('foo\\#bar', 'utf8')
* const {parsed, endPos} = readEscapeSequence({
* searchBuffer: toParse,
* startPos: 3
* })
* // => parsed = '#', endPos = 5
*
* @example Multiple ASCII Sequences In Succession
* const toParse = Buffer.from('foo\\#\\!bar', 'utf8')
* const {parsed, endPos} = readEscapeSequence({
* searchBuffer: toParse,
* startPos: 3
* })
* // => parsed = '#!', endPos = 7
*
* @param searchBuffer
* @param startPos
*
* @returns {ReadEscapeSequenceResult}
*
* @throws When an escaped sequence is not a valid hexadecimal value.
*/
module.exports = function readEscapeSequence ({ searchBuffer, startPos }) {
// This is very similar to the `readEscapedCharacters` algorithm in
// the `utils/escape-filter-value` in `@ldapjs/filter`. The difference being
// that here we want to interpret the escape sequence instead of return it
// as a string to be embedded in an "escaped" string.
// https://github.com/ldapjs/filter/blob/1423612/lib/utils/escape-filter-value.js
let pos = startPos
const buf = []
while (pos < searchBuffer.byteLength) {
const char = searchBuffer[pos]
const nextChar = searchBuffer[pos + 1]
if (char !== 0x5c) {
// End of sequence reached.
break
}
const strHexCode = String.fromCharCode(nextChar) +
String.fromCharCode(searchBuffer[pos + 2])
const hexCode = parseInt(strHexCode, 16)
if (Number.isNaN(hexCode) === true) {
if (nextChar >= 0x00 && nextChar <= 0x7f) {
// Sequence is a single escaped ASCII character
buf.push(nextChar)
pos += 2
continue
} else {
throw Error('invalid hex code in escape sequence')
}
}
if (hexCode >= 0xc0 && hexCode <= 0xdf) {
// Sequence is a 2-byte utf-8 character.
const secondByte = parseInt(
String.fromCharCode(searchBuffer[pos + 4]) +
String.fromCharCode(searchBuffer[pos + 5]),
16
)
buf.push(hexCode)
buf.push(secondByte)
pos += 6
continue
}
if (hexCode >= 0xe0 && hexCode <= 0xef) {
// Sequence is a 3-byte utf-8 character.
const secondByte = parseInt(
String.fromCharCode(searchBuffer[pos + 4]) +
String.fromCharCode(searchBuffer[pos + 5]),
16
)
const thirdByte = parseInt(
String.fromCharCode(searchBuffer[pos + 7]) +
String.fromCharCode(searchBuffer[pos + 8]),
16
)
buf.push(hexCode)
buf.push(secondByte)
buf.push(thirdByte)
pos += 9
continue
}
if (hexCode >= 0xf0 && hexCode <= 0xf7) {
// Sequence is a 4-byte utf-8 character.
const secondByte = parseInt(
String.fromCharCode(searchBuffer[pos + 4]) +
String.fromCharCode(searchBuffer[pos + 5]),
16
)
const thirdByte = parseInt(
String.fromCharCode(searchBuffer[pos + 7]) +
String.fromCharCode(searchBuffer[pos + 8]),
16
)
const fourthByte = parseInt(
String.fromCharCode(searchBuffer[pos + 10]) +
String.fromCharCode(searchBuffer[pos + 11]),
16
)
buf.push(hexCode)
buf.push(secondByte)
buf.push(thirdByte)
buf.push(fourthByte)
pos += 12
continue
}
// The escaped character should be a single hex value.
buf.push(hexCode)
pos += 3
}
return {
endPos: pos,
parsed: Buffer.from(buf)
}
}

View File

@ -0,0 +1,72 @@
'use strict'
const tap = require('tap')
const readEscapeSequence = require('./read-escape-sequence')
tap.test('throws for bad sequence', async t => {
const input = Buffer.from('foo\\ø')
t.throws(
() => readEscapeSequence({ searchBuffer: input, startPos: 3 }),
Error('invalid hex code in escape sequence')
)
})
tap.test('reads a single ascii sequence', async t => {
const input = Buffer.from('foo\\#bar', 'utf8')
const { parsed, endPos } = readEscapeSequence({
searchBuffer: input,
startPos: 3
})
t.equal(parsed.toString(), '#')
t.equal(endPos, 5)
})
tap.test('reads a sequence of ascii sequences', async t => {
const input = Buffer.from('foo\\#\\!bar', 'utf8')
const { parsed, endPos } = readEscapeSequence({
searchBuffer: input,
startPos: 3
})
t.equal(parsed.toString(), '#!')
t.equal(endPos, 7)
})
tap.test('reads a single hex sequence', async t => {
const input = Buffer.from('foo\\2abar', 'utf8')
const { parsed, endPos } = readEscapeSequence({
searchBuffer: input,
startPos: 3
})
t.equal(parsed.toString(), '*')
t.equal(endPos, 6)
})
tap.test('reads 2-byte utf-8 sequence', async t => {
const input = Buffer.from('fo\\c5\\8f bar')
const { parsed, endPos } = readEscapeSequence({
searchBuffer: input,
startPos: 2
})
t.equal(parsed.toString(), 'ŏ')
t.equal(endPos, 8)
})
tap.test('reads 3-byte utf-8 sequence', async t => {
const input = Buffer.from('fo\\e0\\b0\\b0 bar')
const { parsed, endPos } = readEscapeSequence({
searchBuffer: input,
startPos: 2
})
t.equal(parsed.toString(), 'ర')
t.equal(endPos, 11)
})
tap.test('reads 4-byte utf-8 sequence', async t => {
const input = Buffer.from('fo\\f0\\92\\84\\ad bar')
const { parsed, endPos } = readEscapeSequence({
searchBuffer: input,
startPos: 2
})
t.equal(parsed.toString(), '𒄭')
t.equal(endPos, 14)
})

View File

@ -0,0 +1,61 @@
'use strict'
const { BerReader } = require('@ldapjs/asn1')
const isValidHexCode = code => /[0-9a-fA-F]{2}/.test(code) === true
/**
* @typedef {object} ReadHexStringResult
* @property {number} endPos The position in the buffer where the end of the
* hex string was encountered.
* @property {import('@ldapjs/asn1').BerReader} berReader The parsed hex string
* as an BER object.
*/
/**
* Read a sequence of bytes as a hex encoded octet string. The sequence is
* assumed to be a spec compliant encoded BER object.
*
* @param {Buffer} searchBuffer The buffer to read.
* @param {number} startPos The position in the buffer to start reading from.
*
* @returns {ReadHexStringResult}
*
* @throws When an invalid hex pair has been encountered.
*/
module.exports = function readHexString ({ searchBuffer, startPos }) {
const bytes = []
let pos = startPos
while (pos < searchBuffer.byteLength) {
if (isEndChar(searchBuffer[pos])) {
break
}
const hexPair = String.fromCharCode(searchBuffer[pos]) +
String.fromCharCode(searchBuffer[pos + 1])
if (isValidHexCode(hexPair) === false) {
throw Error('invalid hex pair encountered: 0x' + hexPair)
}
bytes.push(parseInt(hexPair, 16))
pos += 2
}
return {
endPos: pos,
berReader: new BerReader(Buffer.from(bytes))
}
}
function isEndChar (c) {
switch (c) {
case 0x20: // space
case 0x2b: // +
case 0x2c: // ,
case 0x3b: // ;
return true
default:
return false
}
}

View File

@ -0,0 +1,52 @@
'use strict'
const tap = require('tap')
const readHexString = require('./read-hex-string')
tap.test('throws for invalid hex pair', async t => {
let input = Buffer.from('1z2f')
t.throws(
() => readHexString({ searchBuffer: input, startPos: 0 }),
'invalid hex pair encountered: 0x1z'
)
input = Buffer.from('a0b1g692')
t.throws(
() => readHexString({ searchBuffer: input, startPos: 0 }),
'invalid hex pair encountered: 0xg6'
)
})
tap.test('handles incorrect length string', async t => {
const input = Buffer.from('a1b')
t.throws(
() => readHexString({ searchBuffer: input, startPos: 0 }),
'invalid hex pair encountered: 0xb'
)
})
tap.test('reads hex string', async t => {
let input = Buffer.from('0403666f6f')
let result = readHexString({ searchBuffer: input, startPos: 0 })
t.equal(result.endPos, 10)
t.equal(result.berReader.readString(), 'foo')
input = Buffer.from('uid=#0409746573742E75736572')
result = readHexString({ searchBuffer: input, startPos: 5 })
t.equal(result.endPos, input.byteLength)
t.equal(result.berReader.readString(), 'test.user')
})
tap.test('stops on end chars', async t => {
const inputs = [
Buffer.from('0403666f6f foo'),
Buffer.from('0403666f6f+foo'),
Buffer.from('0403666f6f,foo'),
Buffer.from('0403666f6f;foo')
]
for (const input of inputs) {
const result = readHexString({ searchBuffer: input, startPos: 0 })
t.equal(result.endPos, 10)
t.equal(result.berReader.readString(), 'foo')
}
})

43
node_modules/@ldapjs/dn/package.json generated vendored Normal file
View File

@ -0,0 +1,43 @@
{
"originalAuthor": "Patrick Mooney",
"originalContributors": [
"Mark Cavage <mcavage@gmail.com>",
"Cody Peter Mello <cody.mello@joyent.com>"
],
"name": "@ldapjs/dn",
"homepage": "https://github.com/ldapjs/dn",
"description": "API for handling LDAP distinguished name strings",
"version": "1.1.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "git@github.com:ldapjs/dn.git"
},
"main": "index.js",
"dependencies": {
"@ldapjs/asn1": "2.0.0",
"process-warning": "^2.1.0"
},
"devDependencies": {
"@fastify/pre-commit": "^2.0.2",
"eslint": "^8.34.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^15.6.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.1.1",
"tap": "^16.3.4"
},
"scripts": {
"lint": "eslint .",
"lint:ci": "eslint .",
"test": "tap --no-coverage-report",
"test:cov": "tap",
"test:cov:html": "tap --coverage-report=html",
"test:watch": "tap -w --no-coverage-report"
},
"precommit": [
"lint",
"test"
]
}