Skip to content
This repository has been archived by the owner on May 14, 2024. It is now read-only.

Update substring filter to match spec #5

Merged
merged 5 commits into from
Jun 30, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/ber-parsing/_fixtures/evolution-filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const bytes = Buffer.from([
// "cn"
0x63, 0x6e,
0x30, 0x05, // sequence, 5 bytes
0x80, 0x03, // subinitial string, 3 bytes
0x80, 0x03, // initial string, 3 bytes
// "ogo"
0x6f, 0x67, 0x6f,

Expand Down
2 changes: 1 addition & 1 deletion lib/ber-parsing/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ tap.test('parses SubstringFilter', async t => {
const f = parse(new BerReader(input))
t.type(f, SubstringFilter)
t.equal(f.attribute, 'foo')
t.equal(f.subInitial, 'bar')
t.equal(f.initial, 'bar')
t.equal(f.toString(), '(foo=bar*)')
})

Expand Down
10 changes: 9 additions & 1 deletion lib/deprecations.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

const warning = require('process-warning')()

warning.create('LdapjsFilterWarning', 'LDAP_FILTER_DEP_001', 'parse is deprecated. Use the parseString function instead.')
const clazz = 'LdapjsFilterWarning'

warning.create(clazz, 'LDAP_FILTER_DEP_001', 'parse is deprecated. Use the parseString function instead.')

warning.create(clazz, 'LDAP_SUBSTRING_FILTER_DEP_001', 'subInitial is deprecated. Use initial instead.')

warning.create(clazz, 'LDAP_SUBSTRING_FILTER_DEP_002', 'subAny is deprecated. Use any instead.')

warning.create(clazz, 'LDAP_SUBSTRING_FILTER_DEP_003', 'subFinal is deprecated. Use final instead.')
jsumners marked this conversation as resolved.
Show resolved Hide resolved

module.exports = warning
8 changes: 4 additions & 4 deletions lib/filters/or.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,13 @@ tap.test('toBer', t => {
// "cn"
0x63, 0x6e,
0x30, 0x05, // sequence, 5 bytes
0x80, 0x03, // string (subinitial tag), 3 bytes
0x80, 0x03, // string (initial tag), 3 bytes
// "foo"
0x66, 0x6f, 0x6f
])
const subFilter = new SubstringFilter({
attribute: 'cn',
subInitial: 'foo'
initial: 'foo'
})
const f = new OrFilter({ filters: [subFilter] })
const ber = f.toBer()
Expand Down Expand Up @@ -164,7 +164,7 @@ tap.test('parse', t => {
// "cn"
0x63, 0x6e,
0x30, 0x05, // sequence, 5 bytes
0x80, 0x03, // string (subinitial tag), 3 bytes
0x80, 0x03, // string (initial tag), 3 bytes
// "foo"
0x66, 0x6f, 0x6f,

Expand All @@ -173,7 +173,7 @@ tap.test('parse', t => {
// "sn"
0x73, 0x6e,
0x30, 0x05, // sequence, 5 bytes
0x80, 0x03, // string (subinitial tag), 3 bytes
0x80, 0x03, // string (initial tag), 3 bytes
0x66, 0x6f, 0x6f
])

Expand Down
139 changes: 97 additions & 42 deletions lib/filters/substring.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,93 +6,148 @@ const { search } = require('@ldapjs/protocol')
const escapeFilterValue = require('../utils/escape-filter-value')
const testValues = require('../utils/test-values')
const getAttributeValue = require('../utils/get-attribute-value')
const warning = require('../deprecations')

/**
* Represents a filter that matches substrings withing LDAP entry attribute
* values, e.g. `(cn=*f*o*o)`.
*/
class SubstringFilter extends FilterString {
#subInitial
#subAny = []
#subFinal
#initial
#any = []
#final

/**
* @typedef {FilterStringParams} SubstringParams
* @property {string} input.attribute The attribute to test against.
* @property {string} [subInitial] Text that must appear at the start
* of a value and may not overlap any value of `subAny` or `subFinal`.
* @property {string[]} [subAny] Text items that must appear in the
* attribute value that do not overlap with `subInitial`, `subFinal`, or
* any other `subAny` item.
* @property {string} [subFinal] Text that must appear at the end of
* the attribute value. May not overlap with `subInitial` or any `subAny`
* @property {string} [initial] Text that must appear at the start
* of a value and may not overlap any value of `any` or `final`.
* @property {string} [subInitial] Deprecated, use `initial`.
* @property {string[]} [any] Text items that must appear in the
* attribute value that do not overlap with `initial`, `final`, or
* any other `any` item.
* @property {string[]} [subAny] Deprecated, use `any`.
* @property {string} [final] Text that must appear at the end of
* the attribute value. May not overlap with `initial` or any `any`
* item.
* @property {string} [subFinal] Deprecated, use `final`.
*/

/**
* @param {SubstringParams} input
*
* @throws When any input parameter is of an incorrect type.
*/
constructor ({ attribute, subInitial, subAny = [], subFinal } = {}) {
constructor ({ attribute, initial, subInitial, any = [], subAny = [], final, subFinal } = {}) {
if (subInitial) {
warning.emit('LDAP_SUBSTRING_FILTER_DEP_001')
initial = subInitial
}

if (Array.isArray(subAny) && subAny.length > 0) {
warning.emit('LDAP_SUBSTRING_FILTER_DEP_002')
any = subAny
}

if (subFinal) {
warning.emit('LDAP_SUBSTRING_FILTER_DEP_003')
final = subFinal
}

if (typeof attribute !== 'string' || attribute.length < 1) {
throw Error('attribute must be a string of at least one character')
}
if (Array.isArray(any) === false) {
throw Error('any must be an array of items')
}
if (Array.isArray(subAny) === false) {
throw Error('subAny must be an array of items')
}
if (subFinal && typeof subFinal !== 'string') {
throw Error('subFinal must be a string')
if (final && typeof final !== 'string') {
throw Error('final must be a string')
}

super({ attribute })

this.#subInitial = subInitial
Array.prototype.push.apply(this.#subAny, subAny)
this.#subFinal = subFinal
this.#initial = initial
Array.prototype.push.apply(this.#any, any)
this.#final = final

Object.defineProperties(this, {
TAG: { value: search.FILTER_SUBSTRINGS },
type: { value: 'SubstringFilter' }
})
}

/**
* @type {string}
*/
get initial () {
return this.#initial
}

/**
* @type {string[]}
*/
get any () {
return this.#any
}

/**
* @type {string}
*/
get final () {
return this.#final
}

/**
* @deprecated 2023-06-29 Use `initial` instead.
* @type {string}
*/
get subInitial () {
return this.#subInitial
return this.#initial
}

/**
* @deprecated 2023-06-29 Use `any` instead.
* @type {string[]}
*/
get subAny () {
return this.#subAny
return this.#any
}

/**
* @deprecated 2023-06-29 Use `final` instead.
* @type {string}
*/
get subFinal () {
return this.#subFinal
return this.#final
}

get json () {
return {
type: this.type,
subInitial: this.#subInitial,
subAny: this.#subAny,
subFinal: this.#subFinal
initial: this.#initial,
any: this.#any,
final: this.#final
}
}
jsumners marked this conversation as resolved.
Show resolved Hide resolved

toString () {
let result = '(' + escapeFilterValue(this.attribute) + '='

if (this.#subInitial) {
result += escapeFilterValue(this.#subInitial)
if (this.#initial) {
result += escapeFilterValue(this.#initial)
}

result += '*'

for (const any of this.#subAny) {
for (const any of this.#any) {
result += escapeFilterValue(any) + '*'
}

if (this.#subFinal) {
result += escapeFilterValue(this.#subFinal)
if (this.#final) {
result += escapeFilterValue(this.#final)
}

result += ')'
Expand All @@ -105,7 +160,7 @@ class SubstringFilter extends FilterString {
* object.
*
* @example
* const filter = new EqualityFilter({ attribute: 'foo', subInitial: 'bar' })
* const filter = new EqualityFilter({ attribute: 'foo', initial: 'bar' })
* assert.equal(filter.matches({ foo: 'bar' }), true)
*
* @param {object} obj An object to check for match.
Expand Down Expand Up @@ -137,11 +192,11 @@ class SubstringFilter extends FilterString {

let re = ''

if (this.#subInitial) { re += '^' + escapeRegExp(this.#subInitial) + '.*' }
this.#subAny.forEach(function (s) {
if (this.#initial) { re += '^' + escapeRegExp(this.#initial) + '.*' }
this.#any.forEach(function (s) {
re += escapeRegExp(s) + '.*'
})
if (this.#subFinal) { re += escapeRegExp(this.#subFinal) + '$' }
if (this.#final) { re += escapeRegExp(this.#final) + '$' }

const matcher = new RegExp(re)
return testValues({
Expand All @@ -156,15 +211,15 @@ class SubstringFilter extends FilterString {
ber.writeString(this.attribute)
ber.startSequence()

if (this.#subInitial) { ber.writeString(this.#subInitial, 0x80) }
if (this.#initial) { ber.writeString(this.#initial, 0x80) }

if (this.#subAny.length > 0) {
for (const sub of this.#subAny) {
if (this.#any.length > 0) {
for (const sub of this.#any) {
ber.writeString(sub, 0x81)
}
}

if (this.#subFinal) { ber.writeString(this.#subFinal, 0x82) }
if (this.#final) { ber.writeString(this.#final, 0x82) }

ber.endSequence()

Expand All @@ -190,9 +245,9 @@ class SubstringFilter extends FilterString {
throw Error(`expected substring filter sequence ${expected}, got ${found}`)
}

let subInitial
const subAny = []
let subFinal
let initial
const any = []
let final

const attribute = reader.readString()
reader.readSequence()
Expand All @@ -204,18 +259,18 @@ class SubstringFilter extends FilterString {
const tag = reader.peek()
switch (tag) {
case 0x80: { // Initial
subInitial = reader.readString(tag)
initial = reader.readString(tag)
break
}

case 0x81: { // Any
const anyVal = reader.readString(tag)
subAny.push(anyVal)
any.push(anyVal)
break
}

case 0x82: { // Final
subFinal = reader.readString(tag)
final = reader.readString(tag)
break
}

Expand All @@ -225,7 +280,7 @@ class SubstringFilter extends FilterString {
}
}

return new SubstringFilter({ attribute, subInitial, subAny, subFinal })
return new SubstringFilter({ attribute, initial, any, final })
}
}

Expand Down
Loading