UNPKG

firebase-tools

Version:
397 lines (396 loc) • 14.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ExprParseError = void 0; exports.isCelExpression = isCelExpression; exports.resolveExpression = resolveExpression; const error_1 = require("../../error"); const functional_1 = require("../../functional"); const paramRegexp = /params\.(\S+)/; const CMP = /((?:!=)|(?:==)|(?:>=)|(?:<=)|>|<)/.source; const identityRegexp = /{{ params\.(\S+) }}/; const dualComparisonRegexp = new RegExp(/{{ params\.(\S+) CMP params\.(\S+) }}/.source.replace("CMP", CMP)); const comparisonRegexp = new RegExp(/{{ params\.(\S+) CMP (.+) }}/.source.replace("CMP", CMP)); const EXPR_PREFIX = "{{ "; const EXPR_SUFFIX = " }}"; const QUESTION_TOKEN = " ? "; const COLON_TOKEN = " : "; function listEquals(a, b) { return a.every((item) => b.includes(item)) && b.every((item) => a.includes(item)); } function isCelExpression(value) { return typeof value === "string" && value.includes("{{") && value.includes("}}"); } function isIdentityExpression(value) { return identityRegexp.test(value); } function isComparisonExpression(value) { return comparisonRegexp.test(value); } function isDualComparisonExpression(value) { return dualComparisonRegexp.test(value); } class ExprParseError extends error_1.FirebaseError { } exports.ExprParseError = ExprParseError; function parseTernary(expr) { if (!expr.startsWith(EXPR_PREFIX) || !expr.endsWith(EXPR_SUFFIX)) { return undefined; } const body = expr.slice(EXPR_PREFIX.length, -EXPR_SUFFIX.length); let inQuotes = false; let depth = 0; let question = -1; let colon = -1; for (let i = 0; i < body.length; i++) { if (inQuotes && body[i] === "\\") { i++; continue; } if (body[i] === '"') { inQuotes = !inQuotes; continue; } if (inQuotes) continue; if (body.startsWith(QUESTION_TOKEN, i)) { if (depth === 0) { question = i; } depth++; i += QUESTION_TOKEN.length - 1; } else if (body.startsWith(COLON_TOKEN, i)) { if (depth > 0) { depth--; if (depth === 0) { colon = i; break; } } i += COLON_TOKEN.length - 1; } } if (question === -1) { return undefined; } if (colon === -1) { throw new ExprParseError(`Malformed CEL ternary expression '${expr}'`); } const condition = body.slice(0, question).trim(); const ifTrue = body.slice(question + QUESTION_TOKEN.length, colon).trim(); const ifFalse = body.slice(colon + COLON_TOKEN.length).trim(); if (!condition || !ifTrue || !ifFalse) { throw new ExprParseError(`Malformed CEL ternary expression '${expr}'`); } return { condition, ifTrue, ifFalse }; } function resolveExpression(wantType, expr, params) { expr = preprocessLists(wantType, expr, params); if (isIdentityExpression(expr)) { return resolveIdentity(wantType, expr, params); } const ternary = parseTernary(expr); if (ternary) { return resolveTernary({ wantType, expr, parts: ternary, params }); } else if (wantType === "boolean" && isDualComparisonExpression(expr)) { return resolveDualComparison(expr, params); } else if (wantType === "boolean" && isComparisonExpression(expr)) { return resolveComparison(expr, params); } else { throw new ExprParseError("CEL expression '" + expr + "' is of an unsupported form"); } } function preprocessLists(wantType, expr, params) { let rv = expr; const listMatcher = /\[[^\[\]]*\]/g; let match; while ((match = listMatcher.exec(expr)) != null) { const list = match[0]; const resolved = resolveList("string", list, params); rv = rv.replace(list, JSON.stringify(resolved)); } return rv; } function resolveList(wantType, list, params) { if (!list.startsWith("[") || !list.endsWith("]")) { throw new ExprParseError("Invalid list: must start with '[' and end with ']'"); } else if (list === "[]") { return []; } const rv = []; const entries = list.slice(1, -1).split(","); for (const entry of entries) { const trimmed = entry.trim(); if (trimmed.startsWith('"') && trimmed.endsWith('"')) { rv.push(trimmed.slice(1, -1)); } else if (trimmed.startsWith("{{") && trimmed.endsWith("}}")) { rv.push(resolveExpression("string", trimmed, params)); } else { const paramMatch = paramRegexp.exec(trimmed); if (!paramMatch) { throw new ExprParseError(`Malformed list component ${trimmed}`); } else if (!(paramMatch[1] in params)) { throw new ExprParseError(`List expansion referenced nonexistent param ${paramMatch[1]}`); } rv.push(resolveParamListOrLiteral("string", trimmed, params)); } } return rv; } function assertType(wantType, paramName, paramValue) { if ((wantType === "string" && !paramValue.legalString) || (wantType === "number" && !paramValue.legalNumber) || (wantType === "boolean" && !paramValue.legalBoolean) || (wantType === "string[]" && !paramValue.legalList)) { throw new ExprParseError(`Illegal type coercion of param ${paramName} to type ${wantType}`); } } function readParamValue(wantType, paramName, paramValue) { assertType(wantType, paramName, paramValue); if (wantType === "string") { return paramValue.asString(); } else if (wantType === "number") { return paramValue.asNumber(); } else if (wantType === "boolean") { return paramValue.asBoolean(); } else if (wantType === "string[]") { return paramValue.asList(); } else { (0, functional_1.assertExhaustive)(wantType); } } function resolveIdentity(wantType, expr, params) { const match = identityRegexp.exec(expr); if (!match) { throw new ExprParseError("Malformed CEL identity expression '" + expr + "'"); } const name = match[1]; const value = params[name]; if (!value) { throw new ExprParseError("CEL identity expression '" + expr + "' was not resolvable to a param"); } return readParamValue(wantType, name, value); } function resolveComparison(expr, params) { const match = comparisonRegexp.exec(expr); if (!match) { throw new ExprParseError("Malformed CEL comparison expression '" + expr + "'"); } const cmp = match[2]; const test = function (a, b) { switch (cmp) { case "!=": return Array.isArray(a) ? !listEquals(a, b) : a !== b; case "==": return Array.isArray(a) ? listEquals(a, b) : a === b; case ">=": return a >= b; case "<=": return a <= b; case ">": return a > b; case "<": return a < b; default: throw new ExprParseError("Illegal comparison operator '" + cmp + "'"); } }; const lhsName = match[1]; const lhsVal = params[lhsName]; if (!lhsVal) { throw new ExprParseError("CEL comparison expression '" + expr + "' references missing param " + lhsName); } let rhs; if (lhsVal.legalString) { rhs = resolveLiteral("string", match[3]); return test(lhsVal.asString(), rhs); } else if (lhsVal.legalNumber) { rhs = resolveLiteral("number", match[3]); return test(lhsVal.asNumber(), rhs); } else if (lhsVal.legalBoolean) { rhs = resolveLiteral("boolean", match[3]); return test(lhsVal.asBoolean(), rhs); } else if (lhsVal.legalList) { if (!["==", "!="].includes(cmp)) { throw new ExprParseError(`Unsupported comparison operation ${cmp} on list operands in expression ${expr}`); } rhs = resolveLiteral("string[]", match[3]); return test(lhsVal.asList(), rhs); } else { throw new ExprParseError(`Could not infer type of param ${lhsName} used in comparison operation`); } } function resolveDualComparison(expr, params) { const match = dualComparisonRegexp.exec(expr); if (!match) { throw new ExprParseError("Malformed CEL comparison expression '" + expr + "'"); } const cmp = match[2]; const test = function (a, b) { switch (cmp) { case "!=": return Array.isArray(a) ? !listEquals(a, b) : a !== b; case "==": return Array.isArray(a) ? listEquals(a, b) : a === b; case ">=": return a >= b; case "<=": return a <= b; case ">": return a > b; case "<": return a < b; default: throw new ExprParseError("Illegal comparison operator '" + cmp + "'"); } }; const lhsName = match[1]; const lhsVal = params[lhsName]; if (!lhsVal) { throw new ExprParseError("CEL comparison expression '" + expr + "' references missing param " + lhsName); } const rhsName = match[3]; const rhsVal = params[rhsName]; if (!rhsVal) { throw new ExprParseError("CEL comparison expression '" + expr + "' references missing param " + lhsName); } if (lhsVal.legalString) { if (!rhsVal.legalString) { throw new ExprParseError(`CEL comparison expression ${expr} has type mismatch between the operands`); } return test(lhsVal.asString(), rhsVal.asString()); } else if (lhsVal.legalNumber) { if (!rhsVal.legalNumber) { throw new ExprParseError(`CEL comparison expression ${expr} has type mismatch between the operands`); } return test(lhsVal.asNumber(), rhsVal.asNumber()); } else if (lhsVal.legalBoolean) { if (!rhsVal.legalBoolean) { throw new ExprParseError(`CEL comparison expression ${expr} has type mismatch between the operands`); } return test(lhsVal.asBoolean(), rhsVal.asBoolean()); } else if (lhsVal.legalList) { if (!rhsVal.legalList) { throw new ExprParseError(`CEL comparison expression ${expr} has type mismatch between the operands`); } if (!["==", "!="].includes(cmp)) { throw new ExprParseError(`Unsupported comparison operation ${cmp} on list operands in expression ${expr}`); } return test(lhsVal.asList(), rhsVal.asList()); } else { throw new ExprParseError(`could not infer type of param ${lhsName} used in comparison operation`); } } function resolveTernary(options) { const { wantType, expr, parts, params } = options; const isTrue = resolveTernaryCondition(expr, parts.condition, params); return resolveTernaryBranch({ wantType, expr, branch: isTrue ? parts.ifTrue : parts.ifFalse, params, }); } function resolveTernaryCondition(expr, condition, params) { const conditionExpr = `${EXPR_PREFIX}${condition}${EXPR_SUFFIX}`; if (isDualComparisonExpression(conditionExpr)) { return resolveDualComparison(conditionExpr, params); } else if (isComparisonExpression(conditionExpr)) { return resolveComparison(conditionExpr, params); } const match = identityRegexp.exec(conditionExpr); if (!match) { throw new ExprParseError(`CEL ternary expression '${expr}' is conditioned on an unsupported form`); } const paramName = match[1]; const paramValue = params[paramName]; if (!paramValue) { throw new ExprParseError(`CEL ternary expression '${expr}' references missing param ${paramName}`); } if (!paramValue.legalBoolean) { throw new ExprParseError(`CEL ternary expression '${expr}' is conditional on non-boolean param ${paramName}`); } return paramValue.asBoolean(); } function resolveTernaryBranch(options) { const { wantType, expr, branch, params } = options; const nested = parseTernary(`${EXPR_PREFIX}${branch}${EXPR_SUFFIX}`); if (nested) { return resolveTernary({ wantType, expr, parts: nested, params }); } return resolveParamListOrLiteral(wantType, branch, params); } function resolveParamListOrLiteral(wantType, field, params) { const match = paramRegexp.exec(field); if (!match) { return resolveLiteral(wantType, field); } const paramValue = params[match[1]]; if (!paramValue) { throw new ExprParseError("CEL expression resolved to the value of a missing param " + match[1]); } return readParamValue(wantType, match[1], paramValue); } function resolveLiteral(wantType, value) { if (paramRegexp.exec(value)) { throw new ExprParseError("CEL tried to evaluate param." + value + " in a context which only permits literal values"); } if (wantType === "string[]") { const parsed = JSON.parse(value); if (!Array.isArray(parsed)) { throw new ExprParseError(`CEL tried to read non-list ${JSON.stringify(parsed)} as a list`); } for (const shouldBeString of parsed) { if (typeof shouldBeString !== "string") { throw new ExprParseError(`Evaluated CEL list ${JSON.stringify(parsed)} contained non-string values`); } } return parsed; } else if (wantType === "number") { if (isNaN(+value)) { throw new ExprParseError("CEL literal " + value + " does not seem to be a number"); } return +value; } else if (wantType === "string") { if (!value.startsWith('"') || !value.endsWith('"')) { throw new ExprParseError("CEL literal " + value + ' does not seem to be a "-delimited string'); } return value.slice(1, -1); } else if (wantType === "boolean") { if (value === "true") { return true; } else if (value === "false") { return false; } else { throw new ExprParseError("CEL literal " + value + "does not seem to be a true/false boolean"); } } else { throw new ExprParseError("CEL literal '" + value + "' somehow was resolved with a non-string/number/boolean type"); } }