@safe-ugc-ui/validator 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.d.ts +21 -44
- package/dist/index.js +101 -528
- package/dist/index.js.map +1 -1
- package/package.json +10 -8
package/dist/index.js
CHANGED
|
@@ -260,7 +260,6 @@ function validateNodes(views) {
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
// src/value-types.ts
|
|
263
|
-
import { isRef, isExpr } from "@safe-ugc-ui/types";
|
|
264
263
|
var STATIC_ONLY_STYLE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
265
264
|
// Position / layout
|
|
266
265
|
"position",
|
|
@@ -268,12 +267,16 @@ var STATIC_ONLY_STYLE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
|
268
267
|
"right",
|
|
269
268
|
"bottom",
|
|
270
269
|
"left",
|
|
270
|
+
// Overflow
|
|
271
|
+
"overflow",
|
|
272
|
+
// Stacking
|
|
273
|
+
"zIndex"
|
|
274
|
+
]);
|
|
275
|
+
var STRUCTURED_OBJECT_STYLE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
271
276
|
// Transform
|
|
272
277
|
"transform",
|
|
273
278
|
// Gradient
|
|
274
279
|
"backgroundGradient",
|
|
275
|
-
// Overflow
|
|
276
|
-
"overflow",
|
|
277
280
|
// Borders
|
|
278
281
|
"border",
|
|
279
282
|
"borderTop",
|
|
@@ -281,46 +284,8 @@ var STATIC_ONLY_STYLE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
|
281
284
|
"borderBottom",
|
|
282
285
|
"borderLeft",
|
|
283
286
|
// Shadow
|
|
284
|
-
"boxShadow"
|
|
285
|
-
// Stacking
|
|
286
|
-
"zIndex"
|
|
287
|
+
"boxShadow"
|
|
287
288
|
]);
|
|
288
|
-
function validateNodeFields(node, ctx, errors) {
|
|
289
|
-
const nodeType = node.type;
|
|
290
|
-
if (nodeType === "Image" || nodeType === "Avatar") {
|
|
291
|
-
const src = node.src;
|
|
292
|
-
if (src !== void 0 && isExpr(src)) {
|
|
293
|
-
errors.push(
|
|
294
|
-
createError(
|
|
295
|
-
"EXPR_NOT_ALLOWED",
|
|
296
|
-
`${nodeType}.src does not allow $expr bindings.`,
|
|
297
|
-
`${ctx.path}.src`
|
|
298
|
-
)
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
if (nodeType === "Icon") {
|
|
303
|
-
const name = node.name;
|
|
304
|
-
if (name !== void 0 && isRef(name)) {
|
|
305
|
-
errors.push(
|
|
306
|
-
createError(
|
|
307
|
-
"REF_NOT_ALLOWED",
|
|
308
|
-
"Icon.name does not allow $ref bindings.",
|
|
309
|
-
`${ctx.path}.name`
|
|
310
|
-
)
|
|
311
|
-
);
|
|
312
|
-
}
|
|
313
|
-
if (name !== void 0 && isExpr(name)) {
|
|
314
|
-
errors.push(
|
|
315
|
-
createError(
|
|
316
|
-
"EXPR_NOT_ALLOWED",
|
|
317
|
-
"Icon.name does not allow $expr bindings.",
|
|
318
|
-
`${ctx.path}.name`
|
|
319
|
-
)
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
289
|
function validateNodeStyle(node, ctx, errors) {
|
|
325
290
|
const style = node.style;
|
|
326
291
|
if (!style) {
|
|
@@ -330,12 +295,21 @@ function validateNodeStyle(node, ctx, errors) {
|
|
|
330
295
|
if (value === void 0) {
|
|
331
296
|
continue;
|
|
332
297
|
}
|
|
333
|
-
if (
|
|
334
|
-
if (
|
|
298
|
+
if (typeof value === "object" && value !== null && "$ref" in value && typeof value.$ref === "string") {
|
|
299
|
+
if (STATIC_ONLY_STYLE_PROPERTIES.has(prop)) {
|
|
335
300
|
errors.push(
|
|
336
301
|
createError(
|
|
337
302
|
"DYNAMIC_NOT_ALLOWED",
|
|
338
|
-
`Style property "${prop}" must be a static literal; $ref
|
|
303
|
+
`Style property "${prop}" must be a static literal; $ref is not allowed.`,
|
|
304
|
+
`${ctx.path}.style.${prop}`
|
|
305
|
+
)
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
if (STRUCTURED_OBJECT_STYLE_PROPERTIES.has(prop)) {
|
|
309
|
+
errors.push(
|
|
310
|
+
createError(
|
|
311
|
+
"DYNAMIC_NOT_ALLOWED",
|
|
312
|
+
`Style property "${prop}" must be an object literal; use $ref only inside its nested fields.`,
|
|
339
313
|
`${ctx.path}.style.${prop}`
|
|
340
314
|
)
|
|
341
315
|
);
|
|
@@ -346,7 +320,6 @@ function validateNodeStyle(node, ctx, errors) {
|
|
|
346
320
|
function validateValueTypes(views) {
|
|
347
321
|
const errors = [];
|
|
348
322
|
traverseCard(views, (node, ctx) => {
|
|
349
|
-
validateNodeFields(node, ctx, errors);
|
|
350
323
|
validateNodeStyle(node, ctx, errors);
|
|
351
324
|
});
|
|
352
325
|
return errors;
|
|
@@ -377,8 +350,7 @@ import {
|
|
|
377
350
|
TRANSITION_DELAY_MAX,
|
|
378
351
|
TRANSITION_MAX_COUNT,
|
|
379
352
|
ALLOWED_TRANSITION_PROPERTIES,
|
|
380
|
-
isRef
|
|
381
|
-
isExpr as isExpr2
|
|
353
|
+
isRef
|
|
382
354
|
} from "@safe-ugc-ui/types";
|
|
383
355
|
var COLOR_PROPERTIES = /* @__PURE__ */ new Set(["backgroundColor", "color"]);
|
|
384
356
|
var LENGTH_PROPERTIES = /* @__PURE__ */ new Set([
|
|
@@ -434,7 +406,7 @@ function isLiteralString(value) {
|
|
|
434
406
|
return typeof value === "string";
|
|
435
407
|
}
|
|
436
408
|
function isDynamic(value) {
|
|
437
|
-
return
|
|
409
|
+
return isRef(value);
|
|
438
410
|
}
|
|
439
411
|
function isValidColor(value) {
|
|
440
412
|
const lower = value.toLowerCase();
|
|
@@ -479,7 +451,7 @@ function collectDangerousCssErrors(value, path, errors) {
|
|
|
479
451
|
}
|
|
480
452
|
return;
|
|
481
453
|
}
|
|
482
|
-
if (
|
|
454
|
+
if (isRef(value)) {
|
|
483
455
|
return;
|
|
484
456
|
}
|
|
485
457
|
if (Array.isArray(value)) {
|
|
@@ -524,7 +496,66 @@ function validateShadowObject(shadow, path, errors) {
|
|
|
524
496
|
}
|
|
525
497
|
}
|
|
526
498
|
var STYLE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
527
|
-
function
|
|
499
|
+
function reportStyleRefError(rawRef, stylePath, cardStyles, errors) {
|
|
500
|
+
const trimmedRef = rawRef.trim();
|
|
501
|
+
if (!STYLE_NAME_PATTERN.test(trimmedRef)) {
|
|
502
|
+
errors.push(
|
|
503
|
+
createError(
|
|
504
|
+
"INVALID_STYLE_REF",
|
|
505
|
+
`$style value "${rawRef}" is invalid; must match /^[A-Za-z][A-Za-z0-9_-]*$/ at "${stylePath}.$style"`,
|
|
506
|
+
`${stylePath}.$style`
|
|
507
|
+
)
|
|
508
|
+
);
|
|
509
|
+
return void 0;
|
|
510
|
+
}
|
|
511
|
+
if (!cardStyles || !(trimmedRef in cardStyles)) {
|
|
512
|
+
errors.push(
|
|
513
|
+
createError(
|
|
514
|
+
"STYLE_REF_NOT_FOUND",
|
|
515
|
+
`$style references "${trimmedRef}" which is not defined in card.styles at "${stylePath}.$style"`,
|
|
516
|
+
`${stylePath}.$style`
|
|
517
|
+
)
|
|
518
|
+
);
|
|
519
|
+
return void 0;
|
|
520
|
+
}
|
|
521
|
+
return trimmedRef;
|
|
522
|
+
}
|
|
523
|
+
function resolveStyleRef(style, stylePath, cardStyles, errors) {
|
|
524
|
+
if (typeof style.$style !== "string") {
|
|
525
|
+
return style;
|
|
526
|
+
}
|
|
527
|
+
const trimmedRef = reportStyleRefError(style.$style, stylePath, cardStyles, errors);
|
|
528
|
+
if (!trimmedRef || !cardStyles) {
|
|
529
|
+
return void 0;
|
|
530
|
+
}
|
|
531
|
+
return mergeStyleWithRef(cardStyles[trimmedRef], style);
|
|
532
|
+
}
|
|
533
|
+
function collectNestedStyleRefErrors(value, path, errors) {
|
|
534
|
+
if (Array.isArray(value)) {
|
|
535
|
+
for (let i = 0; i < value.length; i++) {
|
|
536
|
+
collectNestedStyleRefErrors(value[i], `${path}[${i}]`, errors);
|
|
537
|
+
}
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (typeof value !== "object" || value === null) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
for (const [key, child] of Object.entries(value)) {
|
|
544
|
+
const childPath = `${path}.${key}`;
|
|
545
|
+
if (key === "$style") {
|
|
546
|
+
errors.push(
|
|
547
|
+
createError(
|
|
548
|
+
"STYLE_CIRCULAR_REF",
|
|
549
|
+
`$style cannot be used inside card.styles definitions at "${childPath}"`,
|
|
550
|
+
childPath
|
|
551
|
+
)
|
|
552
|
+
);
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
collectNestedStyleRefErrors(child, childPath, errors);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverStyleRefs = true) {
|
|
528
559
|
const STRUCTURED_FIELDS = /* @__PURE__ */ new Set(["transition", "hoverStyle"]);
|
|
529
560
|
for (const key of Object.keys(style)) {
|
|
530
561
|
if (!STRUCTURED_FIELDS.has(key) && FORBIDDEN_STYLE_PROPERTIES.includes(key)) {
|
|
@@ -809,16 +840,16 @@ function validateSingleStyle(style, stylePath, errors) {
|
|
|
809
840
|
)
|
|
810
841
|
);
|
|
811
842
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
843
|
+
const mergedHoverStyle = allowHoverStyleRefs ? resolveStyleRef(hoverObj, hoverPath, cardStyles, errors) : hoverObj;
|
|
844
|
+
if (mergedHoverStyle) {
|
|
845
|
+
validateSingleStyle(
|
|
846
|
+
mergedHoverStyle,
|
|
847
|
+
hoverPath,
|
|
848
|
+
errors,
|
|
849
|
+
cardStyles,
|
|
850
|
+
allowHoverStyleRefs
|
|
819
851
|
);
|
|
820
852
|
}
|
|
821
|
-
validateSingleStyle(hoverObj, hoverPath, errors);
|
|
822
853
|
}
|
|
823
854
|
}
|
|
824
855
|
if ("transition" in style && style.transition != null) {
|
|
@@ -906,16 +937,8 @@ function validateStyles(views, cardStyles) {
|
|
|
906
937
|
)
|
|
907
938
|
);
|
|
908
939
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
createError(
|
|
912
|
-
"STYLE_CIRCULAR_REF",
|
|
913
|
-
`$style cannot be used inside card.styles definitions at "${entryPath}.$style"`,
|
|
914
|
-
`${entryPath}.$style`
|
|
915
|
-
)
|
|
916
|
-
);
|
|
917
|
-
}
|
|
918
|
-
validateSingleStyle(styleEntry, entryPath, errors);
|
|
940
|
+
collectNestedStyleRefErrors(styleEntry, entryPath, errors);
|
|
941
|
+
validateSingleStyle(styleEntry, entryPath, errors, void 0, false);
|
|
919
942
|
}
|
|
920
943
|
}
|
|
921
944
|
traverseCard(views, (node, ctx) => {
|
|
@@ -924,33 +947,9 @@ function validateStyles(views, cardStyles) {
|
|
|
924
947
|
return;
|
|
925
948
|
}
|
|
926
949
|
const stylePath = `${ctx.path}.style`;
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
if (!STYLE_NAME_PATTERN.test(trimmedRef)) {
|
|
931
|
-
errors.push(
|
|
932
|
-
createError(
|
|
933
|
-
"INVALID_STYLE_REF",
|
|
934
|
-
`$style value "${rawRef}" is invalid; must match /^[A-Za-z][A-Za-z0-9_-]*$/ at "${stylePath}.$style"`,
|
|
935
|
-
`${stylePath}.$style`
|
|
936
|
-
)
|
|
937
|
-
);
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
if (!cardStyles || !(trimmedRef in cardStyles)) {
|
|
941
|
-
errors.push(
|
|
942
|
-
createError(
|
|
943
|
-
"STYLE_REF_NOT_FOUND",
|
|
944
|
-
`$style references "${trimmedRef}" which is not defined in card.styles at "${stylePath}.$style"`,
|
|
945
|
-
`${stylePath}.$style`
|
|
946
|
-
)
|
|
947
|
-
);
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
const mergedStyle = mergeStyleWithRef(cardStyles[trimmedRef], style);
|
|
951
|
-
validateSingleStyle(mergedStyle, stylePath, errors);
|
|
952
|
-
} else {
|
|
953
|
-
validateSingleStyle(style, stylePath, errors);
|
|
950
|
+
const mergedStyle = resolveStyleRef(style, stylePath, cardStyles, errors);
|
|
951
|
+
if (mergedStyle) {
|
|
952
|
+
validateSingleStyle(mergedStyle, stylePath, errors, cardStyles);
|
|
954
953
|
}
|
|
955
954
|
});
|
|
956
955
|
return errors;
|
|
@@ -959,7 +958,7 @@ function validateStyles(views, cardStyles) {
|
|
|
959
958
|
// src/security.ts
|
|
960
959
|
import {
|
|
961
960
|
PROTOTYPE_POLLUTION_SEGMENTS,
|
|
962
|
-
isRef as
|
|
961
|
+
isRef as isRef2
|
|
963
962
|
} from "@safe-ugc-ui/types";
|
|
964
963
|
var FORBIDDEN_URL_PREFIXES = [
|
|
965
964
|
"http://",
|
|
@@ -972,7 +971,7 @@ function scanForRefs(obj, path, errors) {
|
|
|
972
971
|
if (obj === null || obj === void 0 || typeof obj !== "object") {
|
|
973
972
|
return;
|
|
974
973
|
}
|
|
975
|
-
if (
|
|
974
|
+
if (isRef2(obj)) {
|
|
976
975
|
const refStr = obj.$ref;
|
|
977
976
|
const segments = refStr.split(/[.\[]/);
|
|
978
977
|
for (const segment of segments) {
|
|
@@ -1109,12 +1108,11 @@ function validateSecurity(card) {
|
|
|
1109
1108
|
delete nodeFields.type;
|
|
1110
1109
|
delete nodeFields.style;
|
|
1111
1110
|
delete nodeFields.children;
|
|
1112
|
-
delete nodeFields.condition;
|
|
1113
1111
|
if (type === "Image" || type === "Avatar") {
|
|
1114
1112
|
const src = node.src;
|
|
1115
1113
|
if (typeof src === "string") {
|
|
1116
1114
|
checkSrcValue(src, type, `${path}.src`, errors);
|
|
1117
|
-
} else if (
|
|
1115
|
+
} else if (isRef2(src)) {
|
|
1118
1116
|
if (state) {
|
|
1119
1117
|
const resolved = resolveRefFromState(
|
|
1120
1118
|
src.$ref,
|
|
@@ -1207,8 +1205,7 @@ import {
|
|
|
1207
1205
|
MAX_OVERFLOW_AUTO_COUNT,
|
|
1208
1206
|
MAX_STACK_NESTING,
|
|
1209
1207
|
PROTOTYPE_POLLUTION_SEGMENTS as PROTOTYPE_POLLUTION_SEGMENTS2,
|
|
1210
|
-
isRef as
|
|
1211
|
-
isExpr as isExpr3
|
|
1208
|
+
isRef as isRef3
|
|
1212
1209
|
} from "@safe-ugc-ui/types";
|
|
1213
1210
|
function utf8ByteLength(str) {
|
|
1214
1211
|
let bytes = 0;
|
|
@@ -1269,7 +1266,7 @@ function countTemplateMetrics(template, cardStyles) {
|
|
|
1269
1266
|
result.nodes = 1;
|
|
1270
1267
|
if (node.type === "Text") {
|
|
1271
1268
|
const content = node.content;
|
|
1272
|
-
if (typeof content === "string" && !
|
|
1269
|
+
if (typeof content === "string" && !isRef3(content)) {
|
|
1273
1270
|
result.textBytes = utf8ByteLength(content);
|
|
1274
1271
|
}
|
|
1275
1272
|
}
|
|
@@ -1325,7 +1322,7 @@ function validateLimits(card) {
|
|
|
1325
1322
|
nodeCount++;
|
|
1326
1323
|
if (node.type === "Text") {
|
|
1327
1324
|
const content = node.content;
|
|
1328
|
-
if (typeof content === "string" && !
|
|
1325
|
+
if (typeof content === "string" && !isRef3(content)) {
|
|
1329
1326
|
textContentBytes += utf8ByteLength(content);
|
|
1330
1327
|
}
|
|
1331
1328
|
}
|
|
@@ -1460,428 +1457,6 @@ function validateLimits(card) {
|
|
|
1460
1457
|
return errors;
|
|
1461
1458
|
}
|
|
1462
1459
|
|
|
1463
|
-
// src/expr-constraints.ts
|
|
1464
|
-
import {
|
|
1465
|
-
EXPR_MAX_LENGTH,
|
|
1466
|
-
EXPR_MAX_TOKENS,
|
|
1467
|
-
EXPR_MAX_NESTING,
|
|
1468
|
-
EXPR_MAX_CONDITION_NESTING,
|
|
1469
|
-
EXPR_MAX_REF_DEPTH,
|
|
1470
|
-
EXPR_MAX_ARRAY_INDEX,
|
|
1471
|
-
EXPR_MAX_STRING_LITERAL,
|
|
1472
|
-
EXPR_MAX_FRACTIONAL_DIGITS,
|
|
1473
|
-
isRef as isRef5,
|
|
1474
|
-
isExpr as isExpr4
|
|
1475
|
-
} from "@safe-ugc-ui/types";
|
|
1476
|
-
var FORBIDDEN_KEYWORDS = [
|
|
1477
|
-
"typeof",
|
|
1478
|
-
"instanceof",
|
|
1479
|
-
"new",
|
|
1480
|
-
"delete",
|
|
1481
|
-
"function",
|
|
1482
|
-
"return",
|
|
1483
|
-
"var",
|
|
1484
|
-
"let",
|
|
1485
|
-
"const"
|
|
1486
|
-
];
|
|
1487
|
-
var FORBIDDEN_KEYWORD_SET = new Set(FORBIDDEN_KEYWORDS);
|
|
1488
|
-
var PROTOTYPE_POLLUTION_SEGMENTS3 = /* @__PURE__ */ new Set([
|
|
1489
|
-
"__proto__",
|
|
1490
|
-
"constructor",
|
|
1491
|
-
"prototype"
|
|
1492
|
-
]);
|
|
1493
|
-
function tokenize(expr, path) {
|
|
1494
|
-
const tokens = [];
|
|
1495
|
-
const errors = [];
|
|
1496
|
-
let i = 0;
|
|
1497
|
-
while (i < expr.length) {
|
|
1498
|
-
if (/\s/.test(expr[i])) {
|
|
1499
|
-
i++;
|
|
1500
|
-
continue;
|
|
1501
|
-
}
|
|
1502
|
-
if (i + 2 < expr.length) {
|
|
1503
|
-
const three = expr.slice(i, i + 3);
|
|
1504
|
-
if (three === "===" || three === "!==") {
|
|
1505
|
-
tokens.push({ type: "comparison", value: three, position: i });
|
|
1506
|
-
i += 3;
|
|
1507
|
-
continue;
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
if (i + 1 < expr.length) {
|
|
1511
|
-
const two = expr.slice(i, i + 2);
|
|
1512
|
-
if (two === "==" || two === "!=" || two === "<=" || two === ">=") {
|
|
1513
|
-
tokens.push({ type: "comparison", value: two, position: i });
|
|
1514
|
-
i += 2;
|
|
1515
|
-
continue;
|
|
1516
|
-
}
|
|
1517
|
-
if (two === "&&" || two === "||") {
|
|
1518
|
-
tokens.push({ type: "logic_keyword", value: two, position: i });
|
|
1519
|
-
i += 2;
|
|
1520
|
-
continue;
|
|
1521
|
-
}
|
|
1522
|
-
}
|
|
1523
|
-
if (expr[i] === "'" || expr[i] === '"') {
|
|
1524
|
-
const quote = expr[i];
|
|
1525
|
-
let j = i + 1;
|
|
1526
|
-
while (j < expr.length && expr[j] !== quote) {
|
|
1527
|
-
if (expr[j] === "\\" && j + 1 < expr.length) {
|
|
1528
|
-
j += 2;
|
|
1529
|
-
} else {
|
|
1530
|
-
j++;
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
const innerValue = expr.slice(i + 1, j);
|
|
1534
|
-
tokens.push({ type: "string", value: innerValue, position: i });
|
|
1535
|
-
i = j + 1;
|
|
1536
|
-
continue;
|
|
1537
|
-
}
|
|
1538
|
-
if (/[0-9]/.test(expr[i]) || expr[i] === "-" && i + 1 < expr.length && /[0-9]/.test(expr[i + 1]) && isNegativeSign(tokens)) {
|
|
1539
|
-
let j = i;
|
|
1540
|
-
if (expr[j] === "-") j++;
|
|
1541
|
-
while (j < expr.length && /[0-9]/.test(expr[j])) j++;
|
|
1542
|
-
if (j < expr.length && expr[j] === ".") {
|
|
1543
|
-
j++;
|
|
1544
|
-
while (j < expr.length && /[0-9]/.test(expr[j])) j++;
|
|
1545
|
-
}
|
|
1546
|
-
const numStr = expr.slice(i, j);
|
|
1547
|
-
const dotIdx = numStr.indexOf(".");
|
|
1548
|
-
if (dotIdx !== -1) {
|
|
1549
|
-
const fractionalPart = numStr.slice(dotIdx + 1);
|
|
1550
|
-
if (fractionalPart.length > EXPR_MAX_FRACTIONAL_DIGITS) {
|
|
1551
|
-
errors.push(
|
|
1552
|
-
createError(
|
|
1553
|
-
"EXPR_INVALID_TOKEN",
|
|
1554
|
-
`Number literal "${numStr}" at position ${i} has ${fractionalPart.length} fractional digits, maximum is ${EXPR_MAX_FRACTIONAL_DIGITS}.`,
|
|
1555
|
-
path
|
|
1556
|
-
)
|
|
1557
|
-
);
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
tokens.push({ type: "number", value: numStr, position: i });
|
|
1561
|
-
i = j;
|
|
1562
|
-
continue;
|
|
1563
|
-
}
|
|
1564
|
-
if (expr[i] === "$" || /[a-zA-Z_]/.test(expr[i])) {
|
|
1565
|
-
let j = i;
|
|
1566
|
-
if (expr[j] === "$") j++;
|
|
1567
|
-
while (j < expr.length && /[\w]/.test(expr[j])) j++;
|
|
1568
|
-
const word = expr.slice(i, j);
|
|
1569
|
-
if (word === "true" || word === "false") {
|
|
1570
|
-
tokens.push({ type: "boolean", value: word, position: i });
|
|
1571
|
-
} else if (word === "and" || word === "or" || word === "not") {
|
|
1572
|
-
tokens.push({ type: "logic_keyword", value: word, position: i });
|
|
1573
|
-
} else if (word === "if" || word === "then" || word === "else") {
|
|
1574
|
-
tokens.push({ type: "condition_keyword", value: word, position: i });
|
|
1575
|
-
} else {
|
|
1576
|
-
tokens.push({ type: "identifier", value: word, position: i });
|
|
1577
|
-
}
|
|
1578
|
-
i = j;
|
|
1579
|
-
continue;
|
|
1580
|
-
}
|
|
1581
|
-
const ch = expr[i];
|
|
1582
|
-
if ("+-*/%".includes(ch)) {
|
|
1583
|
-
tokens.push({ type: "arithmetic", value: ch, position: i });
|
|
1584
|
-
i++;
|
|
1585
|
-
continue;
|
|
1586
|
-
}
|
|
1587
|
-
if (ch === "<" || ch === ">") {
|
|
1588
|
-
tokens.push({ type: "comparison", value: ch, position: i });
|
|
1589
|
-
i++;
|
|
1590
|
-
continue;
|
|
1591
|
-
}
|
|
1592
|
-
if ("().[]".includes(ch)) {
|
|
1593
|
-
tokens.push({ type: "separator", value: ch, position: i });
|
|
1594
|
-
i++;
|
|
1595
|
-
continue;
|
|
1596
|
-
}
|
|
1597
|
-
if (ch === "!") {
|
|
1598
|
-
tokens.push({ type: "comparison", value: ch, position: i });
|
|
1599
|
-
i++;
|
|
1600
|
-
continue;
|
|
1601
|
-
}
|
|
1602
|
-
errors.push(
|
|
1603
|
-
createError(
|
|
1604
|
-
"EXPR_INVALID_TOKEN",
|
|
1605
|
-
`Unrecognized character "${ch}" at position ${i} in expression.`,
|
|
1606
|
-
path
|
|
1607
|
-
)
|
|
1608
|
-
);
|
|
1609
|
-
i++;
|
|
1610
|
-
}
|
|
1611
|
-
for (let t = 0; t < tokens.length; t++) {
|
|
1612
|
-
const tok = tokens[t];
|
|
1613
|
-
if (tok.value === "===" || tok.value === "!==" || (tok.value === "&&" || tok.value === "||")) {
|
|
1614
|
-
errors.push(
|
|
1615
|
-
createError(
|
|
1616
|
-
"EXPR_FORBIDDEN_TOKEN",
|
|
1617
|
-
`Forbidden operator "${tok.value}" at position ${tok.position}. Use "==" / "!=" or "and" / "or" instead.`,
|
|
1618
|
-
path
|
|
1619
|
-
)
|
|
1620
|
-
);
|
|
1621
|
-
}
|
|
1622
|
-
if (tok.value === "!") {
|
|
1623
|
-
errors.push(
|
|
1624
|
-
createError(
|
|
1625
|
-
"EXPR_FORBIDDEN_TOKEN",
|
|
1626
|
-
`Forbidden operator "!" at position ${tok.position}. Use "not" instead.`,
|
|
1627
|
-
path
|
|
1628
|
-
)
|
|
1629
|
-
);
|
|
1630
|
-
}
|
|
1631
|
-
if (tok.type === "identifier" && FORBIDDEN_KEYWORD_SET.has(tok.value)) {
|
|
1632
|
-
errors.push(
|
|
1633
|
-
createError(
|
|
1634
|
-
"EXPR_FORBIDDEN_TOKEN",
|
|
1635
|
-
`Forbidden keyword "${tok.value}" at position ${tok.position}.`,
|
|
1636
|
-
path
|
|
1637
|
-
)
|
|
1638
|
-
);
|
|
1639
|
-
}
|
|
1640
|
-
if (tok.type === "identifier" && !tok.value.startsWith("$") && !FORBIDDEN_KEYWORD_SET.has(tok.value)) {
|
|
1641
|
-
errors.push(
|
|
1642
|
-
createError(
|
|
1643
|
-
"EXPR_FORBIDDEN_TOKEN",
|
|
1644
|
-
`Identifier "${tok.value}" at position ${tok.position} must start with "$". Use "$${tok.value}" for variable references.`,
|
|
1645
|
-
path
|
|
1646
|
-
)
|
|
1647
|
-
);
|
|
1648
|
-
}
|
|
1649
|
-
if (tok.type === "identifier") {
|
|
1650
|
-
const next = tokens[t + 1];
|
|
1651
|
-
if (next && next.type === "separator" && next.value === "(") {
|
|
1652
|
-
errors.push(
|
|
1653
|
-
createError(
|
|
1654
|
-
"EXPR_FUNCTION_CALL",
|
|
1655
|
-
`Function call pattern detected: "${tok.value}(" at position ${tok.position}. Function calls are not allowed.`,
|
|
1656
|
-
path
|
|
1657
|
-
)
|
|
1658
|
-
);
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
return { tokens, errors };
|
|
1663
|
-
}
|
|
1664
|
-
function isNegativeSign(tokens) {
|
|
1665
|
-
if (tokens.length === 0) return true;
|
|
1666
|
-
const prev = tokens[tokens.length - 1];
|
|
1667
|
-
if (prev.type === "arithmetic" || prev.type === "comparison" || prev.type === "logic_keyword" || prev.type === "condition_keyword") {
|
|
1668
|
-
return true;
|
|
1669
|
-
}
|
|
1670
|
-
if (prev.type === "separator" && (prev.value === "(" || prev.value === "[")) {
|
|
1671
|
-
return true;
|
|
1672
|
-
}
|
|
1673
|
-
return false;
|
|
1674
|
-
}
|
|
1675
|
-
function scanForDynamicValues(obj, basePath, callback) {
|
|
1676
|
-
if (obj === null || obj === void 0 || typeof obj !== "object") {
|
|
1677
|
-
return;
|
|
1678
|
-
}
|
|
1679
|
-
if (isRef5(obj) || isExpr4(obj)) {
|
|
1680
|
-
callback(obj, basePath);
|
|
1681
|
-
return;
|
|
1682
|
-
}
|
|
1683
|
-
if (Array.isArray(obj)) {
|
|
1684
|
-
for (let i = 0; i < obj.length; i++) {
|
|
1685
|
-
scanForDynamicValues(obj[i], `${basePath}[${i}]`, callback);
|
|
1686
|
-
}
|
|
1687
|
-
} else {
|
|
1688
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
1689
|
-
scanForDynamicValues(value, `${basePath}.${key}`, callback);
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
function validateRef(refValue, path) {
|
|
1694
|
-
const errors = [];
|
|
1695
|
-
if (refValue.length > 500) {
|
|
1696
|
-
errors.push(
|
|
1697
|
-
createError(
|
|
1698
|
-
"REF_TOO_LONG",
|
|
1699
|
-
`$ref value exceeds maximum length of 500 characters (got ${refValue.length}).`,
|
|
1700
|
-
path
|
|
1701
|
-
)
|
|
1702
|
-
);
|
|
1703
|
-
}
|
|
1704
|
-
const segments = refValue.split(".");
|
|
1705
|
-
if (segments.length > EXPR_MAX_REF_DEPTH) {
|
|
1706
|
-
errors.push(
|
|
1707
|
-
createError(
|
|
1708
|
-
"EXPR_REF_DEPTH_EXCEEDED",
|
|
1709
|
-
`$ref path depth ${segments.length} exceeds maximum of ${EXPR_MAX_REF_DEPTH}.`,
|
|
1710
|
-
path
|
|
1711
|
-
)
|
|
1712
|
-
);
|
|
1713
|
-
}
|
|
1714
|
-
const arrayIndexPattern = /\[(\d+)\]/g;
|
|
1715
|
-
let match;
|
|
1716
|
-
while ((match = arrayIndexPattern.exec(refValue)) !== null) {
|
|
1717
|
-
const index = parseInt(match[1], 10);
|
|
1718
|
-
if (index > EXPR_MAX_ARRAY_INDEX) {
|
|
1719
|
-
errors.push(
|
|
1720
|
-
createError(
|
|
1721
|
-
"EXPR_ARRAY_INDEX_EXCEEDED",
|
|
1722
|
-
`Array index ${index} in $ref exceeds maximum of ${EXPR_MAX_ARRAY_INDEX}.`,
|
|
1723
|
-
path
|
|
1724
|
-
)
|
|
1725
|
-
);
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
for (const segment of segments) {
|
|
1729
|
-
const cleanSegment = segment.replace(/\[\d+\]/g, "");
|
|
1730
|
-
if (PROTOTYPE_POLLUTION_SEGMENTS3.has(cleanSegment)) {
|
|
1731
|
-
errors.push(
|
|
1732
|
-
createError(
|
|
1733
|
-
"PROTOTYPE_POLLUTION",
|
|
1734
|
-
`$ref path contains forbidden segment "${cleanSegment}".`,
|
|
1735
|
-
path
|
|
1736
|
-
)
|
|
1737
|
-
);
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
return errors;
|
|
1741
|
-
}
|
|
1742
|
-
function validateExpr(exprValue, path) {
|
|
1743
|
-
const errors = [];
|
|
1744
|
-
if (exprValue.length > EXPR_MAX_LENGTH) {
|
|
1745
|
-
errors.push(
|
|
1746
|
-
createError(
|
|
1747
|
-
"EXPR_TOO_LONG",
|
|
1748
|
-
`Expression exceeds maximum length of ${EXPR_MAX_LENGTH} characters (got ${exprValue.length}).`,
|
|
1749
|
-
path
|
|
1750
|
-
)
|
|
1751
|
-
);
|
|
1752
|
-
}
|
|
1753
|
-
const { tokens, errors: tokenErrors } = tokenize(exprValue, path);
|
|
1754
|
-
errors.push(...tokenErrors);
|
|
1755
|
-
if (tokens.length > EXPR_MAX_TOKENS) {
|
|
1756
|
-
errors.push(
|
|
1757
|
-
createError(
|
|
1758
|
-
"EXPR_TOO_MANY_TOKENS",
|
|
1759
|
-
`Expression has ${tokens.length} tokens, exceeding maximum of ${EXPR_MAX_TOKENS}.`,
|
|
1760
|
-
path
|
|
1761
|
-
)
|
|
1762
|
-
);
|
|
1763
|
-
}
|
|
1764
|
-
for (const tok of tokens) {
|
|
1765
|
-
if (tok.type === "string" && tok.value.length > EXPR_MAX_STRING_LITERAL) {
|
|
1766
|
-
errors.push(
|
|
1767
|
-
createError(
|
|
1768
|
-
"EXPR_STRING_LITERAL_TOO_LONG",
|
|
1769
|
-
`String literal at position ${tok.position} has ${tok.value.length} characters, exceeding maximum of ${EXPR_MAX_STRING_LITERAL}.`,
|
|
1770
|
-
path
|
|
1771
|
-
)
|
|
1772
|
-
);
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
let parenDepth = 0;
|
|
1776
|
-
let maxParenDepth = 0;
|
|
1777
|
-
let ifCount = 0;
|
|
1778
|
-
for (const tok of tokens) {
|
|
1779
|
-
if (tok.type === "separator" && tok.value === "(") {
|
|
1780
|
-
parenDepth++;
|
|
1781
|
-
if (parenDepth > maxParenDepth) {
|
|
1782
|
-
maxParenDepth = parenDepth;
|
|
1783
|
-
}
|
|
1784
|
-
} else if (tok.type === "separator" && tok.value === ")") {
|
|
1785
|
-
parenDepth--;
|
|
1786
|
-
} else if (tok.type === "condition_keyword" && tok.value === "if") {
|
|
1787
|
-
ifCount++;
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
if (maxParenDepth > EXPR_MAX_NESTING) {
|
|
1791
|
-
errors.push(
|
|
1792
|
-
createError(
|
|
1793
|
-
"EXPR_NESTING_TOO_DEEP",
|
|
1794
|
-
`Expression parenthesis nesting depth ${maxParenDepth} exceeds maximum of ${EXPR_MAX_NESTING}.`,
|
|
1795
|
-
path
|
|
1796
|
-
)
|
|
1797
|
-
);
|
|
1798
|
-
}
|
|
1799
|
-
if (ifCount > EXPR_MAX_CONDITION_NESTING) {
|
|
1800
|
-
errors.push(
|
|
1801
|
-
createError(
|
|
1802
|
-
"EXPR_CONDITION_NESTING_TOO_DEEP",
|
|
1803
|
-
`Expression has ${ifCount} nested if-conditions, exceeding maximum of ${EXPR_MAX_CONDITION_NESTING}.`,
|
|
1804
|
-
path
|
|
1805
|
-
)
|
|
1806
|
-
);
|
|
1807
|
-
}
|
|
1808
|
-
for (let t = 0; t < tokens.length; t++) {
|
|
1809
|
-
const tok = tokens[t];
|
|
1810
|
-
if (tok.type === "identifier" && tok.value.startsWith("$")) {
|
|
1811
|
-
let depth = 1;
|
|
1812
|
-
let j = t + 1;
|
|
1813
|
-
while (j + 1 < tokens.length) {
|
|
1814
|
-
if (tokens[j].type === "separator" && tokens[j].value === "." && tokens[j + 1].type === "identifier") {
|
|
1815
|
-
depth++;
|
|
1816
|
-
j += 2;
|
|
1817
|
-
} else {
|
|
1818
|
-
break;
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
if (depth > EXPR_MAX_REF_DEPTH) {
|
|
1822
|
-
errors.push(
|
|
1823
|
-
createError(
|
|
1824
|
-
"EXPR_REF_DEPTH_EXCEEDED",
|
|
1825
|
-
`Variable reference "${tok.value}" has path depth ${depth}, exceeding maximum of ${EXPR_MAX_REF_DEPTH}.`,
|
|
1826
|
-
path
|
|
1827
|
-
)
|
|
1828
|
-
);
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
for (let t = 0; t + 2 < tokens.length; t++) {
|
|
1833
|
-
if (tokens[t].type === "separator" && tokens[t].value === "[" && tokens[t + 1].type === "number" && tokens[t + 2].type === "separator" && tokens[t + 2].value === "]") {
|
|
1834
|
-
const indexValue = parseFloat(tokens[t + 1].value);
|
|
1835
|
-
if (indexValue > EXPR_MAX_ARRAY_INDEX) {
|
|
1836
|
-
errors.push(
|
|
1837
|
-
createError(
|
|
1838
|
-
"EXPR_ARRAY_INDEX_EXCEEDED",
|
|
1839
|
-
`Array index ${indexValue} in expression exceeds maximum of ${EXPR_MAX_ARRAY_INDEX}.`,
|
|
1840
|
-
path
|
|
1841
|
-
)
|
|
1842
|
-
);
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
return errors;
|
|
1847
|
-
}
|
|
1848
|
-
function validateExprConstraints(views) {
|
|
1849
|
-
const errors = [];
|
|
1850
|
-
traverseCard(views, (node, context) => {
|
|
1851
|
-
const nodeFields = { ...node };
|
|
1852
|
-
delete nodeFields.type;
|
|
1853
|
-
delete nodeFields.style;
|
|
1854
|
-
delete nodeFields.children;
|
|
1855
|
-
delete nodeFields.condition;
|
|
1856
|
-
scanForDynamicValues(nodeFields, context.path, (value, valuePath) => {
|
|
1857
|
-
if (isRef5(value)) {
|
|
1858
|
-
errors.push(...validateRef(value.$ref, valuePath));
|
|
1859
|
-
} else if (isExpr4(value)) {
|
|
1860
|
-
errors.push(...validateExpr(value.$expr, valuePath));
|
|
1861
|
-
}
|
|
1862
|
-
});
|
|
1863
|
-
if (node.style) {
|
|
1864
|
-
scanForDynamicValues(node.style, `${context.path}.style`, (value, valuePath) => {
|
|
1865
|
-
if (isRef5(value)) {
|
|
1866
|
-
errors.push(...validateRef(value.$ref, valuePath));
|
|
1867
|
-
} else if (isExpr4(value)) {
|
|
1868
|
-
errors.push(...validateExpr(value.$expr, valuePath));
|
|
1869
|
-
}
|
|
1870
|
-
});
|
|
1871
|
-
}
|
|
1872
|
-
if (node.condition !== void 0) {
|
|
1873
|
-
scanForDynamicValues(node.condition, `${context.path}.condition`, (value, valuePath) => {
|
|
1874
|
-
if (isRef5(value)) {
|
|
1875
|
-
errors.push(...validateRef(value.$ref, valuePath));
|
|
1876
|
-
} else if (isExpr4(value)) {
|
|
1877
|
-
errors.push(...validateExpr(value.$expr, valuePath));
|
|
1878
|
-
}
|
|
1879
|
-
});
|
|
1880
|
-
}
|
|
1881
|
-
});
|
|
1882
|
-
return errors;
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
1460
|
// src/index.ts
|
|
1886
1461
|
function utf8ByteLength2(str) {
|
|
1887
1462
|
let bytes = 0;
|
|
@@ -1915,7 +1490,6 @@ function runAllChecks(input) {
|
|
|
1915
1490
|
cardStyles
|
|
1916
1491
|
}));
|
|
1917
1492
|
errors.push(...validateLimits({ state: obj.state, views, cardStyles }));
|
|
1918
|
-
errors.push(...validateExprConstraints(views));
|
|
1919
1493
|
return errors;
|
|
1920
1494
|
}
|
|
1921
1495
|
function validate(input) {
|
|
@@ -1958,7 +1532,6 @@ export {
|
|
|
1958
1532
|
traverseNode,
|
|
1959
1533
|
validResult,
|
|
1960
1534
|
validate,
|
|
1961
|
-
validateExprConstraints,
|
|
1962
1535
|
validateLimits,
|
|
1963
1536
|
validateNodes,
|
|
1964
1537
|
validateRaw,
|