@safe-ugc-ui/validator 0.3.1 → 0.5.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/dist/index.d.ts +22 -44
- package/dist/index.js +154 -527
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/LICENSE +0 -21
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",
|
|
@@ -282,45 +285,8 @@ var STATIC_ONLY_STYLE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
|
282
285
|
"borderLeft",
|
|
283
286
|
// Shadow
|
|
284
287
|
"boxShadow",
|
|
285
|
-
|
|
286
|
-
"zIndex"
|
|
288
|
+
"textShadow"
|
|
287
289
|
]);
|
|
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
290
|
function validateNodeStyle(node, ctx, errors) {
|
|
325
291
|
const style = node.style;
|
|
326
292
|
if (!style) {
|
|
@@ -330,12 +296,21 @@ function validateNodeStyle(node, ctx, errors) {
|
|
|
330
296
|
if (value === void 0) {
|
|
331
297
|
continue;
|
|
332
298
|
}
|
|
333
|
-
if (
|
|
334
|
-
if (
|
|
299
|
+
if (typeof value === "object" && value !== null && "$ref" in value && typeof value.$ref === "string") {
|
|
300
|
+
if (STATIC_ONLY_STYLE_PROPERTIES.has(prop)) {
|
|
335
301
|
errors.push(
|
|
336
302
|
createError(
|
|
337
303
|
"DYNAMIC_NOT_ALLOWED",
|
|
338
|
-
`Style property "${prop}" must be a static literal; $ref
|
|
304
|
+
`Style property "${prop}" must be a static literal; $ref is not allowed.`,
|
|
305
|
+
`${ctx.path}.style.${prop}`
|
|
306
|
+
)
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
if (STRUCTURED_OBJECT_STYLE_PROPERTIES.has(prop)) {
|
|
310
|
+
errors.push(
|
|
311
|
+
createError(
|
|
312
|
+
"DYNAMIC_NOT_ALLOWED",
|
|
313
|
+
`Style property "${prop}" must be an object literal; use $ref only inside its nested fields.`,
|
|
339
314
|
`${ctx.path}.style.${prop}`
|
|
340
315
|
)
|
|
341
316
|
);
|
|
@@ -346,7 +321,6 @@ function validateNodeStyle(node, ctx, errors) {
|
|
|
346
321
|
function validateValueTypes(views) {
|
|
347
322
|
const errors = [];
|
|
348
323
|
traverseCard(views, (node, ctx) => {
|
|
349
|
-
validateNodeFields(node, ctx, errors);
|
|
350
324
|
validateNodeStyle(node, ctx, errors);
|
|
351
325
|
});
|
|
352
326
|
return errors;
|
|
@@ -364,6 +338,8 @@ import {
|
|
|
364
338
|
TRANSFORM_TRANSLATE_MAX,
|
|
365
339
|
FONT_SIZE_MIN,
|
|
366
340
|
FONT_SIZE_MAX,
|
|
341
|
+
TEXT_SHADOW_MAX_COUNT,
|
|
342
|
+
TEXT_SHADOW_BLUR_MAX,
|
|
367
343
|
BOX_SHADOW_MAX_COUNT,
|
|
368
344
|
BOX_SHADOW_BLUR_MAX,
|
|
369
345
|
BOX_SHADOW_SPREAD_MAX,
|
|
@@ -377,8 +353,7 @@ import {
|
|
|
377
353
|
TRANSITION_DELAY_MAX,
|
|
378
354
|
TRANSITION_MAX_COUNT,
|
|
379
355
|
ALLOWED_TRANSITION_PROPERTIES,
|
|
380
|
-
isRef
|
|
381
|
-
isExpr as isExpr2
|
|
356
|
+
isRef
|
|
382
357
|
} from "@safe-ugc-ui/types";
|
|
383
358
|
var COLOR_PROPERTIES = /* @__PURE__ */ new Set(["backgroundColor", "color"]);
|
|
384
359
|
var LENGTH_PROPERTIES = /* @__PURE__ */ new Set([
|
|
@@ -434,7 +409,7 @@ function isLiteralString(value) {
|
|
|
434
409
|
return typeof value === "string";
|
|
435
410
|
}
|
|
436
411
|
function isDynamic(value) {
|
|
437
|
-
return
|
|
412
|
+
return isRef(value);
|
|
438
413
|
}
|
|
439
414
|
function isValidColor(value) {
|
|
440
415
|
const lower = value.toLowerCase();
|
|
@@ -479,7 +454,7 @@ function collectDangerousCssErrors(value, path, errors) {
|
|
|
479
454
|
}
|
|
480
455
|
return;
|
|
481
456
|
}
|
|
482
|
-
if (
|
|
457
|
+
if (isRef(value)) {
|
|
483
458
|
return;
|
|
484
459
|
}
|
|
485
460
|
if (Array.isArray(value)) {
|
|
@@ -523,8 +498,87 @@ function validateShadowObject(shadow, path, errors) {
|
|
|
523
498
|
);
|
|
524
499
|
}
|
|
525
500
|
}
|
|
501
|
+
function validateTextShadowObject(shadow, path, errors) {
|
|
502
|
+
if (isLiteralNumber(shadow.blur) && shadow.blur > TEXT_SHADOW_BLUR_MAX) {
|
|
503
|
+
errors.push(
|
|
504
|
+
createError(
|
|
505
|
+
"STYLE_VALUE_OUT_OF_RANGE",
|
|
506
|
+
`textShadow blur (${shadow.blur}) exceeds maximum of ${TEXT_SHADOW_BLUR_MAX} at "${path}.blur"`,
|
|
507
|
+
`${path}.blur`
|
|
508
|
+
)
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
if (isLiteralString(shadow.color) && !isValidColor(shadow.color)) {
|
|
512
|
+
errors.push(
|
|
513
|
+
createError(
|
|
514
|
+
"INVALID_COLOR",
|
|
515
|
+
`Invalid color "${shadow.color}" at "${path}.color"`,
|
|
516
|
+
`${path}.color`
|
|
517
|
+
)
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
526
521
|
var STYLE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
527
|
-
function
|
|
522
|
+
function reportStyleRefError(rawRef, stylePath, cardStyles, errors) {
|
|
523
|
+
const trimmedRef = rawRef.trim();
|
|
524
|
+
if (!STYLE_NAME_PATTERN.test(trimmedRef)) {
|
|
525
|
+
errors.push(
|
|
526
|
+
createError(
|
|
527
|
+
"INVALID_STYLE_REF",
|
|
528
|
+
`$style value "${rawRef}" is invalid; must match /^[A-Za-z][A-Za-z0-9_-]*$/ at "${stylePath}.$style"`,
|
|
529
|
+
`${stylePath}.$style`
|
|
530
|
+
)
|
|
531
|
+
);
|
|
532
|
+
return void 0;
|
|
533
|
+
}
|
|
534
|
+
if (!cardStyles || !(trimmedRef in cardStyles)) {
|
|
535
|
+
errors.push(
|
|
536
|
+
createError(
|
|
537
|
+
"STYLE_REF_NOT_FOUND",
|
|
538
|
+
`$style references "${trimmedRef}" which is not defined in card.styles at "${stylePath}.$style"`,
|
|
539
|
+
`${stylePath}.$style`
|
|
540
|
+
)
|
|
541
|
+
);
|
|
542
|
+
return void 0;
|
|
543
|
+
}
|
|
544
|
+
return trimmedRef;
|
|
545
|
+
}
|
|
546
|
+
function resolveStyleRef(style, stylePath, cardStyles, errors) {
|
|
547
|
+
if (typeof style.$style !== "string") {
|
|
548
|
+
return style;
|
|
549
|
+
}
|
|
550
|
+
const trimmedRef = reportStyleRefError(style.$style, stylePath, cardStyles, errors);
|
|
551
|
+
if (!trimmedRef || !cardStyles) {
|
|
552
|
+
return void 0;
|
|
553
|
+
}
|
|
554
|
+
return mergeStyleWithRef(cardStyles[trimmedRef], style);
|
|
555
|
+
}
|
|
556
|
+
function collectNestedStyleRefErrors(value, path, errors) {
|
|
557
|
+
if (Array.isArray(value)) {
|
|
558
|
+
for (let i = 0; i < value.length; i++) {
|
|
559
|
+
collectNestedStyleRefErrors(value[i], `${path}[${i}]`, errors);
|
|
560
|
+
}
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (typeof value !== "object" || value === null) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
for (const [key, child] of Object.entries(value)) {
|
|
567
|
+
const childPath = `${path}.${key}`;
|
|
568
|
+
if (key === "$style") {
|
|
569
|
+
errors.push(
|
|
570
|
+
createError(
|
|
571
|
+
"STYLE_CIRCULAR_REF",
|
|
572
|
+
`$style cannot be used inside card.styles definitions at "${childPath}"`,
|
|
573
|
+
childPath
|
|
574
|
+
)
|
|
575
|
+
);
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
collectNestedStyleRefErrors(child, childPath, errors);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function validateSingleStyle(style, stylePath, errors, cardStyles, allowHoverStyleRefs = true) {
|
|
528
582
|
const STRUCTURED_FIELDS = /* @__PURE__ */ new Set(["transition", "hoverStyle"]);
|
|
529
583
|
for (const key of Object.keys(style)) {
|
|
530
584
|
if (!STRUCTURED_FIELDS.has(key) && FORBIDDEN_STYLE_PROPERTIES.includes(key)) {
|
|
@@ -685,6 +739,37 @@ function validateSingleStyle(style, stylePath, errors) {
|
|
|
685
739
|
);
|
|
686
740
|
}
|
|
687
741
|
}
|
|
742
|
+
if ("textShadow" in style && style.textShadow != null) {
|
|
743
|
+
const textShadow = style.textShadow;
|
|
744
|
+
const textShadowPath = `${stylePath}.textShadow`;
|
|
745
|
+
if (Array.isArray(textShadow)) {
|
|
746
|
+
if (textShadow.length > TEXT_SHADOW_MAX_COUNT) {
|
|
747
|
+
errors.push(
|
|
748
|
+
createError(
|
|
749
|
+
"STYLE_VALUE_OUT_OF_RANGE",
|
|
750
|
+
`textShadow has ${textShadow.length} entries, maximum is ${TEXT_SHADOW_MAX_COUNT} at "${textShadowPath}"`,
|
|
751
|
+
textShadowPath
|
|
752
|
+
)
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
for (let i = 0; i < textShadow.length; i++) {
|
|
756
|
+
const shadow = textShadow[i];
|
|
757
|
+
if (typeof shadow === "object" && shadow !== null) {
|
|
758
|
+
validateTextShadowObject(
|
|
759
|
+
shadow,
|
|
760
|
+
`${textShadowPath}[${i}]`,
|
|
761
|
+
errors
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
} else if (typeof textShadow === "object" && textShadow !== null) {
|
|
766
|
+
validateTextShadowObject(
|
|
767
|
+
textShadow,
|
|
768
|
+
textShadowPath,
|
|
769
|
+
errors
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
688
773
|
for (const [key, value] of Object.entries(style)) {
|
|
689
774
|
collectDangerousCssErrors(value, `${stylePath}.${key}`, errors);
|
|
690
775
|
}
|
|
@@ -809,16 +894,16 @@ function validateSingleStyle(style, stylePath, errors) {
|
|
|
809
894
|
)
|
|
810
895
|
);
|
|
811
896
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
897
|
+
const mergedHoverStyle = allowHoverStyleRefs ? resolveStyleRef(hoverObj, hoverPath, cardStyles, errors) : hoverObj;
|
|
898
|
+
if (mergedHoverStyle) {
|
|
899
|
+
validateSingleStyle(
|
|
900
|
+
mergedHoverStyle,
|
|
901
|
+
hoverPath,
|
|
902
|
+
errors,
|
|
903
|
+
cardStyles,
|
|
904
|
+
allowHoverStyleRefs
|
|
819
905
|
);
|
|
820
906
|
}
|
|
821
|
-
validateSingleStyle(hoverObj, hoverPath, errors);
|
|
822
907
|
}
|
|
823
908
|
}
|
|
824
909
|
if ("transition" in style && style.transition != null) {
|
|
@@ -906,16 +991,8 @@ function validateStyles(views, cardStyles) {
|
|
|
906
991
|
)
|
|
907
992
|
);
|
|
908
993
|
}
|
|
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);
|
|
994
|
+
collectNestedStyleRefErrors(styleEntry, entryPath, errors);
|
|
995
|
+
validateSingleStyle(styleEntry, entryPath, errors, void 0, false);
|
|
919
996
|
}
|
|
920
997
|
}
|
|
921
998
|
traverseCard(views, (node, ctx) => {
|
|
@@ -924,33 +1001,9 @@ function validateStyles(views, cardStyles) {
|
|
|
924
1001
|
return;
|
|
925
1002
|
}
|
|
926
1003
|
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);
|
|
1004
|
+
const mergedStyle = resolveStyleRef(style, stylePath, cardStyles, errors);
|
|
1005
|
+
if (mergedStyle) {
|
|
1006
|
+
validateSingleStyle(mergedStyle, stylePath, errors, cardStyles);
|
|
954
1007
|
}
|
|
955
1008
|
});
|
|
956
1009
|
return errors;
|
|
@@ -959,7 +1012,7 @@ function validateStyles(views, cardStyles) {
|
|
|
959
1012
|
// src/security.ts
|
|
960
1013
|
import {
|
|
961
1014
|
PROTOTYPE_POLLUTION_SEGMENTS,
|
|
962
|
-
isRef as
|
|
1015
|
+
isRef as isRef2
|
|
963
1016
|
} from "@safe-ugc-ui/types";
|
|
964
1017
|
var FORBIDDEN_URL_PREFIXES = [
|
|
965
1018
|
"http://",
|
|
@@ -972,7 +1025,7 @@ function scanForRefs(obj, path, errors) {
|
|
|
972
1025
|
if (obj === null || obj === void 0 || typeof obj !== "object") {
|
|
973
1026
|
return;
|
|
974
1027
|
}
|
|
975
|
-
if (
|
|
1028
|
+
if (isRef2(obj)) {
|
|
976
1029
|
const refStr = obj.$ref;
|
|
977
1030
|
const segments = refStr.split(/[.\[]/);
|
|
978
1031
|
for (const segment of segments) {
|
|
@@ -1109,12 +1162,11 @@ function validateSecurity(card) {
|
|
|
1109
1162
|
delete nodeFields.type;
|
|
1110
1163
|
delete nodeFields.style;
|
|
1111
1164
|
delete nodeFields.children;
|
|
1112
|
-
delete nodeFields.condition;
|
|
1113
1165
|
if (type === "Image" || type === "Avatar") {
|
|
1114
1166
|
const src = node.src;
|
|
1115
1167
|
if (typeof src === "string") {
|
|
1116
1168
|
checkSrcValue(src, type, `${path}.src`, errors);
|
|
1117
|
-
} else if (
|
|
1169
|
+
} else if (isRef2(src)) {
|
|
1118
1170
|
if (state) {
|
|
1119
1171
|
const resolved = resolveRefFromState(
|
|
1120
1172
|
src.$ref,
|
|
@@ -1207,8 +1259,7 @@ import {
|
|
|
1207
1259
|
MAX_OVERFLOW_AUTO_COUNT,
|
|
1208
1260
|
MAX_STACK_NESTING,
|
|
1209
1261
|
PROTOTYPE_POLLUTION_SEGMENTS as PROTOTYPE_POLLUTION_SEGMENTS2,
|
|
1210
|
-
isRef as
|
|
1211
|
-
isExpr as isExpr3
|
|
1262
|
+
isRef as isRef3
|
|
1212
1263
|
} from "@safe-ugc-ui/types";
|
|
1213
1264
|
function utf8ByteLength(str) {
|
|
1214
1265
|
let bytes = 0;
|
|
@@ -1269,7 +1320,7 @@ function countTemplateMetrics(template, cardStyles) {
|
|
|
1269
1320
|
result.nodes = 1;
|
|
1270
1321
|
if (node.type === "Text") {
|
|
1271
1322
|
const content = node.content;
|
|
1272
|
-
if (typeof content === "string" && !
|
|
1323
|
+
if (typeof content === "string" && !isRef3(content)) {
|
|
1273
1324
|
result.textBytes = utf8ByteLength(content);
|
|
1274
1325
|
}
|
|
1275
1326
|
}
|
|
@@ -1325,7 +1376,7 @@ function validateLimits(card) {
|
|
|
1325
1376
|
nodeCount++;
|
|
1326
1377
|
if (node.type === "Text") {
|
|
1327
1378
|
const content = node.content;
|
|
1328
|
-
if (typeof content === "string" && !
|
|
1379
|
+
if (typeof content === "string" && !isRef3(content)) {
|
|
1329
1380
|
textContentBytes += utf8ByteLength(content);
|
|
1330
1381
|
}
|
|
1331
1382
|
}
|
|
@@ -1460,428 +1511,6 @@ function validateLimits(card) {
|
|
|
1460
1511
|
return errors;
|
|
1461
1512
|
}
|
|
1462
1513
|
|
|
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
1514
|
// src/index.ts
|
|
1886
1515
|
function utf8ByteLength2(str) {
|
|
1887
1516
|
let bytes = 0;
|
|
@@ -1915,7 +1544,6 @@ function runAllChecks(input) {
|
|
|
1915
1544
|
cardStyles
|
|
1916
1545
|
}));
|
|
1917
1546
|
errors.push(...validateLimits({ state: obj.state, views, cardStyles }));
|
|
1918
|
-
errors.push(...validateExprConstraints(views));
|
|
1919
1547
|
return errors;
|
|
1920
1548
|
}
|
|
1921
1549
|
function validate(input) {
|
|
@@ -1958,7 +1586,6 @@ export {
|
|
|
1958
1586
|
traverseNode,
|
|
1959
1587
|
validResult,
|
|
1960
1588
|
validate,
|
|
1961
|
-
validateExprConstraints,
|
|
1962
1589
|
validateLimits,
|
|
1963
1590
|
validateNodes,
|
|
1964
1591
|
validateRaw,
|