UNPKG

@bufbuild/protovalidate

Version:

Protocol Buffer Validation for ECMAScript

390 lines (389 loc) 14.6 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.EvalStandardRulesCel = exports.EvalExtendedRulesCel = exports.EvalCustomCel = exports.CelManager = void 0; const protobuf_1 = require("@bufbuild/protobuf"); const wkt_1 = require("@bufbuild/protobuf/wkt"); const cel_1 = require("@bufbuild/cel"); const validate_pb_js_1 = require("./gen/buf/validate/validate_pb.js"); const error_js_1 = require("./error.js"); const rules_js_1 = require("./rules.js"); const lib_js_1 = require("./lib.js"); function createCustomFuncs(regexMatcher) { const reg = new cel_1.FuncRegistry(); reg.add(cel_1.Func.unary("isNan", ["double_is_nan_bool"], (_id, arg) => { return typeof arg == "number" && Number.isNaN(arg); })); reg.add(cel_1.Func.newStrict("isInf", ["double_is_inf_bool", "double_int_is_inf_bool"], (_id, args) => { if (args.length == 1) { return typeof args[0] == "number" && (0, lib_js_1.isInf)(args[0]); } if (args.length == 2) { return (typeof args[0] == "number" && (typeof args[1] == "number" || typeof args[1] == "bigint") && (0, lib_js_1.isInf)(args[0], args[1])); } return false; })); reg.add(cel_1.Func.unary("isHostname", ["string_is_hostname_bool"], (_id, arg) => { if (typeof arg != "string") { return false; } return (0, lib_js_1.isHostname)(arg); })); reg.add(cel_1.Func.binary("isHostAndPort", ["string_bool_is_host_and_port_bool"], (_id, lhs, rhs) => { if (typeof lhs != "string" || typeof rhs != "boolean") { return false; } return (0, lib_js_1.isHostAndPort)(lhs, rhs); })); reg.add(cel_1.Func.unary("isEmail", ["string_is_email_bool"], (_id, arg) => { if (typeof arg != "string") { return false; } return (0, lib_js_1.isEmail)(arg); })); reg.add(cel_1.Func.newStrict("isIp", ["string_is_ip_bool", "string_int_is_ip_bool"], (_id, args) => { if (args.length == 1) { return typeof args[0] == "string" && (0, lib_js_1.isIp)(args[0]); } if (args.length == 2) { return (typeof args[0] == "string" && (typeof args[1] == "number" || typeof args[1] == "bigint") && (0, lib_js_1.isIp)(args[0], args[1])); } return false; })); reg.add(cel_1.Func.newStrict("isIpPrefix", [ "string_is_ip_prefix_bool", "string_int_is_ip_prefix_bool", "string_bool_is_ip_prefix_bool", "string_int_bool_is_ip_prefix_bool", ], (_id, args) => { if (args.length < 1 || typeof args[0] != "string") { return undefined; } if (args.length == 1) { return (0, lib_js_1.isIpPrefix)(args[0]); } if (args.length == 2) { if (typeof args[1] == "boolean") { return (0, lib_js_1.isIpPrefix)(args[0], undefined, args[1]); } if (typeof args[1] == "number" || typeof args[1] == "bigint") { return (0, lib_js_1.isIpPrefix)(args[0], args[1]); } } if (args.length == 3 && (typeof args[1] == "number" || typeof args[1] == "bigint") && typeof args[2] == "boolean") { return (0, lib_js_1.isIpPrefix)(args[0], args[1], args[2]); } return undefined; })); reg.add(cel_1.Func.unary("isUri", ["string_is_uri_bool"], (_id, arg) => { return typeof arg == "string" && (0, lib_js_1.isUri)(arg); })); reg.add(cel_1.Func.unary("isUriRef", ["string_is_uri_ref_bool"], (_id, arg) => { return typeof arg == "string" && (0, lib_js_1.isUriRef)(arg); })); reg.add(cel_1.Func.unary("unique", ["list_unique_bool"], (_id, arg) => { return arg instanceof cel_1.CelList && (0, lib_js_1.unique)(arg); })); reg.add(cel_1.Func.binary("getField", ["dyn_string_get_field_dyn"], (id, lhs, rhs) => { if (typeof rhs == "string" && lhs instanceof cel_1.CelObject) { return lhs.accessByName(id, rhs); } return undefined; })); reg.add(cel_1.Func.binary("contains", ["string_contains_string", "bytes_contains_bytes"], (_id, x, y) => { if (x instanceof Uint8Array && y instanceof Uint8Array) { return (0, lib_js_1.bytesContains)(x, y); } if (typeof x == "string" && typeof y == "string") { return x.includes(y); } return undefined; })); reg.add(cel_1.Func.binary("endsWith", ["string_ends_with_string", "bytes_ends_with_bytes"], (_id, x, y) => { if (x instanceof Uint8Array && y instanceof Uint8Array) { return (0, lib_js_1.bytesEndsWith)(x, y); } if (typeof x === "string" && typeof y === "string") { return x.endsWith(y); } return undefined; })); reg.add(cel_1.Func.binary("startsWith", ["string_starts_with_string", "bytes_starts_with_bytes"], (_id, x, y) => { if (x instanceof Uint8Array && y instanceof Uint8Array) { return (0, lib_js_1.bytesStartsWith)(x, y); } if (typeof x === "string" && typeof y === "string") { return x.startsWith(y); } return undefined; })); if (regexMatcher) { reg.add(cel_1.Func.binary("matches", ["matches_string"], (_id, x, y) => { if (typeof x === "string" && typeof y === "string") { return regexMatcher(y, x); } return undefined; })); } return reg; } class CelManager { constructor(registry, regexMatcher) { this.registry = registry; this.rulesCache = new Map(); this.now = (0, wkt_1.timestampNow)(); this.env = (0, cel_1.createEnv)("", registry); this.env.addFuncs(createCustomFuncs(regexMatcher)); this.env.set("now", this.now); } /** * Update the CEL variable "now" to the current time. */ updateCelNow() { const n2 = (0, wkt_1.timestampNow)(); this.now.seconds = n2.seconds; this.now.nanos = n2.nanos; } setEnv(key, value) { this.env.set(key, value); } eval(compiled) { if (compiled.kind == "compilation_error") { throw compiled.error; } const rule = compiled.rule; const result = this.env.eval(compiled.interpretable); if (typeof result == "string" || typeof result == "boolean") { const success = typeof result == "boolean" ? result : result.length == 0; if (success) { return undefined; } // From field buf.validate.Rule.message: // > If a non-empty message is provided, any strings resulting from the CEL // > expression evaluation are ignored. return { message: rule.message.length == 0 && typeof result == "string" ? result : rule.message, ruleId: rule.id, }; } if (result instanceof cel_1.CelError) { throw new error_js_1.RuntimeError(result.message, { cause: result }); } throw new error_js_1.RuntimeError(`expression ${rule.id} outputs ${typeof result}, wanted either bool or string`); } compileRule(rule) { try { return { kind: "interpretable", interpretable: this.env.plan(this.env.parse(rule.expression)), rule, }; } catch (cause) { return { kind: "compilation_error", error: new error_js_1.CompilationError(`failed to compile ${rule.id}: ${String(cause)}`, { cause }), }; } } compileRules(descMessage) { let compiled = this.rulesCache.get(descMessage.typeName); if (!compiled) { compiled = this.compileRulesUncached(descMessage); this.rulesCache.set(descMessage.typeName, compiled); } return compiled; } compileRulesUncached(descMessage) { const standard = []; const extensions = new Map(); for (const field of descMessage.fields) { if (!(0, protobuf_1.hasOption)(field, validate_pb_js_1.predefined)) { continue; } for (const rule of (0, protobuf_1.getOption)(field, validate_pb_js_1.predefined).cel) { standard.push({ field, rule, compiled: this.compileRule(rule), }); } } for (const ext of registryGetExtensionsFor(this.registry, descMessage)) { if (!(0, protobuf_1.hasOption)(ext, validate_pb_js_1.predefined)) { continue; } let list = extensions.get(ext.number); if (!list) { list = []; extensions.set(ext.number, list); } for (const rule of (0, protobuf_1.getOption)(ext, validate_pb_js_1.predefined).cel) { list.push({ ext, rule, compiled: this.compileRule(rule), }); } } return { standard, extensions }; } } exports.CelManager = CelManager; function registryGetExtensionsFor(registry, extendee) { const result = []; for (const type of registry) { if (type.kind == "extension" && type.extendee.typeName == extendee.typeName) { result.push(type); } } return result; } class EvalCustomCel { constructor(celMan, forMapKey, thisScalarType) { this.celMan = celMan; this.forMapKey = forMapKey; this.thisScalarType = thisScalarType; this.children = []; } add(compiled, rulePath) { this.children.push({ compiled, rulePath }); } eval(val, cursor) { this.celMan.setEnv("this", reflectToCel(val, this.thisScalarType)); this.celMan.setEnv("rules", undefined); this.celMan.setEnv("rule", undefined); for (const child of this.children) { const vio = this.celMan.eval(child.compiled); if (vio) { cursor.violate(vio.message, vio.ruleId, child.rulePath, this.forMapKey); } } } prune() { return this.children.length == 0; } } exports.EvalCustomCel = EvalCustomCel; class EvalExtendedRulesCel { constructor(celMan, rules, forMapKey) { this.celMan = celMan; this.rules = rules; this.forMapKey = forMapKey; this.children = []; this.thisScalarType = (0, rules_js_1.getRuleScalarType)(rules); } add(compiled, rulePath, ruleValue, ruleScalarType) { this.children.push({ compiled, rulePath, ruleCelValue: reflectToCel(ruleValue, ruleScalarType), }); } eval(val, cursor) { this.celMan.setEnv("rules", this.rules); this.celMan.setEnv("this", reflectToCel(val, this.thisScalarType)); for (const child of this.children) { this.celMan.setEnv("rule", child.ruleCelValue); const vio = this.celMan.eval(child.compiled); if (vio) { cursor.violate(vio.message, vio.ruleId, child.rulePath, this.forMapKey); } } } prune() { return this.children.length == 0; } } exports.EvalExtendedRulesCel = EvalExtendedRulesCel; class EvalStandardRulesCel { constructor(celMan, rules, forMapKey) { this.celMan = celMan; this.rules = rules; this.forMapKey = forMapKey; this.children = []; this.thisScalarType = (0, rules_js_1.getRuleScalarType)(rules); } add(compiled, rulePath) { this.children.push({ compiled, rulePath }); } eval(val, cursor) { this.celMan.setEnv("this", reflectToCel(val, this.thisScalarType)); this.celMan.setEnv("rules", this.rules); this.celMan.setEnv("rule", undefined); for (const child of this.children) { const vio = this.celMan.eval(child.compiled); if (vio) { cursor.violate(vio.message, vio.ruleId, child.rulePath, this.forMapKey); } } } prune() { return this.children.length == 0; } } exports.EvalStandardRulesCel = EvalStandardRulesCel; function reflectToCel(val, scalarType) { switch (scalarType) { case protobuf_1.ScalarType.DOUBLE: case protobuf_1.ScalarType.FLOAT: case protobuf_1.ScalarType.BOOL: case protobuf_1.ScalarType.STRING: case protobuf_1.ScalarType.BYTES: break; case protobuf_1.ScalarType.UINT32: case protobuf_1.ScalarType.FIXED32: if (typeof val == "number") { return cel_1.CelUint.of(BigInt(val)); } break; case protobuf_1.ScalarType.UINT64: case protobuf_1.ScalarType.FIXED64: switch (typeof val) { case "bigint": return cel_1.CelUint.of(val); case "number": case "string": return cel_1.CelUint.of(BigInt(val)); } break; case protobuf_1.ScalarType.INT32: case protobuf_1.ScalarType.SFIXED32: case protobuf_1.ScalarType.SINT32: if (typeof val == "number") { return BigInt(val); } break; case protobuf_1.ScalarType.INT64: case protobuf_1.ScalarType.SFIXED64: case protobuf_1.ScalarType.SINT64: switch (typeof val) { case "number": case "string": return BigInt(val); } break; } return val; }