UNPKG

@bufbuild/cel

Version:

A CEL evaluator for ECMAScript

487 lines (486 loc) 17.5 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.STRINGS_EXT_FUNCS = void 0; const protobuf_1 = require("@bufbuild/protobuf"); const wkt_1 = require("@bufbuild/protobuf/wkt"); const func_js_1 = require("../../func.js"); const type_js_1 = require("../../type.js"); const list_js_1 = require("../../list.js"); const map_js_1 = require("../../map.js"); const uint_js_1 = require("../../uint.js"); const reflect_1 = require("@bufbuild/protobuf/reflect"); const charAt = (0, func_js_1.celFunc)("charAt", [ (0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.INT], type_js_1.CelScalar.STRING, (str, index) => { const i = Number(index); if (i < 0 || i > str.length) { throw indexOutOfBounds(i, str.length); } return str.charAt(i); }), ]); const indexOf = (0, func_js_1.celFunc)("indexOf", [ (0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.STRING], type_js_1.CelScalar.INT, (str, substr) => BigInt(str.indexOf(substr))), (0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.STRING, type_js_1.CelScalar.INT], type_js_1.CelScalar.INT, (str, substr, startN) => { const start = Number(startN); if (start !== undefined && (start < 0 || start >= str.length)) { throw indexOutOfBounds(start, str.length); } return BigInt(str.indexOf(substr, start)); }), ]); const lastIndexOf = (0, func_js_1.celFunc)("lastIndexOf", [ (0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.STRING], type_js_1.CelScalar.INT, (str, substr) => BigInt(str.lastIndexOf(substr))), (0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.STRING, type_js_1.CelScalar.INT], type_js_1.CelScalar.INT, (str, substr, startN) => { const start = Number(startN); if (start !== undefined && (start < 0 || start >= str.length)) { throw indexOutOfBounds(start, str.length); } return BigInt(str.lastIndexOf(substr, start)); }), ]); const lowerAscii = (0, func_js_1.celFunc)("lowerAscii", [ (0, func_js_1.celOverload)([type_js_1.CelScalar.STRING], type_js_1.CelScalar.STRING, (str) => { // Only lower case ascii characters. let result = ""; for (let i = 0; i < str.length; i++) { const code = str.charCodeAt(i); if (code >= 65 && code <= 90) { result += String.fromCharCode(code + 32); } else { result += str.charAt(i); } } return result; }), ]); const upperAscii = (0, func_js_1.celFunc)("upperAscii", [ (0, func_js_1.celOverload)([type_js_1.CelScalar.STRING], type_js_1.CelScalar.STRING, (str) => { let result = ""; for (let i = 0; i < str.length; i++) { const c = str.charCodeAt(i); if (c >= 97 && c <= 122) { result += String.fromCharCode(c - 32); } else { result += str.charAt(i); } } return result; }), ]); function replaceOp(str, substr, repl, num) { // Replace the first num occurrences of substr with repl. let result = str; let offset = 0; let index = result.indexOf(substr, offset); while (num > 0 && index !== -1) { result = result.substring(0, index) + repl + result.substring(index + substr.length); offset = index + repl.length; num--; index = result.indexOf(substr, offset); } return result; } const replace = (0, func_js_1.celFunc)("replace", [ (0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.STRING, type_js_1.CelScalar.STRING], type_js_1.CelScalar.STRING, (str, substr, repl) => replaceOp(str, substr, repl, str.length)), (0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.STRING, type_js_1.CelScalar.STRING, type_js_1.CelScalar.INT], type_js_1.CelScalar.STRING, (str, substr, repl, num) => replaceOp(str, substr, repl, Number(num))), ]); function splitOp(str, sep, num) { if (num === 1) { return (0, list_js_1.celList)([str]); } return (0, list_js_1.celList)(str.split(sep, num)); } const split = (0, func_js_1.celFunc)("split", [ (0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.STRING], (0, type_js_1.listType)(type_js_1.CelScalar.STRING), splitOp), (0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.STRING, type_js_1.CelScalar.INT], (0, type_js_1.listType)(type_js_1.CelScalar.STRING), (str, sep, num) => splitOp(str, sep, Number(num))), ]); function substringOp(str, start, end) { if (end === undefined) { const i = Number(start); if (i < 0 || i > str.length) { throw indexOutOfBounds(i, str.length); } return str.substring(i); } const i = Number(start); const j = Number(end); if (i < 0 || i > str.length) { throw indexOutOfBounds(i, str.length); } if (j < 0 || j > str.length) { throw indexOutOfBounds(j, str.length); } if (i > j) { throw invalidArgument("substring", "start > end"); } return str.substring(Number(start), Number(end)); } const substring = (0, func_js_1.celFunc)("substring", [ (0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.INT], type_js_1.CelScalar.STRING, substringOp), (0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, type_js_1.CelScalar.INT, type_js_1.CelScalar.INT], type_js_1.CelScalar.STRING, substringOp), ]); // The set of white space characters defined by the unicode standard. const WHITE_SPACE = new Set([ 0x20, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x85, 0xa0, 0x1680, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200a, 0x2028, 0x2029, 0x202f, 0x205f, 0x3000, ]); const trim = (0, func_js_1.celFunc)("trim", [ (0, func_js_1.celOverload)([type_js_1.CelScalar.STRING], type_js_1.CelScalar.STRING, (str) => { // Trim using the unicode white space definition. let start = 0; let end = str.length - 1; while (start < str.length && WHITE_SPACE.has(str.charCodeAt(start))) { start++; } while (end > start && WHITE_SPACE.has(str.charCodeAt(end))) { end--; } return str.substring(start, end + 1); }), ]); function joinOp(list, sep = "") { let result = ""; for (let i = 0; i < list.size; i++) { const item = list.get(i); if (typeof item !== "string") { throw invalidArgument("join", "list contains non-string value"); } if (i > 0) { result += sep; } result += item; } return result; } const join = (0, func_js_1.celFunc)("join", [ (0, func_js_1.celOverload)([(0, type_js_1.listType)(type_js_1.CelScalar.DYN)], type_js_1.CelScalar.STRING, joinOp), (0, func_js_1.celOverload)([(0, type_js_1.listType)(type_js_1.CelScalar.DYN), type_js_1.CelScalar.STRING], type_js_1.CelScalar.STRING, joinOp), ]); const QUOTE_MAP = new Map([ [0x00, "\\0"], [0x07, "\\a"], [0x08, "\\b"], [0x09, "\\t"], [0x0a, "\\n"], [0x0b, "\\v"], [0x0c, "\\f"], [0x0d, "\\r"], [0x22, '\\"'], [0x5c, "\\\\"], ]); const quote = (0, func_js_1.celFunc)("strings.quote", [ (0, func_js_1.celOverload)([type_js_1.CelScalar.STRING], type_js_1.CelScalar.STRING, (str) => { let result = '"'; for (let i = 0; i < str.length; i++) { const c = str.charCodeAt(i); result += QUOTE_MAP.get(c) ?? str.charAt(i); } result += '"'; return result; }), ]); function formatFloatString(val) { switch (val) { case "Infinity": case "-Infinity": case "NaN": return val; default: throw invalidArgument("format", "invalid floating point value"); } } function formatFloating(val, precision) { switch (true) { case typeof val === "number": if (Number.isNaN(val)) { return "NaN"; } if (val === Infinity) { return "Infinity"; } if (val === -Infinity) { return "-Infinity"; } if (precision === undefined) { return val.toString(); } return val.toFixed(precision); case typeof val === "string": return formatFloatString(val); default: throw invalidArgument("format", "fixed-point clause can only be used on doubles"); } } function formatExponent(val, precision) { switch (true) { case typeof val === "number": if (Number.isNaN(val)) { return "NaN"; } if (val === Infinity) { return "Infinity"; } if (val === -Infinity) { return "-Infinity"; } let str = val.toExponential(precision); // toExponential returns 1 or 2 digits after the `+`. // CEL spec mandates 2. So we insert a 0 if we find only // one character after '+'. const plusIdx = str.lastIndexOf("+"); if (plusIdx === str.length - 2) { str = `${str.substring(0, plusIdx + 1)}0${str.substring(plusIdx + 1)}`; } return str; case typeof val === "string": return formatFloatString(val); default: throw invalidArgument("format", "scientific clause can only be used on doubles"); } } function formatBinary(val) { switch (true) { case typeof val === "boolean": return val ? "1" : "0"; case typeof val === "bigint": return val.toString(2); case (0, uint_js_1.isCelUint)(val): return val.value.toString(2); default: throw invalidArgument("format", "only integers and bools can be formatted as binary"); } } function formatOctal(val) { switch (true) { case typeof val === "bigint": return val.toString(8); case (0, uint_js_1.isCelUint)(val): return val.value.toString(8); default: throw invalidArgument("format", "invalid integer value"); } } function formatDecimal(val) { switch (true) { case typeof val === "bigint": return val.toString(10); case (0, uint_js_1.isCelUint)(val): return val.value.toString(10); case typeof val === "number" && Number.isNaN(val): return "NaN"; case val === Infinity: return "Infinity"; case val === -Infinity: return "-Infinity"; default: throw invalidArgument("format", "invalid integer value"); } } function formatHexBytes(val) { let result = ""; for (let i = 0; i < val.length; i++) { result += val[i].toString(16).padStart(2, "0"); } return result; } function formatHex(val) { switch (true) { case typeof val === "bigint": return val.toString(16); case (0, uint_js_1.isCelUint)(val): return val.value.toString(16); case typeof val === "string": const encoder = new TextEncoder(); return formatHexBytes(encoder.encode(val)); case val instanceof Uint8Array: return formatHexBytes(val); default: throw invalidArgument("format", "only integers, byte buffers, and strings can be formatted as hex"); } } function formatHeX(val) { const result = formatHex(val); if (typeof result !== "string") { return result; } return result.toUpperCase(); } function formatList(val) { let result = "["; for (let i = 0; i < val.size; i++) { if (i > 0) { result += ", "; } result += formatString(val.get(i)); } result += "]"; return result; } function formatMap(val) { const formatted = new Array(val.size); let i = 0; for (const [key, value] of val) { formatted[i] = [formatString(key), formatString(value)]; i++; } let result = "{"; let delim = ""; for (const [key, value] of formatted.sort((a, b) => a[0].localeCompare(b[0]))) { result += delim + key + ": " + value; delim = ", "; } result += "}"; return result; } function formatString(val) { switch (typeof val) { case "boolean": return val ? "true" : "false"; case "bigint": return formatDecimal(val); case "number": return formatFloating(val, undefined); case "string": return val; case "object": switch (true) { case val === null: return "null"; case (0, type_js_1.isCelType)(val): return val.name; case (0, uint_js_1.isCelUint)(val): return val.value.toString(); case (0, reflect_1.isReflectMessage)(val, wkt_1.TimestampSchema): return (0, protobuf_1.toJson)(wkt_1.TimestampSchema, val.message); case (0, reflect_1.isReflectMessage)(val, wkt_1.DurationSchema): return (0, protobuf_1.toJson)(wkt_1.DurationSchema, val.message); case val instanceof Uint8Array: return new TextDecoder().decode(val); case (0, list_js_1.isCelList)(val): return formatList(val); case (0, map_js_1.isCelMap)(val): return formatMap(val); } } throw invalidArgument("format", "invalid string value"); } function formatImpl(format, args) { let result = ""; let i = 0; let j = 0; while (i < format.length) { if (format.charAt(i) !== "%") { result += format.charAt(i); i++; continue; } if (i + 1 >= format.length) { throw invalidArgument("format", "invalid format string"); } let c = format.charAt(i + 1); i += 2; if (c === "%") { result += "%"; continue; } let precision = 6; if (c === ".") { // Parse precision. precision = 0; while (i < format.length && format.charAt(i) >= "0" && format.charAt(i) <= "9") { precision = precision * 10 + Number(format.charAt(i)); i++; } if (i >= format.length) { throw invalidArgument("format", "invalid format string"); } c = format.charAt(i); i++; } const val = args.get(j++); if (val === undefined) { throw invalidArgument("format", "too few arguments for format string"); } let str; switch (c) { case "e": str = formatExponent(val, precision); break; case "f": str = formatFloating(val, precision); break; case "b": str = formatBinary(val); break; case "d": str = formatDecimal(val); break; case "s": str = formatString(val); break; case "x": str = formatHex(val); break; case "X": str = formatHeX(val); break; case "o": str = formatOctal(val); break; default: throw invalidArgument("format", `could not parse formatting clause: unrecognized formatting clause: ${c}`); } result += str; } if (j < args.size) { throw invalidArgument("format", "too many arguments for format string"); } return result; } const format = (0, func_js_1.celFunc)("format", [ (0, func_js_1.celOverload)([type_js_1.CelScalar.STRING, (0, type_js_1.listType)(type_js_1.CelScalar.DYN)], type_js_1.CelScalar.STRING, formatImpl), ]); function invalidArgument(func, issue) { return new Error(`invalid argument to function ${func}: ${issue}`); } function indexOutOfBounds(index, length) { return new Error(`index ${index} out of bounds [0, ${length})`); } /** * Provides the strings extension - CEL functions for string manipulation. */ exports.STRINGS_EXT_FUNCS = [ charAt, indexOf, lastIndexOf, lowerAscii, upperAscii, replace, split, substring, trim, join, quote, format, ];