UNPKG

@bufbuild/cel

Version:

A CEL evaluator for ECMAScript

322 lines (321 loc) 18.1 kB
"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: (?<!Sprat)Jack, /\\c[A-Z]/, // control character eg: /\cM\cJ/ /\\u[0-9a-fA-F]{4}/, // UTF-16 code-unit /\\0(?!\d)/, // NUL /\[\\b.*\]/, // Backspace eg: [\b] ]; const flagPattern = new RegExp(/^\(\?(?<flags>[ims\-]+)\)/); function matchesString(x, y) { for (const invalidPattern of invalidPatterns) { if (invalidPattern.test(y)) { throw new Error(`Error evaluating pattern ${y}, invalid RE2 syntax`); } } // CEL use RE2 syntax which is a subset of Ecmascript RE except for // the flags and the ability to change the flags mid-sequence. // // The conformance tests use flags at the very beginning of the sequence, which // is likely the most common place where this rare feature will be used. // // Instead of importing an RE2 engine to be able to support this niche, we // can instead just check for the flags at the very beginning and apply them. // // Unsupported flags and flags mid-sequence will fail with to compile the regex. // // Users can choose to override this function and provide an RE2 engine if they really // need to. let flags = ""; const flagMatches = y.match(flagPattern); if (flagMatches) { for (let flag of flagMatches?.groups?.flags ?? "") { if (flag == "-") { break; } flags += flag; } y = y.substring(flagMatches[0].length); } const re = new RegExp(y, flags); return re.test(x); } const matchesFunc = (0, func_js_1.celFunc)(olc.MATCHES, [ (0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.STRING], type_js_1.CelScalar.BOOL, matchesString), ]); const sizeFunc = (0, func_js_1.celFunc)(olc.SIZE, [ (0, func_js_1.celOverload)([type_js_1.CelScalar.STRING], type_js_1.CelScalar.INT, (x) => { 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; }