moray-filter
Version:
API for handling Moray-style filters
319 lines (274 loc) • 7.97 kB
JavaScript
// Copyright 2014 Mark Cavage, Inc. All rights reserved.
// Copyright 2014 Patrick Mooney. All rights reserved.
// Copyright 2016 Joyent, Inc.
var assert = require('assert-plus');
var helpers = require('./helpers.js');
var AndFilter = require('./and_filter');
var ApproximateFilter = require('./approx_filter');
var EqualityFilter = require('./equality_filter');
var ExtensibleFilter = require('./ext_filter');
var GreaterThanEqualsFilter = require('./ge_filter');
var LessThanEqualsFilter = require('./le_filter');
var NotFilter = require('./not_filter');
var OrFilter = require('./or_filter');
var PresenceFilter = require('./presence_filter');
var SubstringFilter = require('./substr_filter');
///--- Globals
/* JSSTYLED */
var attrRegex = /^[-_a-zA-Z0-9]+/;
var hexRegex = /^[a-fA-F0-9]{2}$/;
var morayChars = [ '(', ')', '*', '\\', '=' ];
///--- Internal
function indexOfSkipEscapes(str, start, c)
{
var len = str.length;
for (var cur = start; cur < len; cur++) {
if (str[cur] === '\\') {
cur++;
} else if (str[cur] === c) {
return cur;
}
}
return -1;
}
function escapeValue(str)
{
var cur = 0;
var len = str.length;
var out = '';
while (cur < len) {
var c = str[cur];
switch (c) {
case '(':
/*
* Although '*' characters should be escaped too, we ignore them here in
* case downstream ExtensibleFilter consumers wish to perform their own
* value-add parsing after the fact.
*
* Handling unescaped ')' is not needed since such occurances will parse
* as premature (and likely) unbalanced parens in the filter expression.
*/
throw new Error('illegal unescaped char: ' + c);
case '\\':
/* Parse a \XX hex escape value */
var val = str.substr(cur + 1, 2);
if (val.match(hexRegex) === null) {
if (morayChars.indexOf(val[0]) === -1) {
throw new Error('invalid escape sequence: "\\' + val + '"');
}
out += val[0];
cur += 2;
} else {
out += String.fromCharCode(parseInt(val, 16));
cur += 3;
}
break;
default:
/* Add one regular char */
out += c;
cur++;
break;
}
}
return out;
}
function escapeSubstr(str)
{
var fields = [];
var out = {};
var idx;
while ((idx = indexOfSkipEscapes(str, 0, '*')) !== -1) {
fields.push(str.substring(0, idx));
str = str.substring(idx + 1);
}
fields.push(str);
assert.ok(fields.length > 1, 'wildcard missing');
out.initial = escapeValue(fields.shift());
out.final = escapeValue(fields.pop());
out.any = fields.map(escapeValue);
return out;
}
function parseExt(attr, str)
{
var fields = str.split(':');
var res = {
attribute: attr
};
var out;
/* Having already parsed the attr, the first entry should be empty */
assert.ok(fields.length > 1, 'invalid ext filter');
fields.shift();
if (fields[0].toLowerCase() === 'dn') {
res.dnAttributes = true;
fields.shift();
}
if (fields.length !== 0 && fields[0][0] !== '=') {
res.rule = fields.shift();
}
if (fields.length === 0 || fields[0][0] !== '=') {
/* With matchType, dnAttribute, and rule consumed, the := must be next */
throw new Error('missing := in ext filter');
}
/*
* Trim the leading = (from the :=) and reinsert any extra ':' charachters
* which may have been present in the value field.
*/
str = fields.join(':').substr(1);
res.value = escapeValue(str);
out = new ExtensibleFilter(res);
/*
* Some extensible filters (such as caseIgnoreSubstringsMatch) operate with
* values formatted with the substring syntax. In order to prevent ambiguity
* between '*' characters which are not escaped and any which are, we attempt
* substring-style parsing on any value which contains the former.
*/
if (indexOfSkipEscapes(str, 0, '*') !== -1) {
var subres = escapeSubstr(str);
out.initial = subres.initial;
out.any = subres.any;
out.final = subres.final;
}
return out;
}
function parseExpr(str)
{
var attr, match, remain;
if (str[0] === ':') {
/*
* An extensible filter can have no attribute name.
* (Only valid when using dn and * matching-rule evaluation)
*/
attr = '';
remain = str;
} else if ((match = str.match(attrRegex)) !== null) {
attr = match[0];
remain = str.substr(attr.length);
} else {
throw new Error('invalid attribute name');
}
if (remain === '=*') {
return new PresenceFilter({
attribute: attr
});
} else if (remain[0] === '=') {
remain = remain.substr(1);
if (indexOfSkipEscapes(remain, 0, '*') !== -1) {
var val = escapeSubstr(remain);
return new SubstringFilter({
attribute: attr,
initial: val.initial,
any: val.any,
final: val.final
});
} else {
return new EqualityFilter({
attribute: attr,
value: escapeValue(remain)
});
}
} else if (remain[0] === '>' && remain[1] === '=') {
return new GreaterThanEqualsFilter({
attribute: attr,
value: escapeValue(remain.substr(2))
});
} else if (remain[0] === '<' && remain[1] === '=') {
return new LessThanEqualsFilter({
attribute: attr,
value: escapeValue(remain.substr(2))
});
} else if (remain[0] === '~' && remain[1] === '=') {
return new ApproximateFilter({
attribute: attr,
value: escapeValue(remain.substr(2))
});
} else if (remain[0] === ':') {
return parseExt(attr, remain);
}
throw new Error('invalid expression');
}
function parseFilter(str, start)
{
var unwrappedbang = false;
var cur = start;
var len = str.length;
var res, end, output, children = [];
switch (str[cur]) {
case '(':
cur++;
break;
case '!':
unwrappedbang = true;
break;
default:
throw new Error('expected \'(\', but found \'' + str[cur] + '\'');
}
if (str[cur] === '&') {
cur++;
do {
res = parseFilter(str, cur);
children.push(res.filter);
cur = res.end + 1;
} while (cur < len && str[cur] !== ')');
output = new AndFilter({filters: children});
} else if (str[cur] === '|') {
cur++;
do {
res = parseFilter(str, cur);
children.push(res.filter);
cur = res.end + 1;
} while (cur < len && str[cur] !== ')');
output = new OrFilter({filters: children});
} else if (str[cur] === '!') {
res = parseFilter(str, cur + 1);
output = new NotFilter({filter: res.filter});
if (unwrappedbang) {
cur = res.end;
} else {
cur = res.end + 1;
assert.equal(str[cur], ')', 'unbalanced parens');
}
} else {
end = indexOfSkipEscapes(str, cur, ')');
assert.notEqual(end, -1, 'unbalanced parens');
output = parseExpr(str.substr(cur, end - cur));
cur = end;
}
if (cur >= len) {
throw new Error('unbalanced parens');
}
return {
end: cur,
filter: output
};
}
///--- Exports
module.exports = {
parse: function (str) {
assert.string(str, 'input must be string');
assert.ok(str.length > 0, 'input string cannot be empty');
/* Wrap the input in parens if it was not already */
if (str.charAt(0) !== '(') {
str = '(' + str + ')';
}
var parsed = parseFilter(str, 0);
var lastIdx = str.length - 1;
if (parsed.end < lastIdx) {
throw new Error('unbalanced parens');
}
return parsed.filter;
},
// Helper utilties for writing custom matchers
testValues: helpers.testValues,
getAttrValue: helpers.getAttrValue,
// Filter definitions
AndFilter: AndFilter,
ApproximateFilter: ApproximateFilter,
EqualityFilter: EqualityFilter,
ExtensibleFilter: ExtensibleFilter,
GreaterThanEqualsFilter: GreaterThanEqualsFilter,
LessThanEqualsFilter: LessThanEqualsFilter,
NotFilter: NotFilter,
OrFilter: OrFilter,
PresenceFilter: PresenceFilter,
SubstringFilter: SubstringFilter
};