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

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')
}
})