@bufbuild/cel
Version:
A CEL evaluator for ECMAScript
322 lines (321 loc) • 18.1 kB
JavaScript
"use strict";
// Copyright 2024-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
Object.defineProperty(exports, "__esModule", { value: true });
exports.matchesString = matchesString;
exports.addLogic = addLogic;
const func_js_1 = require("../func.js");
const opc = require("../gen/dev/cel/expr/operator_const.js");
const olc = require("../gen/dev/cel/expr/overload_const.js");
const error_js_1 = require("../error.js");
const type_js_1 = require("../type.js");
const equals_js_1 = require("../equals.js");
/**
* This is not in the spec but is part of at least go,java, and cpp implementations.
*
* It should return true for anything exept for the literal `false`.
*/
const notStrictlyFalse = {
dispatch(_, args) {
const raw = args[0];
if ((0, error_js_1.isCelError)(raw)) {
return true;
}
return raw !== false;
},
};
const notFunc = (0, func_js_1.celFunc)(opc.LOGICAL_NOT, [
(0, func_js_1.celOverload)([type_js_1.CelScalar.BOOL], type_js_1.CelScalar.BOOL, (x) => !x),
]);
const and = {
dispatch(_id, args) {
let allBools = true;
const errors = [];
for (let i = 0; i < args.length; i++) {
let arg = args[i];
if (typeof arg === "boolean") {
if (!arg)
return false; // short-circuit
}
else {
allBools = false;
if ((0, error_js_1.isCelError)(arg)) {
errors.push(arg);
}
}
}
if (allBools) {
return true;
}
if (errors.length > 0) {
return (0, error_js_1.celErrorMerge)(errors[0], ...errors.slice(1));
}
return undefined;
},
};
const or = {
dispatch(_, args) {
let allBools = true;
const errors = [];
for (let i = 0; i < args.length; i++) {
let arg = args[i];
if (typeof arg === "boolean") {
if (arg)
return true; // short-circuit
}
else {
allBools = false;
if ((0, error_js_1.isCelError)(arg)) {
errors.push(arg);
}
}
}
if (allBools) {
return false;
}
if (errors.length > 0) {
return (0, error_js_1.celErrorMerge)(errors[0], ...errors.slice(1));
}
return undefined;
},
};
const eqFunc = (0, func_js_1.celFunc)(opc.EQUALS, [
(0, func_js_1.celOverload)([type_js_1.CelScalar.DYN, type_js_1.CelScalar.DYN], type_js_1.CelScalar.BOOL, equals_js_1.equals),
]);
const neFunc = (0, func_js_1.celFunc)(opc.NOT_EQUALS, [
(0, func_js_1.celOverload)([type_js_1.CelScalar.DYN, type_js_1.CelScalar.DYN], type_js_1.CelScalar.BOOL, (lhs, rhs) => !(0, equals_js_1.equals)(lhs, rhs)),
]);
function ltOp(lhs, rhs) {
return lhs < rhs;
}
// biome-ignore format: Easier to read it like a table
const ltFunc = (0, func_js_1.celFunc)(opc.LESS, [
(0, func_js_1.celOverload)([type_js_1.CelScalar.BOOL, type_js_1.CelScalar.BOOL], type_js_1.CelScalar.BOOL, ltOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.BYTES, type_js_1.CelScalar.BYTES], type_js_1.CelScalar.BOOL, (l, r) => compareBytes(l, r) < 0),
(0, func_js_1.celOverload)([type_js_1.CelScalar.DOUBLE, type_js_1.CelScalar.DOUBLE], type_js_1.CelScalar.BOOL, ltOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.STRING], type_js_1.CelScalar.BOOL, ltOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.INT, type_js_1.CelScalar.INT], type_js_1.CelScalar.BOOL, ltOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.INT, type_js_1.CelScalar.UINT], type_js_1.CelScalar.BOOL, (l, r) => l < r.value),
(0, func_js_1.celOverload)([type_js_1.CelScalar.UINT, type_js_1.CelScalar.INT], type_js_1.CelScalar.BOOL, (l, r) => l.value < r),
(0, func_js_1.celOverload)([type_js_1.CelScalar.UINT, type_js_1.CelScalar.UINT], type_js_1.CelScalar.BOOL, (l, r) => l.value < r.value),
// TODO investigate: ECMAScript relational operators support mixed bigint/number operands,
// but removing the coercion to number here fails the conformance test "not_lt_dyn_int_big_lossy_double"
(0, func_js_1.celOverload)([type_js_1.CelScalar.INT, type_js_1.CelScalar.DOUBLE], type_js_1.CelScalar.BOOL, (l, r) => Number(l) < r),
(0, func_js_1.celOverload)([type_js_1.CelScalar.DOUBLE, type_js_1.CelScalar.INT], type_js_1.CelScalar.BOOL, (l, r) => l < Number(r)),
(0, func_js_1.celOverload)([type_js_1.CelScalar.DOUBLE, type_js_1.CelScalar.UINT], type_js_1.CelScalar.BOOL, (l, r) => l < Number(r.value)),
(0, func_js_1.celOverload)([type_js_1.CelScalar.UINT, type_js_1.CelScalar.DOUBLE], type_js_1.CelScalar.BOOL, (l, r) => Number(l.value) < r),
(0, func_js_1.celOverload)([type_js_1.DURATION, type_js_1.DURATION], type_js_1.CelScalar.BOOL, (l, r) => compareDuration(l, r) < 0),
(0, func_js_1.celOverload)([type_js_1.TIMESTAMP, type_js_1.TIMESTAMP], type_js_1.CelScalar.BOOL, (l, r) => compareTimestamp(l, r) < 0),
]);
function lteOp(lhs, rhs) {
return lhs <= rhs;
}
// biome-ignore format: Easier to read it like a table
const leFunc = (0, func_js_1.celFunc)(opc.LESS_EQUALS, [
(0, func_js_1.celOverload)([type_js_1.CelScalar.BOOL, type_js_1.CelScalar.BOOL], type_js_1.CelScalar.BOOL, lteOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.BYTES, type_js_1.CelScalar.BYTES], type_js_1.CelScalar.BOOL, (l, r) => compareBytes(l, r) <= 0),
(0, func_js_1.celOverload)([type_js_1.CelScalar.DOUBLE, type_js_1.CelScalar.DOUBLE], type_js_1.CelScalar.BOOL, lteOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.STRING], type_js_1.CelScalar.BOOL, lteOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.INT, type_js_1.CelScalar.INT], type_js_1.CelScalar.BOOL, lteOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.INT, type_js_1.CelScalar.UINT], type_js_1.CelScalar.BOOL, (l, r) => l <= r.value),
(0, func_js_1.celOverload)([type_js_1.CelScalar.UINT, type_js_1.CelScalar.INT], type_js_1.CelScalar.BOOL, (l, r) => l.value <= r),
(0, func_js_1.celOverload)([type_js_1.CelScalar.UINT, type_js_1.CelScalar.UINT], type_js_1.CelScalar.BOOL, (l, r) => l.value <= r.value),
(0, func_js_1.celOverload)([type_js_1.CelScalar.INT, type_js_1.CelScalar.DOUBLE], type_js_1.CelScalar.BOOL, (l, r) => Number(l) <= r),
(0, func_js_1.celOverload)([type_js_1.CelScalar.DOUBLE, type_js_1.CelScalar.INT], type_js_1.CelScalar.BOOL, (l, r) => l <= Number(r)),
(0, func_js_1.celOverload)([type_js_1.CelScalar.DOUBLE, type_js_1.CelScalar.UINT], type_js_1.CelScalar.BOOL, (l, r) => l <= Number(r.value)),
(0, func_js_1.celOverload)([type_js_1.CelScalar.UINT, type_js_1.CelScalar.DOUBLE], type_js_1.CelScalar.BOOL, (l, r) => Number(l.value) <= r),
(0, func_js_1.celOverload)([type_js_1.DURATION, type_js_1.DURATION], type_js_1.CelScalar.BOOL, (l, r) => compareDuration(l, r) <= 0),
(0, func_js_1.celOverload)([type_js_1.TIMESTAMP, type_js_1.TIMESTAMP], type_js_1.CelScalar.BOOL, (l, r) => compareTimestamp(l, r) <= 0),
]);
function gtOp(lhs, rhs) {
return lhs > rhs;
}
// biome-ignore format: Easier to read it like a table
const gtFunc = (0, func_js_1.celFunc)(opc.GREATER, [
(0, func_js_1.celOverload)([type_js_1.CelScalar.BOOL, type_js_1.CelScalar.BOOL], type_js_1.CelScalar.BOOL, gtOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.BYTES, type_js_1.CelScalar.BYTES], type_js_1.CelScalar.BOOL, (l, r) => compareBytes(l, r) > 0),
(0, func_js_1.celOverload)([type_js_1.CelScalar.DOUBLE, type_js_1.CelScalar.DOUBLE], type_js_1.CelScalar.BOOL, gtOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.STRING], type_js_1.CelScalar.BOOL, gtOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.INT, type_js_1.CelScalar.INT], type_js_1.CelScalar.BOOL, gtOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.INT, type_js_1.CelScalar.UINT], type_js_1.CelScalar.BOOL, (l, r) => l > r.value),
(0, func_js_1.celOverload)([type_js_1.CelScalar.UINT, type_js_1.CelScalar.INT], type_js_1.CelScalar.BOOL, (l, r) => l.value > r),
(0, func_js_1.celOverload)([type_js_1.CelScalar.UINT, type_js_1.CelScalar.UINT], type_js_1.CelScalar.BOOL, (l, r) => l.value > r.value),
(0, func_js_1.celOverload)([type_js_1.CelScalar.INT, type_js_1.CelScalar.DOUBLE], type_js_1.CelScalar.BOOL, (l, r) => Number(l) > r),
(0, func_js_1.celOverload)([type_js_1.CelScalar.DOUBLE, type_js_1.CelScalar.INT], type_js_1.CelScalar.BOOL, (l, r) => l > Number(r)),
(0, func_js_1.celOverload)([type_js_1.CelScalar.DOUBLE, type_js_1.CelScalar.UINT], type_js_1.CelScalar.BOOL, (l, r) => l > Number(r.value)),
(0, func_js_1.celOverload)([type_js_1.CelScalar.UINT, type_js_1.CelScalar.DOUBLE], type_js_1.CelScalar.BOOL, (l, r) => Number(l.value) > r),
(0, func_js_1.celOverload)([type_js_1.DURATION, type_js_1.DURATION], type_js_1.CelScalar.BOOL, (l, r) => compareDuration(l, r) > 0),
(0, func_js_1.celOverload)([type_js_1.TIMESTAMP, type_js_1.TIMESTAMP], type_js_1.CelScalar.BOOL, (l, r) => compareTimestamp(l, r) > 0),
]);
function gteOp(lhs, rhs) {
return lhs >= rhs;
}
// biome-ignore format: Easier to read it like a table
const geFunc = (0, func_js_1.celFunc)(opc.GREATER_EQUALS, [
(0, func_js_1.celOverload)([type_js_1.CelScalar.BOOL, type_js_1.CelScalar.BOOL], type_js_1.CelScalar.BOOL, gteOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.BYTES, type_js_1.CelScalar.BYTES], type_js_1.CelScalar.BOOL, (l, r) => compareBytes(l, r) >= 0),
(0, func_js_1.celOverload)([type_js_1.CelScalar.DOUBLE, type_js_1.CelScalar.DOUBLE], type_js_1.CelScalar.BOOL, gteOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.STRING], type_js_1.CelScalar.BOOL, gteOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.INT, type_js_1.CelScalar.INT], type_js_1.CelScalar.BOOL, gteOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.INT, type_js_1.CelScalar.UINT], type_js_1.CelScalar.BOOL, (l, r) => l >= r.value),
(0, func_js_1.celOverload)([type_js_1.CelScalar.UINT, type_js_1.CelScalar.INT], type_js_1.CelScalar.BOOL, (l, r) => l.value >= r),
(0, func_js_1.celOverload)([type_js_1.CelScalar.UINT, type_js_1.CelScalar.UINT], type_js_1.CelScalar.BOOL, (l, r) => l.value >= r.value),
(0, func_js_1.celOverload)([type_js_1.CelScalar.INT, type_js_1.CelScalar.DOUBLE], type_js_1.CelScalar.BOOL, (l, r) => Number(l) >= r),
(0, func_js_1.celOverload)([type_js_1.CelScalar.DOUBLE, type_js_1.CelScalar.INT], type_js_1.CelScalar.BOOL, (l, r) => l >= Number(r)),
(0, func_js_1.celOverload)([type_js_1.CelScalar.DOUBLE, type_js_1.CelScalar.UINT], type_js_1.CelScalar.BOOL, (l, r) => l >= Number(r.value)),
(0, func_js_1.celOverload)([type_js_1.CelScalar.UINT, type_js_1.CelScalar.DOUBLE], type_js_1.CelScalar.BOOL, (l, r) => Number(l.value) >= r),
(0, func_js_1.celOverload)([type_js_1.DURATION, type_js_1.DURATION], type_js_1.CelScalar.BOOL, (l, r) => compareDuration(l, r) >= 0),
(0, func_js_1.celOverload)([type_js_1.TIMESTAMP, type_js_1.TIMESTAMP], type_js_1.CelScalar.BOOL, (l, r) => compareTimestamp(l, r) >= 0),
]);
const containsFunc = (0, func_js_1.celFunc)(olc.CONTAINS, [
(0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.STRING], type_js_1.CelScalar.BOOL, (x, y) => x.includes(y)),
]);
const endsWithFunc = (0, func_js_1.celFunc)(olc.ENDS_WITH, [
(0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.STRING], type_js_1.CelScalar.BOOL, (x, y) => x.endsWith(y)),
]);
const startsWithFunc = (0, func_js_1.celFunc)(olc.STARTS_WITH, [
(0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.STRING], type_js_1.CelScalar.BOOL, (x, y) => x.startsWith(y)),
]);
/**
* Patterns that are supported in ECMAScript RE and not in
* RE2.
*
* ECMAScript Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Cheatsheet
* RE2: https://github.com/google/re2/wiki/syntax
*/
const invalidPatterns = [
/\\[1-9]/, // backreference eg: \1
/\\k<.>/, // backreference eg: \k<name>
/\(\?\=/, // lookahead eg: Jack(?=Sprat)
/\(\?\!/, // negative lookahead eg: Jack(?!Sprat)
/\(\?\<\=/, // lookbehind eg: (?<=Sprat)Jack
/\(\?\<\!/, // negative lookbehind eg: (? {
let size = 0;
for (const _ of x) {
size++;
}
return BigInt(size);
}),
(0, func_js_1.celOverload)([type_js_1.CelScalar.BYTES], type_js_1.CelScalar.INT, (x) => BigInt(x.length)),
(0, func_js_1.celOverload)([(0, type_js_1.listType)(type_js_1.CelScalar.DYN)], type_js_1.CelScalar.INT, (x) => BigInt(x.size)),
(0, func_js_1.celOverload)([(0, type_js_1.mapType)(type_js_1.CelScalar.INT, type_js_1.CelScalar.DYN)], type_js_1.CelScalar.INT, (x) => BigInt(x.size)),
(0, func_js_1.celOverload)([(0, type_js_1.mapType)(type_js_1.CelScalar.UINT, type_js_1.CelScalar.DYN)], type_js_1.CelScalar.INT, (x) => BigInt(x.size)),
(0, func_js_1.celOverload)([(0, type_js_1.mapType)(type_js_1.CelScalar.BOOL, type_js_1.CelScalar.DYN)], type_js_1.CelScalar.INT, (x) => BigInt(x.size)),
(0, func_js_1.celOverload)([(0, type_js_1.mapType)(type_js_1.CelScalar.STRING, type_js_1.CelScalar.DYN)], type_js_1.CelScalar.INT, (x) => BigInt(x.size)),
]);
function mapInOp(x, y) {
return y.has(x);
}
const inFunc = (0, func_js_1.celFunc)(opc.IN, [
(0, func_js_1.celOverload)([type_js_1.CelScalar.DYN, (0, type_js_1.listType)(type_js_1.CelScalar.DYN)], type_js_1.CelScalar.BOOL, (x, y) => {
for (const v of y) {
if ((0, equals_js_1.equals)(x, v)) {
return true;
}
}
return false;
}),
(0, func_js_1.celOverload)([type_js_1.CelScalar.DYN, (0, type_js_1.mapType)(type_js_1.CelScalar.STRING, type_js_1.CelScalar.DYN)], type_js_1.CelScalar.BOOL, mapInOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.DYN, (0, type_js_1.mapType)(type_js_1.CelScalar.INT, type_js_1.CelScalar.DYN)], type_js_1.CelScalar.BOOL, mapInOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.DYN, (0, type_js_1.mapType)(type_js_1.CelScalar.UINT, type_js_1.CelScalar.DYN)], type_js_1.CelScalar.BOOL, mapInOp),
(0, func_js_1.celOverload)([type_js_1.CelScalar.DYN, (0, type_js_1.mapType)(type_js_1.CelScalar.BOOL, type_js_1.CelScalar.DYN)], type_js_1.CelScalar.BOOL, mapInOp),
]);
function addLogic(funcs) {
funcs.add(opc.NOT_STRICTLY_FALSE, notStrictlyFalse);
funcs.add(opc.LOGICAL_AND, and);
funcs.add(opc.LOGICAL_OR, or);
funcs.add(notFunc);
funcs.add(eqFunc);
funcs.add(neFunc);
funcs.add(ltFunc);
funcs.add(leFunc);
funcs.add(gtFunc);
funcs.add(geFunc);
funcs.add(containsFunc);
funcs.add(endsWithFunc);
funcs.add(startsWithFunc);
funcs.add(matchesFunc);
funcs.add(sizeFunc);
funcs.add(inFunc);
}
function compareDuration(lhs, rhs) {
const cmp = lhs.message.seconds - rhs.message.seconds;
if (cmp == 0n) {
return lhs.message.nanos - rhs.message.nanos;
}
return cmp < 0n ? -1 : 1;
}
function compareTimestamp(lhs, rhs) {
const cmp = lhs.message.seconds - rhs.message.seconds;
if (cmp == 0n) {
return lhs.message.nanos - rhs.message.nanos;
}
return cmp < 0n ? -1 : 1;
}
function compareBytes(lhs, rhs) {
const minLen = Math.min(lhs.length, rhs.length);
for (let i = 0; i < minLen; i++) {
if (lhs[i] < rhs[i]) {
return -1;
}
if (lhs[i] > rhs[i]) {
return 1;
}
}
return lhs.length - rhs.length;
}