No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

cel.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.resolveExpression = exports.ExprParseError = exports.isCelExpression = void 0;
  4. const error_1 = require("../../error");
  5. const functional_1 = require("../../functional");
  6. const paramRegexp = /params\.(\S+)/;
  7. const CMP = /((?:!=)|(?:==)|(?:>=)|(?:<=)|>|<)/.source;
  8. const identityRegexp = /{{ params\.(\S+) }}/;
  9. const dualComparisonRegexp = new RegExp(/{{ params\.(\S+) CMP params\.(\S+) }}/.source.replace("CMP", CMP));
  10. const comparisonRegexp = new RegExp(/{{ params\.(\S+) CMP (.+) }}/.source.replace("CMP", CMP));
  11. const dualTernaryRegexp = new RegExp(/{{ params\.(\S+) CMP params\.(\S+) \? (.+) : (.+) }/.source.replace("CMP", CMP));
  12. const ternaryRegexp = new RegExp(/{{ params\.(\S+) CMP (.+) \? (.+) : (.+) }/.source.replace("CMP", CMP));
  13. const literalTernaryRegexp = /{{ params\.(\S+) \? (.+) : (.+) }/;
  14. function listEquals(a, b) {
  15. return a.every((item) => b.includes(item)) && b.every((item) => a.includes(item));
  16. }
  17. function isCelExpression(value) {
  18. return typeof value === "string" && value.includes("{{") && value.includes("}}");
  19. }
  20. exports.isCelExpression = isCelExpression;
  21. function isIdentityExpression(value) {
  22. return identityRegexp.test(value);
  23. }
  24. function isComparisonExpression(value) {
  25. return comparisonRegexp.test(value);
  26. }
  27. function isDualComparisonExpression(value) {
  28. return dualComparisonRegexp.test(value);
  29. }
  30. function isTernaryExpression(value) {
  31. return ternaryRegexp.test(value);
  32. }
  33. function isLiteralTernaryExpression(value) {
  34. return literalTernaryRegexp.test(value);
  35. }
  36. function isDualTernaryExpression(value) {
  37. return dualTernaryRegexp.test(value);
  38. }
  39. class ExprParseError extends error_1.FirebaseError {
  40. }
  41. exports.ExprParseError = ExprParseError;
  42. function resolveExpression(wantType, expr, params) {
  43. expr = preprocessLists(wantType, expr, params);
  44. if (isIdentityExpression(expr)) {
  45. return resolveIdentity(wantType, expr, params);
  46. }
  47. else if (isDualTernaryExpression(expr)) {
  48. return resolveDualTernary(wantType, expr, params);
  49. }
  50. else if (isLiteralTernaryExpression(expr)) {
  51. return resolveLiteralTernary(wantType, expr, params);
  52. }
  53. else if (isTernaryExpression(expr)) {
  54. return resolveTernary(wantType, expr, params);
  55. }
  56. else if (isDualComparisonExpression(expr)) {
  57. return resolveDualComparison(expr, params);
  58. }
  59. else if (isComparisonExpression(expr)) {
  60. return resolveComparison(expr, params);
  61. }
  62. else {
  63. throw new ExprParseError("CEL expression '" + expr + "' is of an unsupported form");
  64. }
  65. }
  66. exports.resolveExpression = resolveExpression;
  67. function preprocessLists(wantType, expr, params) {
  68. let rv = expr;
  69. const listMatcher = /\[[^\[\]]*\]/g;
  70. let match;
  71. while ((match = listMatcher.exec(expr)) != null) {
  72. const list = match[0];
  73. const resolved = resolveList("string", list, params);
  74. rv = rv.replace(list, JSON.stringify(resolved));
  75. }
  76. return rv;
  77. }
  78. function resolveList(wantType, list, params) {
  79. if (!list.startsWith("[") || !list.endsWith("]")) {
  80. throw new ExprParseError("Invalid list: must start with '[' and end with ']'");
  81. }
  82. else if (list === "[]") {
  83. return [];
  84. }
  85. const rv = [];
  86. const entries = list.slice(1, -1).split(",");
  87. for (const entry of entries) {
  88. const trimmed = entry.trim();
  89. if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
  90. rv.push(trimmed.slice(1, -1));
  91. }
  92. else if (trimmed.startsWith("{{") && trimmed.endsWith("}}")) {
  93. rv.push(resolveExpression("string", trimmed, params));
  94. }
  95. else {
  96. const paramMatch = paramRegexp.exec(trimmed);
  97. if (!paramMatch) {
  98. throw new ExprParseError(`Malformed list component ${trimmed}`);
  99. }
  100. else if (!(paramMatch[1] in params)) {
  101. throw new ExprParseError(`List expansion referenced nonexistent param ${paramMatch[1]}`);
  102. }
  103. rv.push(resolveParamListOrLiteral("string", trimmed, params));
  104. }
  105. }
  106. return rv;
  107. }
  108. function assertType(wantType, paramName, paramValue) {
  109. if ((wantType === "string" && !paramValue.legalString) ||
  110. (wantType === "number" && !paramValue.legalNumber) ||
  111. (wantType === "boolean" && !paramValue.legalBoolean) ||
  112. (wantType === "string[]" && !paramValue.legalList)) {
  113. throw new ExprParseError(`Illegal type coercion of param ${paramName} to type ${wantType}`);
  114. }
  115. }
  116. function readParamValue(wantType, paramName, paramValue) {
  117. assertType(wantType, paramName, paramValue);
  118. if (wantType === "string") {
  119. return paramValue.asString();
  120. }
  121. else if (wantType === "number") {
  122. return paramValue.asNumber();
  123. }
  124. else if (wantType === "boolean") {
  125. return paramValue.asBoolean();
  126. }
  127. else if (wantType === "string[]") {
  128. return paramValue.asList();
  129. }
  130. else {
  131. (0, functional_1.assertExhaustive)(wantType);
  132. }
  133. }
  134. function resolveIdentity(wantType, expr, params) {
  135. const match = identityRegexp.exec(expr);
  136. if (!match) {
  137. throw new ExprParseError("Malformed CEL identity expression '" + expr + "'");
  138. }
  139. const name = match[1];
  140. const value = params[name];
  141. if (!value) {
  142. throw new ExprParseError("CEL identity expression '" + expr + "' was not resolvable to a param");
  143. }
  144. return readParamValue(wantType, name, value);
  145. }
  146. function resolveComparison(expr, params) {
  147. const match = comparisonRegexp.exec(expr);
  148. if (!match) {
  149. throw new ExprParseError("Malformed CEL comparison expression '" + expr + "'");
  150. }
  151. const cmp = match[2];
  152. const test = function (a, b) {
  153. switch (cmp) {
  154. case "!=":
  155. return Array.isArray(a) ? !listEquals(a, b) : a !== b;
  156. case "==":
  157. return Array.isArray(a) ? listEquals(a, b) : a === b;
  158. case ">=":
  159. return a >= b;
  160. case "<=":
  161. return a <= b;
  162. case ">":
  163. return a > b;
  164. case "<":
  165. return a < b;
  166. default:
  167. throw new ExprParseError("Illegal comparison operator '" + cmp + "'");
  168. }
  169. };
  170. const lhsName = match[1];
  171. const lhsVal = params[lhsName];
  172. if (!lhsVal) {
  173. throw new ExprParseError("CEL comparison expression '" + expr + "' references missing param " + lhsName);
  174. }
  175. let rhs;
  176. if (lhsVal.legalString) {
  177. rhs = resolveLiteral("string", match[3]);
  178. return test(lhsVal.asString(), rhs);
  179. }
  180. else if (lhsVal.legalNumber) {
  181. rhs = resolveLiteral("number", match[3]);
  182. return test(lhsVal.asNumber(), rhs);
  183. }
  184. else if (lhsVal.legalBoolean) {
  185. rhs = resolveLiteral("boolean", match[3]);
  186. return test(lhsVal.asBoolean(), rhs);
  187. }
  188. else if (lhsVal.legalList) {
  189. if (!["==", "!="].includes(cmp)) {
  190. throw new ExprParseError(`Unsupported comparison operation ${cmp} on list operands in expression ${expr}`);
  191. }
  192. rhs = resolveLiteral("string[]", match[3]);
  193. return test(lhsVal.asList(), rhs);
  194. }
  195. else {
  196. throw new ExprParseError(`Could not infer type of param ${lhsName} used in comparison operation`);
  197. }
  198. }
  199. function resolveDualComparison(expr, params) {
  200. const match = dualComparisonRegexp.exec(expr);
  201. if (!match) {
  202. throw new ExprParseError("Malformed CEL comparison expression '" + expr + "'");
  203. }
  204. const cmp = match[2];
  205. const test = function (a, b) {
  206. switch (cmp) {
  207. case "!=":
  208. return Array.isArray(a) ? !listEquals(a, b) : a !== b;
  209. case "==":
  210. return Array.isArray(a) ? listEquals(a, b) : a === b;
  211. case ">=":
  212. return a >= b;
  213. case "<=":
  214. return a <= b;
  215. case ">":
  216. return a > b;
  217. case "<":
  218. return a < b;
  219. default:
  220. throw new ExprParseError("Illegal comparison operator '" + cmp + "'");
  221. }
  222. };
  223. const lhsName = match[1];
  224. const lhsVal = params[lhsName];
  225. if (!lhsVal) {
  226. throw new ExprParseError("CEL comparison expression '" + expr + "' references missing param " + lhsName);
  227. }
  228. const rhsName = match[3];
  229. const rhsVal = params[rhsName];
  230. if (!rhsVal) {
  231. throw new ExprParseError("CEL comparison expression '" + expr + "' references missing param " + lhsName);
  232. }
  233. if (lhsVal.legalString) {
  234. if (!rhsVal.legalString) {
  235. throw new ExprParseError(`CEL comparison expression ${expr} has type mismatch between the operands`);
  236. }
  237. return test(lhsVal.asString(), rhsVal.asString());
  238. }
  239. else if (lhsVal.legalNumber) {
  240. if (!rhsVal.legalNumber) {
  241. throw new ExprParseError(`CEL comparison expression ${expr} has type mismatch between the operands`);
  242. }
  243. return test(lhsVal.asNumber(), rhsVal.asNumber());
  244. }
  245. else if (lhsVal.legalBoolean) {
  246. if (!rhsVal.legalBoolean) {
  247. throw new ExprParseError(`CEL comparison expression ${expr} has type mismatch between the operands`);
  248. }
  249. return test(lhsVal.asBoolean(), rhsVal.asBoolean());
  250. }
  251. else if (lhsVal.legalList) {
  252. if (!rhsVal.legalList) {
  253. throw new ExprParseError(`CEL comparison expression ${expr} has type mismatch between the operands`);
  254. }
  255. if (!["==", "!="].includes(cmp)) {
  256. throw new ExprParseError(`Unsupported comparison operation ${cmp} on list operands in expression ${expr}`);
  257. }
  258. return test(lhsVal.asList(), rhsVal.asList());
  259. }
  260. else {
  261. throw new ExprParseError(`could not infer type of param ${lhsName} used in comparison operation`);
  262. }
  263. }
  264. function resolveTernary(wantType, expr, params) {
  265. const match = ternaryRegexp.exec(expr);
  266. if (!match) {
  267. throw new ExprParseError("malformed CEL ternary expression '" + expr + "'");
  268. }
  269. const comparisonExpr = `{{ params.${match[1]} ${match[2]} ${match[3]} }}`;
  270. const isTrue = resolveComparison(comparisonExpr, params);
  271. if (isTrue) {
  272. return resolveParamListOrLiteral(wantType, match[4], params);
  273. }
  274. else {
  275. return resolveParamListOrLiteral(wantType, match[5], params);
  276. }
  277. }
  278. function resolveDualTernary(wantType, expr, params) {
  279. const match = dualTernaryRegexp.exec(expr);
  280. if (!match) {
  281. throw new ExprParseError("Malformed CEL ternary expression '" + expr + "'");
  282. }
  283. const comparisonExpr = `{{ params.${match[1]} ${match[2]} params.${match[3]} }}`;
  284. const isTrue = resolveDualComparison(comparisonExpr, params);
  285. if (isTrue) {
  286. return resolveParamListOrLiteral(wantType, match[4], params);
  287. }
  288. else {
  289. return resolveParamListOrLiteral(wantType, match[5], params);
  290. }
  291. }
  292. function resolveLiteralTernary(wantType, expr, params) {
  293. const match = literalTernaryRegexp.exec(expr);
  294. if (!match) {
  295. throw new ExprParseError("Malformed CEL ternary expression '" + expr + "'");
  296. }
  297. const paramName = match[1];
  298. const paramValue = params[match[1]];
  299. if (!paramValue) {
  300. throw new ExprParseError("CEL ternary expression '" + expr + "' references missing param " + paramName);
  301. }
  302. if (!paramValue.legalBoolean) {
  303. throw new ExprParseError("CEL ternary expression '" + expr + "' is conditional on non-boolean param " + paramName);
  304. }
  305. if (paramValue.asBoolean()) {
  306. return resolveParamListOrLiteral(wantType, match[2], params);
  307. }
  308. else {
  309. return resolveParamListOrLiteral(wantType, match[3], params);
  310. }
  311. }
  312. function resolveParamListOrLiteral(wantType, field, params) {
  313. const match = paramRegexp.exec(field);
  314. if (!match) {
  315. return resolveLiteral(wantType, field);
  316. }
  317. const paramValue = params[match[1]];
  318. if (!paramValue) {
  319. throw new ExprParseError("CEL expression resolved to the value of a missing param " + match[1]);
  320. }
  321. return readParamValue(wantType, match[1], paramValue);
  322. }
  323. function resolveLiteral(wantType, value) {
  324. if (paramRegexp.exec(value)) {
  325. throw new ExprParseError("CEL tried to evaluate param." + value + " in a context which only permits literal values");
  326. }
  327. if (wantType === "string[]") {
  328. const parsed = JSON.parse(value);
  329. if (!Array.isArray(parsed)) {
  330. throw new ExprParseError(`CEL tried to read non-list ${JSON.stringify(parsed)} as a list`);
  331. }
  332. for (const shouldBeString of parsed) {
  333. if (typeof shouldBeString !== "string") {
  334. throw new ExprParseError(`Evaluated CEL list ${JSON.stringify(parsed)} contained non-string values`);
  335. }
  336. }
  337. return parsed;
  338. }
  339. else if (wantType === "number") {
  340. if (isNaN(+value)) {
  341. throw new ExprParseError("CEL literal " + value + " does not seem to be a number");
  342. }
  343. return +value;
  344. }
  345. else if (wantType === "string") {
  346. if (!value.startsWith('"') || !value.endsWith('"')) {
  347. throw new ExprParseError("CEL literal " + value + ' does not seem to be a "-delimited string');
  348. }
  349. return value.slice(1, -1);
  350. }
  351. else if (wantType === "boolean") {
  352. if (value === "true") {
  353. return true;
  354. }
  355. else if (value === "false") {
  356. return false;
  357. }
  358. else {
  359. throw new ExprParseError("CEL literal " + value + "does not seem to be a true/false boolean");
  360. }
  361. }
  362. else {
  363. throw new ExprParseError("CEL literal '" + value + "' somehow was resolved with a non-string/number/boolean type");
  364. }
  365. }