@bufbuild/protovalidate
Version:
Protocol Buffer Validation for ECMAScript
390 lines (389 loc) • 14.6 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.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;
}