@safe-ugc-ui/validator 0.1.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 +385 -0
- package/dist/index.js +1870 -0
- package/dist/index.js.map +1 -0
- package/package.json +31 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1870 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { CARD_JSON_MAX_BYTES } from "@safe-ugc-ui/types";
|
|
3
|
+
|
|
4
|
+
// src/result.ts
|
|
5
|
+
function createError(code, message, path) {
|
|
6
|
+
return { code, message, path };
|
|
7
|
+
}
|
|
8
|
+
function validResult() {
|
|
9
|
+
return { valid: true, errors: [] };
|
|
10
|
+
}
|
|
11
|
+
function invalidResult(errors) {
|
|
12
|
+
return { valid: false, errors };
|
|
13
|
+
}
|
|
14
|
+
function toResult(errors) {
|
|
15
|
+
return {
|
|
16
|
+
valid: errors.length === 0,
|
|
17
|
+
errors
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function merge(...results) {
|
|
21
|
+
const errors = [];
|
|
22
|
+
for (const r of results) {
|
|
23
|
+
errors.push(...r.errors);
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
valid: errors.length === 0,
|
|
27
|
+
errors
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/schema.ts
|
|
32
|
+
import { ugcCardSchema } from "@safe-ugc-ui/types";
|
|
33
|
+
function validateSchema(input) {
|
|
34
|
+
const errors = [];
|
|
35
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
36
|
+
errors.push(
|
|
37
|
+
createError("SCHEMA_ERROR", "Card must be a plain object.", "")
|
|
38
|
+
);
|
|
39
|
+
return toResult(errors);
|
|
40
|
+
}
|
|
41
|
+
const obj = input;
|
|
42
|
+
if (!obj.meta) {
|
|
43
|
+
errors.push(
|
|
44
|
+
createError("MISSING_FIELD", 'Card is missing required field "meta".', "meta")
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (!obj.views) {
|
|
48
|
+
errors.push(
|
|
49
|
+
createError("MISSING_FIELD", 'Card is missing required field "views".', "views")
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (errors.length > 0) {
|
|
53
|
+
return toResult(errors);
|
|
54
|
+
}
|
|
55
|
+
if (typeof obj.meta !== "object" || obj.meta === null) {
|
|
56
|
+
errors.push(
|
|
57
|
+
createError("INVALID_TYPE", '"meta" must be an object.', "meta")
|
|
58
|
+
);
|
|
59
|
+
return toResult(errors);
|
|
60
|
+
}
|
|
61
|
+
const meta = obj.meta;
|
|
62
|
+
if (typeof meta.name !== "string") {
|
|
63
|
+
errors.push(
|
|
64
|
+
createError("MISSING_FIELD", '"meta.name" is required and must be a string.', "meta.name")
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (typeof meta.version !== "string") {
|
|
68
|
+
errors.push(
|
|
69
|
+
createError("MISSING_FIELD", '"meta.version" is required and must be a string.', "meta.version")
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (typeof obj.views !== "object" || obj.views === null || Array.isArray(obj.views)) {
|
|
73
|
+
errors.push(
|
|
74
|
+
createError("INVALID_TYPE", '"views" must be an object.', "views")
|
|
75
|
+
);
|
|
76
|
+
return toResult(errors);
|
|
77
|
+
}
|
|
78
|
+
const views = obj.views;
|
|
79
|
+
if (Object.keys(views).length === 0) {
|
|
80
|
+
errors.push(
|
|
81
|
+
createError("MISSING_FIELD", '"views" must contain at least one view.', "views")
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (errors.length > 0) {
|
|
85
|
+
return toResult(errors);
|
|
86
|
+
}
|
|
87
|
+
const result = ugcCardSchema.safeParse(input);
|
|
88
|
+
if (!result.success) {
|
|
89
|
+
for (const issue of result.error.issues) {
|
|
90
|
+
const path = issue.path.join(".");
|
|
91
|
+
errors.push(
|
|
92
|
+
createError("SCHEMA_ERROR", issue.message, path)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return toResult(errors);
|
|
97
|
+
}
|
|
98
|
+
function parseCard(input) {
|
|
99
|
+
const result = ugcCardSchema.safeParse(input);
|
|
100
|
+
return result.success ? result.data : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/node-validator.ts
|
|
104
|
+
import { ALL_COMPONENT_TYPES } from "@safe-ugc-ui/types";
|
|
105
|
+
|
|
106
|
+
// src/traverse.ts
|
|
107
|
+
function isForLoop(children) {
|
|
108
|
+
return typeof children === "object" && children !== null && "for" in children && "in" in children && "template" in children;
|
|
109
|
+
}
|
|
110
|
+
function hasOverflowAuto(style) {
|
|
111
|
+
return style?.overflow === "auto";
|
|
112
|
+
}
|
|
113
|
+
function traverseNode(node, context, visitor) {
|
|
114
|
+
const result = visitor(node, context);
|
|
115
|
+
if (result === false) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const children = node.children;
|
|
119
|
+
if (children == null) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const nextStackDepth = node.type === "Stack" ? context.stackDepth + 1 : context.stackDepth;
|
|
123
|
+
const nextOverflowAuto = context.overflowAutoAncestor || hasOverflowAuto(node.style);
|
|
124
|
+
if (isForLoop(children)) {
|
|
125
|
+
const childCtx = {
|
|
126
|
+
path: `${context.path}.children.template`,
|
|
127
|
+
depth: context.depth + 1,
|
|
128
|
+
parentType: node.type,
|
|
129
|
+
loopDepth: context.loopDepth + 1,
|
|
130
|
+
overflowAutoAncestor: nextOverflowAuto,
|
|
131
|
+
stackDepth: nextStackDepth
|
|
132
|
+
};
|
|
133
|
+
traverseNode(children.template, childCtx, visitor);
|
|
134
|
+
} else if (Array.isArray(children)) {
|
|
135
|
+
for (let i = 0; i < children.length; i++) {
|
|
136
|
+
const child = children[i];
|
|
137
|
+
if (child && typeof child === "object" && "type" in child) {
|
|
138
|
+
const childCtx = {
|
|
139
|
+
path: `${context.path}.children[${i}]`,
|
|
140
|
+
depth: context.depth + 1,
|
|
141
|
+
parentType: node.type,
|
|
142
|
+
loopDepth: context.loopDepth,
|
|
143
|
+
overflowAutoAncestor: nextOverflowAuto,
|
|
144
|
+
stackDepth: nextStackDepth
|
|
145
|
+
};
|
|
146
|
+
traverseNode(child, childCtx, visitor);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function traverseCard(views, visitor) {
|
|
152
|
+
for (const [viewName, rootNode] of Object.entries(views)) {
|
|
153
|
+
if (rootNode == null || typeof rootNode !== "object" || !("type" in rootNode)) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const context = {
|
|
157
|
+
path: `views.${viewName}`,
|
|
158
|
+
depth: 0,
|
|
159
|
+
parentType: null,
|
|
160
|
+
loopDepth: 0,
|
|
161
|
+
overflowAutoAncestor: false,
|
|
162
|
+
stackDepth: 0
|
|
163
|
+
};
|
|
164
|
+
traverseNode(rootNode, context, visitor);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/node-validator.ts
|
|
169
|
+
var REQUIRED_FIELDS = {
|
|
170
|
+
Text: ["content"],
|
|
171
|
+
Image: ["src"],
|
|
172
|
+
ProgressBar: ["value", "max"],
|
|
173
|
+
Avatar: ["src"],
|
|
174
|
+
Icon: ["name"],
|
|
175
|
+
Badge: ["label"],
|
|
176
|
+
Chip: ["label"],
|
|
177
|
+
Button: ["label", "action"],
|
|
178
|
+
Toggle: ["value", "onToggle"]
|
|
179
|
+
};
|
|
180
|
+
var KNOWN_TYPES = new Set(ALL_COMPONENT_TYPES);
|
|
181
|
+
function looksLikeForLoop(value) {
|
|
182
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && "for" in value && "in" in value && "template" in value;
|
|
183
|
+
}
|
|
184
|
+
function validateForLoop(children, path) {
|
|
185
|
+
const errors = [];
|
|
186
|
+
if (typeof children["for"] !== "string") {
|
|
187
|
+
errors.push(
|
|
188
|
+
createError(
|
|
189
|
+
"INVALID_VALUE",
|
|
190
|
+
'ForLoop "for" must be a string.',
|
|
191
|
+
`${path}.children.for`
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const inValue = children["in"];
|
|
196
|
+
if (typeof inValue !== "string" || !inValue.startsWith("$")) {
|
|
197
|
+
errors.push(
|
|
198
|
+
createError(
|
|
199
|
+
"INVALID_VALUE",
|
|
200
|
+
'ForLoop "in" must be a string starting with "$".',
|
|
201
|
+
`${path}.children.in`
|
|
202
|
+
)
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
const template = children["template"];
|
|
206
|
+
if (typeof template !== "object" || template === null || !("type" in template)) {
|
|
207
|
+
errors.push(
|
|
208
|
+
createError(
|
|
209
|
+
"INVALID_VALUE",
|
|
210
|
+
'ForLoop "template" must be an object with a "type" property.',
|
|
211
|
+
`${path}.children.template`
|
|
212
|
+
)
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return errors;
|
|
216
|
+
}
|
|
217
|
+
function validateNode(node, context) {
|
|
218
|
+
const errors = [];
|
|
219
|
+
const { path } = context;
|
|
220
|
+
if (!KNOWN_TYPES.has(node.type)) {
|
|
221
|
+
errors.push(
|
|
222
|
+
createError(
|
|
223
|
+
"UNKNOWN_NODE_TYPE",
|
|
224
|
+
`Unknown node type "${node.type}".`,
|
|
225
|
+
path
|
|
226
|
+
)
|
|
227
|
+
);
|
|
228
|
+
return errors;
|
|
229
|
+
}
|
|
230
|
+
const requiredFields = REQUIRED_FIELDS[node.type];
|
|
231
|
+
if (requiredFields && requiredFields.length > 0) {
|
|
232
|
+
for (const field of requiredFields) {
|
|
233
|
+
if (!(field in node) || node[field] === void 0) {
|
|
234
|
+
errors.push(
|
|
235
|
+
createError(
|
|
236
|
+
"MISSING_FIELD",
|
|
237
|
+
`"${node.type}" node is missing required field "${field}".`,
|
|
238
|
+
`${path}.${field}`
|
|
239
|
+
)
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (node.children != null && looksLikeForLoop(node.children)) {
|
|
245
|
+
errors.push(
|
|
246
|
+
...validateForLoop(
|
|
247
|
+
node.children,
|
|
248
|
+
path
|
|
249
|
+
)
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return errors;
|
|
253
|
+
}
|
|
254
|
+
function validateNodes(views) {
|
|
255
|
+
const errors = [];
|
|
256
|
+
traverseCard(views, (node, context) => {
|
|
257
|
+
errors.push(...validateNode(node, context));
|
|
258
|
+
});
|
|
259
|
+
return errors;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/value-types.ts
|
|
263
|
+
import { isRef, isExpr } from "@safe-ugc-ui/types";
|
|
264
|
+
var STATIC_ONLY_STYLE_PROPERTIES = /* @__PURE__ */ new Set([
|
|
265
|
+
// Position / layout
|
|
266
|
+
"position",
|
|
267
|
+
"top",
|
|
268
|
+
"right",
|
|
269
|
+
"bottom",
|
|
270
|
+
"left",
|
|
271
|
+
// Transform
|
|
272
|
+
"transform",
|
|
273
|
+
// Gradient
|
|
274
|
+
"backgroundGradient",
|
|
275
|
+
// Overflow
|
|
276
|
+
"overflow",
|
|
277
|
+
// Borders
|
|
278
|
+
"border",
|
|
279
|
+
"borderTop",
|
|
280
|
+
"borderRight",
|
|
281
|
+
"borderBottom",
|
|
282
|
+
"borderLeft",
|
|
283
|
+
// Shadow
|
|
284
|
+
"boxShadow",
|
|
285
|
+
// Stacking
|
|
286
|
+
"zIndex"
|
|
287
|
+
]);
|
|
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
|
+
function validateNodeStyle(node, ctx, errors) {
|
|
325
|
+
const style = node.style;
|
|
326
|
+
if (!style) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
for (const [prop, value] of Object.entries(style)) {
|
|
330
|
+
if (value === void 0) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (STATIC_ONLY_STYLE_PROPERTIES.has(prop)) {
|
|
334
|
+
if (isRef(value) || isExpr(value)) {
|
|
335
|
+
errors.push(
|
|
336
|
+
createError(
|
|
337
|
+
"DYNAMIC_NOT_ALLOWED",
|
|
338
|
+
`Style property "${prop}" must be a static literal; $ref and $expr are not allowed.`,
|
|
339
|
+
`${ctx.path}.style.${prop}`
|
|
340
|
+
)
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function validateValueTypes(views) {
|
|
347
|
+
const errors = [];
|
|
348
|
+
traverseCard(views, (node, ctx) => {
|
|
349
|
+
validateNodeFields(node, ctx, errors);
|
|
350
|
+
validateNodeStyle(node, ctx, errors);
|
|
351
|
+
});
|
|
352
|
+
return errors;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/style-validator.ts
|
|
356
|
+
import {
|
|
357
|
+
FORBIDDEN_STYLE_PROPERTIES,
|
|
358
|
+
DANGEROUS_CSS_FUNCTIONS,
|
|
359
|
+
ZINDEX_MIN,
|
|
360
|
+
ZINDEX_MAX,
|
|
361
|
+
TRANSFORM_SCALE_MIN,
|
|
362
|
+
TRANSFORM_SCALE_MAX,
|
|
363
|
+
TRANSFORM_TRANSLATE_MIN,
|
|
364
|
+
TRANSFORM_TRANSLATE_MAX,
|
|
365
|
+
FONT_SIZE_MIN,
|
|
366
|
+
FONT_SIZE_MAX,
|
|
367
|
+
BOX_SHADOW_MAX_COUNT,
|
|
368
|
+
BOX_SHADOW_BLUR_MAX,
|
|
369
|
+
BOX_SHADOW_SPREAD_MAX,
|
|
370
|
+
BORDER_RADIUS_MAX,
|
|
371
|
+
LETTER_SPACING_MIN,
|
|
372
|
+
LETTER_SPACING_MAX,
|
|
373
|
+
OPACITY_MIN,
|
|
374
|
+
OPACITY_MAX,
|
|
375
|
+
CSS_NAMED_COLORS,
|
|
376
|
+
isRef as isRef2,
|
|
377
|
+
isExpr as isExpr2
|
|
378
|
+
} from "@safe-ugc-ui/types";
|
|
379
|
+
var COLOR_PROPERTIES = /* @__PURE__ */ new Set(["backgroundColor", "color"]);
|
|
380
|
+
var LENGTH_PROPERTIES = /* @__PURE__ */ new Set([
|
|
381
|
+
"width",
|
|
382
|
+
"height",
|
|
383
|
+
"minWidth",
|
|
384
|
+
"maxWidth",
|
|
385
|
+
"minHeight",
|
|
386
|
+
"maxHeight",
|
|
387
|
+
"padding",
|
|
388
|
+
"paddingTop",
|
|
389
|
+
"paddingRight",
|
|
390
|
+
"paddingBottom",
|
|
391
|
+
"paddingLeft",
|
|
392
|
+
"margin",
|
|
393
|
+
"marginTop",
|
|
394
|
+
"marginRight",
|
|
395
|
+
"marginBottom",
|
|
396
|
+
"marginLeft",
|
|
397
|
+
"top",
|
|
398
|
+
"right",
|
|
399
|
+
"bottom",
|
|
400
|
+
"left",
|
|
401
|
+
"gap",
|
|
402
|
+
"lineHeight"
|
|
403
|
+
]);
|
|
404
|
+
var LENGTH_AUTO_ALLOWED = /* @__PURE__ */ new Set([
|
|
405
|
+
"width",
|
|
406
|
+
"height",
|
|
407
|
+
"minWidth",
|
|
408
|
+
"maxWidth",
|
|
409
|
+
"minHeight",
|
|
410
|
+
"maxHeight",
|
|
411
|
+
"margin",
|
|
412
|
+
"marginTop",
|
|
413
|
+
"marginRight",
|
|
414
|
+
"marginBottom",
|
|
415
|
+
"marginLeft"
|
|
416
|
+
]);
|
|
417
|
+
var RANGE_LENGTH_PROPERTIES = {
|
|
418
|
+
fontSize: { min: FONT_SIZE_MIN, max: FONT_SIZE_MAX },
|
|
419
|
+
letterSpacing: { min: LETTER_SPACING_MIN, max: LETTER_SPACING_MAX },
|
|
420
|
+
borderRadius: { min: 0, max: BORDER_RADIUS_MAX },
|
|
421
|
+
borderRadiusTopLeft: { min: 0, max: BORDER_RADIUS_MAX },
|
|
422
|
+
borderRadiusTopRight: { min: 0, max: BORDER_RADIUS_MAX },
|
|
423
|
+
borderRadiusBottomLeft: { min: 0, max: BORDER_RADIUS_MAX },
|
|
424
|
+
borderRadiusBottomRight: { min: 0, max: BORDER_RADIUS_MAX }
|
|
425
|
+
};
|
|
426
|
+
function isLiteralNumber(value) {
|
|
427
|
+
return typeof value === "number";
|
|
428
|
+
}
|
|
429
|
+
function isLiteralString(value) {
|
|
430
|
+
return typeof value === "string";
|
|
431
|
+
}
|
|
432
|
+
function isDynamic(value) {
|
|
433
|
+
return isRef2(value) || isExpr2(value);
|
|
434
|
+
}
|
|
435
|
+
function isValidColor(value) {
|
|
436
|
+
const lower = value.toLowerCase();
|
|
437
|
+
if (/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(lower)) {
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
if (lower.startsWith("rgb(") || lower.startsWith("rgba(") || lower.startsWith("hsl(") || lower.startsWith("hsla(")) {
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
if (CSS_NAMED_COLORS.has(lower)) {
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
if (lower === "transparent" || lower === "currentcolor") {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
function isValidLength(value) {
|
|
452
|
+
return /^-?[0-9]+(\.[0-9]+)?(px|%|em|rem)?$/.test(value);
|
|
453
|
+
}
|
|
454
|
+
function parseLengthValue(value) {
|
|
455
|
+
const match = value.match(/^(-?[0-9]+(\.[0-9]+)?)(px|%|em|rem)?$/);
|
|
456
|
+
if (!match) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
return Number(match[1]);
|
|
460
|
+
}
|
|
461
|
+
function collectDangerousCssErrors(value, path, errors) {
|
|
462
|
+
if (typeof value === "string") {
|
|
463
|
+
const lower = value.toLowerCase();
|
|
464
|
+
for (const fn of DANGEROUS_CSS_FUNCTIONS) {
|
|
465
|
+
if (lower.includes(fn)) {
|
|
466
|
+
errors.push(
|
|
467
|
+
createError(
|
|
468
|
+
"FORBIDDEN_CSS_FUNCTION",
|
|
469
|
+
`Style value contains forbidden CSS function "${fn.slice(0, -1)}" at "${path}"`,
|
|
470
|
+
path
|
|
471
|
+
)
|
|
472
|
+
);
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (isRef2(value) || isExpr2(value)) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (Array.isArray(value)) {
|
|
482
|
+
for (let i = 0; i < value.length; i++) {
|
|
483
|
+
collectDangerousCssErrors(value[i], `${path}[${i}]`, errors);
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (typeof value === "object" && value !== null) {
|
|
488
|
+
for (const [key, child] of Object.entries(value)) {
|
|
489
|
+
collectDangerousCssErrors(child, `${path}.${key}`, errors);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function validateShadowObject(shadow, path, errors) {
|
|
494
|
+
if (isLiteralNumber(shadow.blur) && shadow.blur > BOX_SHADOW_BLUR_MAX) {
|
|
495
|
+
errors.push(
|
|
496
|
+
createError(
|
|
497
|
+
"STYLE_VALUE_OUT_OF_RANGE",
|
|
498
|
+
`boxShadow blur (${shadow.blur}) exceeds maximum of ${BOX_SHADOW_BLUR_MAX} at "${path}.blur"`,
|
|
499
|
+
`${path}.blur`
|
|
500
|
+
)
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
if (isLiteralNumber(shadow.spread) && shadow.spread > BOX_SHADOW_SPREAD_MAX) {
|
|
504
|
+
errors.push(
|
|
505
|
+
createError(
|
|
506
|
+
"STYLE_VALUE_OUT_OF_RANGE",
|
|
507
|
+
`boxShadow spread (${shadow.spread}) exceeds maximum of ${BOX_SHADOW_SPREAD_MAX} at "${path}.spread"`,
|
|
508
|
+
`${path}.spread`
|
|
509
|
+
)
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
if (isLiteralString(shadow.color) && !isValidColor(shadow.color)) {
|
|
513
|
+
errors.push(
|
|
514
|
+
createError(
|
|
515
|
+
"INVALID_COLOR",
|
|
516
|
+
`Invalid color "${shadow.color}" at "${path}.color"`,
|
|
517
|
+
`${path}.color`
|
|
518
|
+
)
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
var STYLE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
523
|
+
function validateSingleStyle(style, stylePath, errors) {
|
|
524
|
+
for (const key of Object.keys(style)) {
|
|
525
|
+
if (FORBIDDEN_STYLE_PROPERTIES.includes(key)) {
|
|
526
|
+
errors.push(
|
|
527
|
+
createError(
|
|
528
|
+
"FORBIDDEN_STYLE_PROPERTY",
|
|
529
|
+
`Style property "${key}" is forbidden at "${stylePath}.${key}"`,
|
|
530
|
+
`${stylePath}.${key}`
|
|
531
|
+
)
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if ("zIndex" in style && isLiteralNumber(style.zIndex)) {
|
|
536
|
+
const v = style.zIndex;
|
|
537
|
+
if (v < ZINDEX_MIN || v > ZINDEX_MAX) {
|
|
538
|
+
errors.push(
|
|
539
|
+
createError(
|
|
540
|
+
"STYLE_VALUE_OUT_OF_RANGE",
|
|
541
|
+
`zIndex (${v}) must be between ${ZINDEX_MIN} and ${ZINDEX_MAX} at "${stylePath}.zIndex"`,
|
|
542
|
+
`${stylePath}.zIndex`
|
|
543
|
+
)
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if ("fontSize" in style && isLiteralNumber(style.fontSize)) {
|
|
548
|
+
const v = style.fontSize;
|
|
549
|
+
if (v < FONT_SIZE_MIN || v > FONT_SIZE_MAX) {
|
|
550
|
+
errors.push(
|
|
551
|
+
createError(
|
|
552
|
+
"STYLE_VALUE_OUT_OF_RANGE",
|
|
553
|
+
`fontSize (${v}) must be between ${FONT_SIZE_MIN} and ${FONT_SIZE_MAX} at "${stylePath}.fontSize"`,
|
|
554
|
+
`${stylePath}.fontSize`
|
|
555
|
+
)
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if ("opacity" in style && isLiteralNumber(style.opacity)) {
|
|
560
|
+
const v = style.opacity;
|
|
561
|
+
if (v < OPACITY_MIN || v > OPACITY_MAX) {
|
|
562
|
+
errors.push(
|
|
563
|
+
createError(
|
|
564
|
+
"STYLE_VALUE_OUT_OF_RANGE",
|
|
565
|
+
`opacity (${v}) must be between ${OPACITY_MIN} and ${OPACITY_MAX} at "${stylePath}.opacity"`,
|
|
566
|
+
`${stylePath}.opacity`
|
|
567
|
+
)
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if ("letterSpacing" in style && isLiteralNumber(style.letterSpacing)) {
|
|
572
|
+
const v = style.letterSpacing;
|
|
573
|
+
if (v < LETTER_SPACING_MIN || v > LETTER_SPACING_MAX) {
|
|
574
|
+
errors.push(
|
|
575
|
+
createError(
|
|
576
|
+
"STYLE_VALUE_OUT_OF_RANGE",
|
|
577
|
+
`letterSpacing (${v}) must be between ${LETTER_SPACING_MIN} and ${LETTER_SPACING_MAX} at "${stylePath}.letterSpacing"`,
|
|
578
|
+
`${stylePath}.letterSpacing`
|
|
579
|
+
)
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
for (const prop of [
|
|
584
|
+
"borderRadius",
|
|
585
|
+
"borderRadiusTopLeft",
|
|
586
|
+
"borderRadiusTopRight",
|
|
587
|
+
"borderRadiusBottomLeft",
|
|
588
|
+
"borderRadiusBottomRight"
|
|
589
|
+
]) {
|
|
590
|
+
if (prop in style && isLiteralNumber(style[prop])) {
|
|
591
|
+
const v = style[prop];
|
|
592
|
+
if (v < 0 || v > BORDER_RADIUS_MAX) {
|
|
593
|
+
errors.push(
|
|
594
|
+
createError(
|
|
595
|
+
"STYLE_VALUE_OUT_OF_RANGE",
|
|
596
|
+
`${prop} (${v}) must be between 0 and ${BORDER_RADIUS_MAX} at "${stylePath}.${prop}"`,
|
|
597
|
+
`${stylePath}.${prop}`
|
|
598
|
+
)
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if ("transform" in style && typeof style.transform === "object" && style.transform !== null && !isDynamic(style.transform)) {
|
|
604
|
+
const transform = style.transform;
|
|
605
|
+
const transformPath = `${stylePath}.transform`;
|
|
606
|
+
if ("scale" in transform && isLiteralNumber(transform.scale)) {
|
|
607
|
+
const v = transform.scale;
|
|
608
|
+
if (v < TRANSFORM_SCALE_MIN || v > TRANSFORM_SCALE_MAX) {
|
|
609
|
+
errors.push(
|
|
610
|
+
createError(
|
|
611
|
+
"STYLE_VALUE_OUT_OF_RANGE",
|
|
612
|
+
`transform.scale (${v}) must be between ${TRANSFORM_SCALE_MIN} and ${TRANSFORM_SCALE_MAX} at "${transformPath}.scale"`,
|
|
613
|
+
`${transformPath}.scale`
|
|
614
|
+
)
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if ("translateX" in transform && isLiteralNumber(transform.translateX)) {
|
|
619
|
+
const v = transform.translateX;
|
|
620
|
+
if (v < TRANSFORM_TRANSLATE_MIN || v > TRANSFORM_TRANSLATE_MAX) {
|
|
621
|
+
errors.push(
|
|
622
|
+
createError(
|
|
623
|
+
"STYLE_VALUE_OUT_OF_RANGE",
|
|
624
|
+
`transform.translateX (${v}) must be between ${TRANSFORM_TRANSLATE_MIN} and ${TRANSFORM_TRANSLATE_MAX} at "${transformPath}.translateX"`,
|
|
625
|
+
`${transformPath}.translateX`
|
|
626
|
+
)
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if ("translateY" in transform && isLiteralNumber(transform.translateY)) {
|
|
631
|
+
const v = transform.translateY;
|
|
632
|
+
if (v < TRANSFORM_TRANSLATE_MIN || v > TRANSFORM_TRANSLATE_MAX) {
|
|
633
|
+
errors.push(
|
|
634
|
+
createError(
|
|
635
|
+
"STYLE_VALUE_OUT_OF_RANGE",
|
|
636
|
+
`transform.translateY (${v}) must be between ${TRANSFORM_TRANSLATE_MIN} and ${TRANSFORM_TRANSLATE_MAX} at "${transformPath}.translateY"`,
|
|
637
|
+
`${transformPath}.translateY`
|
|
638
|
+
)
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if ("skew" in transform) {
|
|
643
|
+
errors.push(
|
|
644
|
+
createError(
|
|
645
|
+
"TRANSFORM_SKEW_FORBIDDEN",
|
|
646
|
+
`transform.skew is forbidden at "${transformPath}.skew"`,
|
|
647
|
+
`${transformPath}.skew`
|
|
648
|
+
)
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if ("boxShadow" in style && style.boxShadow != null) {
|
|
653
|
+
const boxShadow = style.boxShadow;
|
|
654
|
+
const boxShadowPath = `${stylePath}.boxShadow`;
|
|
655
|
+
if (Array.isArray(boxShadow)) {
|
|
656
|
+
if (boxShadow.length > BOX_SHADOW_MAX_COUNT) {
|
|
657
|
+
errors.push(
|
|
658
|
+
createError(
|
|
659
|
+
"STYLE_VALUE_OUT_OF_RANGE",
|
|
660
|
+
`boxShadow has ${boxShadow.length} entries, maximum is ${BOX_SHADOW_MAX_COUNT} at "${boxShadowPath}"`,
|
|
661
|
+
boxShadowPath
|
|
662
|
+
)
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
for (let i = 0; i < boxShadow.length; i++) {
|
|
666
|
+
const shadow = boxShadow[i];
|
|
667
|
+
if (typeof shadow === "object" && shadow !== null) {
|
|
668
|
+
validateShadowObject(
|
|
669
|
+
shadow,
|
|
670
|
+
`${boxShadowPath}[${i}]`,
|
|
671
|
+
errors
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
} else if (typeof boxShadow === "object" && boxShadow !== null) {
|
|
676
|
+
validateShadowObject(
|
|
677
|
+
boxShadow,
|
|
678
|
+
boxShadowPath,
|
|
679
|
+
errors
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
for (const [key, value] of Object.entries(style)) {
|
|
684
|
+
collectDangerousCssErrors(value, `${stylePath}.${key}`, errors);
|
|
685
|
+
}
|
|
686
|
+
if ("overflow" in style && style.overflow === "scroll") {
|
|
687
|
+
errors.push(
|
|
688
|
+
createError(
|
|
689
|
+
"FORBIDDEN_OVERFLOW_VALUE",
|
|
690
|
+
`overflow "scroll" is forbidden; use "visible", "hidden", or "auto" at "${stylePath}.overflow"`,
|
|
691
|
+
`${stylePath}.overflow`
|
|
692
|
+
)
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
for (const prop of COLOR_PROPERTIES) {
|
|
696
|
+
if (prop in style && isLiteralString(style[prop]) && !isDynamic(style[prop])) {
|
|
697
|
+
if (!isValidColor(style[prop])) {
|
|
698
|
+
errors.push(
|
|
699
|
+
createError(
|
|
700
|
+
"INVALID_COLOR",
|
|
701
|
+
`Invalid color "${style[prop]}" at "${stylePath}.${prop}"`,
|
|
702
|
+
`${stylePath}.${prop}`
|
|
703
|
+
)
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
for (const prop of LENGTH_PROPERTIES) {
|
|
709
|
+
if (prop in style && isLiteralString(style[prop]) && !isDynamic(style[prop])) {
|
|
710
|
+
const val = style[prop];
|
|
711
|
+
if (val === "auto") {
|
|
712
|
+
if (!LENGTH_AUTO_ALLOWED.has(prop)) {
|
|
713
|
+
errors.push(
|
|
714
|
+
createError(
|
|
715
|
+
"INVALID_LENGTH",
|
|
716
|
+
`"auto" is not allowed for "${prop}" at "${stylePath}.${prop}"`,
|
|
717
|
+
`${stylePath}.${prop}`
|
|
718
|
+
)
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
} else if (!isValidLength(val)) {
|
|
722
|
+
errors.push(
|
|
723
|
+
createError(
|
|
724
|
+
"INVALID_LENGTH",
|
|
725
|
+
`Invalid length "${val}" at "${stylePath}.${prop}"`,
|
|
726
|
+
`${stylePath}.${prop}`
|
|
727
|
+
)
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
for (const [prop, range] of Object.entries(RANGE_LENGTH_PROPERTIES)) {
|
|
733
|
+
if (prop in style && isLiteralString(style[prop]) && !isDynamic(style[prop])) {
|
|
734
|
+
const numericValue = parseLengthValue(style[prop]);
|
|
735
|
+
if (numericValue !== null) {
|
|
736
|
+
if (numericValue < range.min || numericValue > range.max) {
|
|
737
|
+
errors.push(
|
|
738
|
+
createError(
|
|
739
|
+
"STYLE_VALUE_OUT_OF_RANGE",
|
|
740
|
+
`${prop} (${style[prop]}) must be between ${range.min} and ${range.max} at "${stylePath}.${prop}"`,
|
|
741
|
+
`${stylePath}.${prop}`
|
|
742
|
+
)
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
const borderKeys = ["border", "borderTop", "borderRight", "borderBottom", "borderLeft"];
|
|
749
|
+
for (const borderKey of borderKeys) {
|
|
750
|
+
if (borderKey in style && typeof style[borderKey] === "object" && style[borderKey] !== null && !isDynamic(style[borderKey])) {
|
|
751
|
+
const border = style[borderKey];
|
|
752
|
+
const borderPath = `${stylePath}.${borderKey}`;
|
|
753
|
+
if (isLiteralString(border.color) && !isDynamic(border.color) && !isValidColor(border.color)) {
|
|
754
|
+
errors.push(
|
|
755
|
+
createError(
|
|
756
|
+
"INVALID_COLOR",
|
|
757
|
+
`Invalid color "${border.color}" at "${borderPath}.color"`,
|
|
758
|
+
`${borderPath}.color`
|
|
759
|
+
)
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if ("backgroundGradient" in style && typeof style.backgroundGradient === "object" && style.backgroundGradient !== null && !isDynamic(style.backgroundGradient)) {
|
|
765
|
+
const gradient = style.backgroundGradient;
|
|
766
|
+
const gradientPath = `${stylePath}.backgroundGradient`;
|
|
767
|
+
if (Array.isArray(gradient.stops)) {
|
|
768
|
+
for (let i = 0; i < gradient.stops.length; i++) {
|
|
769
|
+
const stop = gradient.stops[i];
|
|
770
|
+
if (typeof stop === "object" && stop !== null && !isDynamic(stop)) {
|
|
771
|
+
const stopObj = stop;
|
|
772
|
+
if (isLiteralString(stopObj.color) && !isDynamic(stopObj.color) && !isValidColor(stopObj.color)) {
|
|
773
|
+
errors.push(
|
|
774
|
+
createError(
|
|
775
|
+
"INVALID_COLOR",
|
|
776
|
+
`Invalid color "${stopObj.color}" at "${gradientPath}.stops[${i}].color"`,
|
|
777
|
+
`${gradientPath}.stops[${i}].color`
|
|
778
|
+
)
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function mergeStyleWithRef(cardStyleEntry, inlineStyle) {
|
|
787
|
+
const merged = { ...cardStyleEntry };
|
|
788
|
+
for (const [key, value] of Object.entries(inlineStyle)) {
|
|
789
|
+
if (key !== "$style") {
|
|
790
|
+
merged[key] = value;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return merged;
|
|
794
|
+
}
|
|
795
|
+
function validateStyles(views, cardStyles) {
|
|
796
|
+
const errors = [];
|
|
797
|
+
if (cardStyles) {
|
|
798
|
+
for (const [styleName, styleEntry] of Object.entries(cardStyles)) {
|
|
799
|
+
const entryPath = `styles.${styleName}`;
|
|
800
|
+
if (!STYLE_NAME_PATTERN.test(styleName)) {
|
|
801
|
+
errors.push(
|
|
802
|
+
createError(
|
|
803
|
+
"INVALID_STYLE_NAME",
|
|
804
|
+
`Style name "${styleName}" is invalid; must match /^[A-Za-z][A-Za-z0-9_-]*$/ at "${entryPath}"`,
|
|
805
|
+
entryPath
|
|
806
|
+
)
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
if ("$style" in styleEntry) {
|
|
810
|
+
errors.push(
|
|
811
|
+
createError(
|
|
812
|
+
"STYLE_CIRCULAR_REF",
|
|
813
|
+
`$style cannot be used inside card.styles definitions at "${entryPath}.$style"`,
|
|
814
|
+
`${entryPath}.$style`
|
|
815
|
+
)
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
validateSingleStyle(styleEntry, entryPath, errors);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
traverseCard(views, (node, ctx) => {
|
|
822
|
+
const style = node.style;
|
|
823
|
+
if (style == null || typeof style !== "object") {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const stylePath = `${ctx.path}.style`;
|
|
827
|
+
if ("$style" in style && typeof style.$style === "string") {
|
|
828
|
+
const rawRef = style.$style;
|
|
829
|
+
const trimmedRef = rawRef.trim();
|
|
830
|
+
if (!STYLE_NAME_PATTERN.test(trimmedRef)) {
|
|
831
|
+
errors.push(
|
|
832
|
+
createError(
|
|
833
|
+
"INVALID_STYLE_REF",
|
|
834
|
+
`$style value "${rawRef}" is invalid; must match /^[A-Za-z][A-Za-z0-9_-]*$/ at "${stylePath}.$style"`,
|
|
835
|
+
`${stylePath}.$style`
|
|
836
|
+
)
|
|
837
|
+
);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (!cardStyles || !(trimmedRef in cardStyles)) {
|
|
841
|
+
errors.push(
|
|
842
|
+
createError(
|
|
843
|
+
"STYLE_REF_NOT_FOUND",
|
|
844
|
+
`$style references "${trimmedRef}" which is not defined in card.styles at "${stylePath}.$style"`,
|
|
845
|
+
`${stylePath}.$style`
|
|
846
|
+
)
|
|
847
|
+
);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const mergedStyle = mergeStyleWithRef(cardStyles[trimmedRef], style);
|
|
851
|
+
validateSingleStyle(mergedStyle, stylePath, errors);
|
|
852
|
+
} else {
|
|
853
|
+
validateSingleStyle(style, stylePath, errors);
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
return errors;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// src/security.ts
|
|
860
|
+
import {
|
|
861
|
+
PROTOTYPE_POLLUTION_SEGMENTS,
|
|
862
|
+
isRef as isRef3
|
|
863
|
+
} from "@safe-ugc-ui/types";
|
|
864
|
+
var FORBIDDEN_URL_PREFIXES = [
|
|
865
|
+
"http://",
|
|
866
|
+
"https://",
|
|
867
|
+
"//",
|
|
868
|
+
"data:",
|
|
869
|
+
"javascript:"
|
|
870
|
+
];
|
|
871
|
+
function scanForRefs(obj, path, errors) {
|
|
872
|
+
if (obj === null || obj === void 0 || typeof obj !== "object") {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (isRef3(obj)) {
|
|
876
|
+
const refStr = obj.$ref;
|
|
877
|
+
const segments = refStr.split(/[.\[]/);
|
|
878
|
+
for (const segment of segments) {
|
|
879
|
+
const clean = segment.replace(/]$/, "");
|
|
880
|
+
if (PROTOTYPE_POLLUTION_SEGMENTS.includes(clean)) {
|
|
881
|
+
errors.push(
|
|
882
|
+
createError(
|
|
883
|
+
"PROTOTYPE_POLLUTION",
|
|
884
|
+
`$ref "${refStr}" contains forbidden prototype pollution segment "${clean}".`,
|
|
885
|
+
path
|
|
886
|
+
)
|
|
887
|
+
);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
if (Array.isArray(obj)) {
|
|
894
|
+
for (let i = 0; i < obj.length; i++) {
|
|
895
|
+
scanForRefs(obj[i], `${path}[${i}]`, errors);
|
|
896
|
+
}
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
900
|
+
scanForRefs(value, `${path}.${key}`, errors);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function isForbiddenUrl(value) {
|
|
904
|
+
const lower = value.trim().toLowerCase();
|
|
905
|
+
return FORBIDDEN_URL_PREFIXES.some((prefix) => lower.startsWith(prefix));
|
|
906
|
+
}
|
|
907
|
+
function validateAssetPath(value) {
|
|
908
|
+
if (!value.startsWith("@assets/")) {
|
|
909
|
+
return "INVALID_ASSET_PATH";
|
|
910
|
+
}
|
|
911
|
+
if (value.includes("../")) {
|
|
912
|
+
return "ASSET_PATH_TRAVERSAL";
|
|
913
|
+
}
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
function resolveRefFromState(refPath, state) {
|
|
917
|
+
const path = refPath.startsWith("$") ? refPath.slice(1) : refPath;
|
|
918
|
+
const dotSegments = path.split(".");
|
|
919
|
+
const keys = [];
|
|
920
|
+
for (const dotSeg of dotSegments) {
|
|
921
|
+
const bracketPattern = /\[(\d+)\]/g;
|
|
922
|
+
const firstBracket = dotSeg.indexOf("[");
|
|
923
|
+
const baseName = firstBracket === -1 ? dotSeg : dotSeg.slice(0, firstBracket);
|
|
924
|
+
if (baseName) {
|
|
925
|
+
keys.push(baseName);
|
|
926
|
+
}
|
|
927
|
+
let match;
|
|
928
|
+
while ((match = bracketPattern.exec(dotSeg)) !== null) {
|
|
929
|
+
keys.push(match[1]);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
for (const key of keys) {
|
|
933
|
+
if (PROTOTYPE_POLLUTION_SEGMENTS.includes(key)) {
|
|
934
|
+
return void 0;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
let current = state;
|
|
938
|
+
for (const key of keys) {
|
|
939
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
940
|
+
if (Array.isArray(current)) {
|
|
941
|
+
const index = Number(key);
|
|
942
|
+
if (!Number.isInteger(index) || index < 0) return void 0;
|
|
943
|
+
current = current[index];
|
|
944
|
+
} else {
|
|
945
|
+
current = current[key];
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return current;
|
|
949
|
+
}
|
|
950
|
+
function checkSrcValue(resolved, type, errorPath, errors) {
|
|
951
|
+
if (isForbiddenUrl(resolved)) {
|
|
952
|
+
errors.push(
|
|
953
|
+
createError(
|
|
954
|
+
"EXTERNAL_URL",
|
|
955
|
+
`External URLs are not allowed in ${type}.src. Got "${resolved}".`,
|
|
956
|
+
errorPath
|
|
957
|
+
)
|
|
958
|
+
);
|
|
959
|
+
} else {
|
|
960
|
+
const assetError = validateAssetPath(resolved);
|
|
961
|
+
if (assetError === "ASSET_PATH_TRAVERSAL") {
|
|
962
|
+
errors.push(
|
|
963
|
+
createError(
|
|
964
|
+
"ASSET_PATH_TRAVERSAL",
|
|
965
|
+
`Asset path contains path traversal ("../"). Got "${resolved}".`,
|
|
966
|
+
errorPath
|
|
967
|
+
)
|
|
968
|
+
);
|
|
969
|
+
} else if (assetError === "INVALID_ASSET_PATH") {
|
|
970
|
+
errors.push(
|
|
971
|
+
createError(
|
|
972
|
+
"INVALID_ASSET_PATH",
|
|
973
|
+
`Asset path must start with "@assets/". Got "${resolved}".`,
|
|
974
|
+
errorPath
|
|
975
|
+
)
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
function validateSecurity(card) {
|
|
981
|
+
const errors = [];
|
|
982
|
+
const { views, state, cardAssets, cardStyles } = card;
|
|
983
|
+
if (cardAssets) {
|
|
984
|
+
for (const [key, value] of Object.entries(cardAssets)) {
|
|
985
|
+
const assetError = validateAssetPath(value);
|
|
986
|
+
if (assetError === "ASSET_PATH_TRAVERSAL") {
|
|
987
|
+
errors.push(
|
|
988
|
+
createError(
|
|
989
|
+
"ASSET_PATH_TRAVERSAL",
|
|
990
|
+
`Asset path contains path traversal ("../"). Got "${value}".`,
|
|
991
|
+
`assets.${key}`
|
|
992
|
+
)
|
|
993
|
+
);
|
|
994
|
+
} else if (assetError === "INVALID_ASSET_PATH") {
|
|
995
|
+
errors.push(
|
|
996
|
+
createError(
|
|
997
|
+
"INVALID_ASSET_PATH",
|
|
998
|
+
`Asset path must start with "@assets/". Got "${value}".`,
|
|
999
|
+
`assets.${key}`
|
|
1000
|
+
)
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
traverseCard(views, (node, context) => {
|
|
1006
|
+
const { path } = context;
|
|
1007
|
+
const { style, type } = node;
|
|
1008
|
+
const nodeFields = { ...node };
|
|
1009
|
+
delete nodeFields.type;
|
|
1010
|
+
delete nodeFields.style;
|
|
1011
|
+
delete nodeFields.children;
|
|
1012
|
+
delete nodeFields.condition;
|
|
1013
|
+
if (type === "Image" || type === "Avatar") {
|
|
1014
|
+
const src = node.src;
|
|
1015
|
+
if (typeof src === "string") {
|
|
1016
|
+
checkSrcValue(src, type, `${path}.src`, errors);
|
|
1017
|
+
} else if (isRef3(src)) {
|
|
1018
|
+
if (state) {
|
|
1019
|
+
const resolved = resolveRefFromState(
|
|
1020
|
+
src.$ref,
|
|
1021
|
+
state
|
|
1022
|
+
);
|
|
1023
|
+
if (typeof resolved === "string") {
|
|
1024
|
+
checkSrcValue(resolved, type, `${path}.src`, errors);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (style) {
|
|
1030
|
+
for (const [prop, value] of Object.entries(style)) {
|
|
1031
|
+
if (typeof value === "string" && value.toLowerCase().includes("url(")) {
|
|
1032
|
+
errors.push(
|
|
1033
|
+
createError(
|
|
1034
|
+
"FORBIDDEN_CSS_FUNCTION",
|
|
1035
|
+
`CSS url() function is forbidden in style values. Found in "${prop}".`,
|
|
1036
|
+
`${path}.style.${prop}`
|
|
1037
|
+
)
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
let effectiveStyle = style;
|
|
1043
|
+
if (style && typeof style.$style === "string" && cardStyles && style.$style.trim() in cardStyles) {
|
|
1044
|
+
const refName = style.$style.trim();
|
|
1045
|
+
const merged = { ...cardStyles[refName] };
|
|
1046
|
+
for (const [key, value] of Object.entries(style)) {
|
|
1047
|
+
if (key !== "$style") {
|
|
1048
|
+
merged[key] = value;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
effectiveStyle = merged;
|
|
1052
|
+
}
|
|
1053
|
+
if (effectiveStyle && typeof effectiveStyle.position === "string") {
|
|
1054
|
+
const position = effectiveStyle.position;
|
|
1055
|
+
if (position === "fixed") {
|
|
1056
|
+
errors.push(
|
|
1057
|
+
createError(
|
|
1058
|
+
"POSITION_FIXED_FORBIDDEN",
|
|
1059
|
+
'CSS position "fixed" is not allowed.',
|
|
1060
|
+
`${path}.style.position`
|
|
1061
|
+
)
|
|
1062
|
+
);
|
|
1063
|
+
} else if (position === "sticky") {
|
|
1064
|
+
errors.push(
|
|
1065
|
+
createError(
|
|
1066
|
+
"POSITION_STICKY_FORBIDDEN",
|
|
1067
|
+
'CSS position "sticky" is not allowed.',
|
|
1068
|
+
`${path}.style.position`
|
|
1069
|
+
)
|
|
1070
|
+
);
|
|
1071
|
+
} else if (position === "absolute") {
|
|
1072
|
+
if (context.parentType !== "Stack") {
|
|
1073
|
+
errors.push(
|
|
1074
|
+
createError(
|
|
1075
|
+
"POSITION_ABSOLUTE_NOT_IN_STACK",
|
|
1076
|
+
'CSS position "absolute" is only allowed inside a Stack component.',
|
|
1077
|
+
`${path}.style.position`
|
|
1078
|
+
)
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (effectiveStyle && effectiveStyle.overflow === "auto" && context.overflowAutoAncestor) {
|
|
1084
|
+
errors.push(
|
|
1085
|
+
createError(
|
|
1086
|
+
"OVERFLOW_AUTO_NESTED",
|
|
1087
|
+
"Nested overflow:auto is not allowed. An ancestor already has overflow:auto.",
|
|
1088
|
+
`${path}.style.overflow`
|
|
1089
|
+
)
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
scanForRefs(nodeFields, path, errors);
|
|
1093
|
+
if (style) {
|
|
1094
|
+
scanForRefs(style, `${path}.style`, errors);
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
return errors;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// src/limits.ts
|
|
1101
|
+
import {
|
|
1102
|
+
TEXT_CONTENT_TOTAL_MAX_BYTES,
|
|
1103
|
+
STYLE_OBJECTS_TOTAL_MAX_BYTES,
|
|
1104
|
+
MAX_NODE_COUNT,
|
|
1105
|
+
MAX_LOOP_ITERATIONS,
|
|
1106
|
+
MAX_NESTED_LOOPS,
|
|
1107
|
+
MAX_OVERFLOW_AUTO_COUNT,
|
|
1108
|
+
MAX_STACK_NESTING,
|
|
1109
|
+
PROTOTYPE_POLLUTION_SEGMENTS as PROTOTYPE_POLLUTION_SEGMENTS2,
|
|
1110
|
+
isRef as isRef4,
|
|
1111
|
+
isExpr as isExpr3
|
|
1112
|
+
} from "@safe-ugc-ui/types";
|
|
1113
|
+
function utf8ByteLength(str) {
|
|
1114
|
+
let bytes = 0;
|
|
1115
|
+
for (let i = 0; i < str.length; i++) {
|
|
1116
|
+
const code = str.charCodeAt(i);
|
|
1117
|
+
if (code <= 127) {
|
|
1118
|
+
bytes += 1;
|
|
1119
|
+
} else if (code <= 2047) {
|
|
1120
|
+
bytes += 2;
|
|
1121
|
+
} else if (code >= 55296 && code <= 56319) {
|
|
1122
|
+
bytes += 4;
|
|
1123
|
+
i++;
|
|
1124
|
+
} else {
|
|
1125
|
+
bytes += 3;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return bytes;
|
|
1129
|
+
}
|
|
1130
|
+
function resolveRefFromState2(refPath, state) {
|
|
1131
|
+
const path = refPath.startsWith("$") ? refPath.slice(1) : refPath;
|
|
1132
|
+
const dotSegments = path.split(".");
|
|
1133
|
+
const keys = [];
|
|
1134
|
+
for (const dotSeg of dotSegments) {
|
|
1135
|
+
const bracketPattern = /\[(\d+)\]/g;
|
|
1136
|
+
const firstBracket = dotSeg.indexOf("[");
|
|
1137
|
+
const baseName = firstBracket === -1 ? dotSeg : dotSeg.slice(0, firstBracket);
|
|
1138
|
+
if (baseName) {
|
|
1139
|
+
keys.push(baseName);
|
|
1140
|
+
}
|
|
1141
|
+
let match;
|
|
1142
|
+
while ((match = bracketPattern.exec(dotSeg)) !== null) {
|
|
1143
|
+
keys.push(match[1]);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
for (const key of keys) {
|
|
1147
|
+
if (PROTOTYPE_POLLUTION_SEGMENTS2.includes(key)) {
|
|
1148
|
+
return void 0;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
let current = state;
|
|
1152
|
+
for (const key of keys) {
|
|
1153
|
+
if (current == null || typeof current !== "object") return void 0;
|
|
1154
|
+
if (Array.isArray(current)) {
|
|
1155
|
+
const index = Number(key);
|
|
1156
|
+
if (!Number.isInteger(index) || index < 0) return void 0;
|
|
1157
|
+
current = current[index];
|
|
1158
|
+
} else {
|
|
1159
|
+
current = current[key];
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return current;
|
|
1163
|
+
}
|
|
1164
|
+
function countTemplateMetrics(template, cardStyles) {
|
|
1165
|
+
const result = { nodes: 0, textBytes: 0, styleBytes: 0, overflowAutoCount: 0 };
|
|
1166
|
+
if (template == null || typeof template !== "object") return result;
|
|
1167
|
+
const node = template;
|
|
1168
|
+
if (!node.type) return result;
|
|
1169
|
+
result.nodes = 1;
|
|
1170
|
+
if (node.type === "Text") {
|
|
1171
|
+
const content = node.content;
|
|
1172
|
+
if (typeof content === "string" && !isRef4(content) && !isExpr3(content)) {
|
|
1173
|
+
result.textBytes = utf8ByteLength(content);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
if (node.style != null && typeof node.style === "object") {
|
|
1177
|
+
const style = node.style;
|
|
1178
|
+
let styleForBytes = style;
|
|
1179
|
+
if (typeof style.$style === "string" && cardStyles && style.$style.trim() in cardStyles) {
|
|
1180
|
+
const refName = style.$style.trim();
|
|
1181
|
+
const merged = { ...cardStyles[refName] };
|
|
1182
|
+
for (const [key, value] of Object.entries(style)) {
|
|
1183
|
+
if (key !== "$style") merged[key] = value;
|
|
1184
|
+
}
|
|
1185
|
+
styleForBytes = merged;
|
|
1186
|
+
}
|
|
1187
|
+
result.styleBytes = utf8ByteLength(JSON.stringify(styleForBytes));
|
|
1188
|
+
let effectiveOverflow = style.overflow;
|
|
1189
|
+
if (typeof style.$style === "string" && cardStyles && style.$style.trim() in cardStyles) {
|
|
1190
|
+
const refName = style.$style.trim();
|
|
1191
|
+
if (!("overflow" in style) || style.overflow === void 0) {
|
|
1192
|
+
effectiveOverflow = cardStyles[refName].overflow;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if (effectiveOverflow === "auto") {
|
|
1196
|
+
result.overflowAutoCount = 1;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
const children = node.children;
|
|
1200
|
+
if (Array.isArray(children)) {
|
|
1201
|
+
for (const child of children) {
|
|
1202
|
+
const childMetrics = countTemplateMetrics(child, cardStyles);
|
|
1203
|
+
result.nodes += childMetrics.nodes;
|
|
1204
|
+
result.textBytes += childMetrics.textBytes;
|
|
1205
|
+
result.styleBytes += childMetrics.styleBytes;
|
|
1206
|
+
result.overflowAutoCount += childMetrics.overflowAutoCount;
|
|
1207
|
+
}
|
|
1208
|
+
} else if (children != null && typeof children === "object" && !Array.isArray(children) && "template" in children) {
|
|
1209
|
+
const innerTemplate = children.template;
|
|
1210
|
+
const innerMetrics = countTemplateMetrics(innerTemplate, cardStyles);
|
|
1211
|
+
result.nodes += innerMetrics.nodes;
|
|
1212
|
+
result.textBytes += innerMetrics.textBytes;
|
|
1213
|
+
result.styleBytes += innerMetrics.styleBytes;
|
|
1214
|
+
result.overflowAutoCount += innerMetrics.overflowAutoCount;
|
|
1215
|
+
}
|
|
1216
|
+
return result;
|
|
1217
|
+
}
|
|
1218
|
+
function validateLimits(card) {
|
|
1219
|
+
const errors = [];
|
|
1220
|
+
let nodeCount = 0;
|
|
1221
|
+
let textContentBytes = 0;
|
|
1222
|
+
let styleObjectsBytes = 0;
|
|
1223
|
+
let overflowAutoCount = 0;
|
|
1224
|
+
traverseCard(card.views, (node, context) => {
|
|
1225
|
+
nodeCount++;
|
|
1226
|
+
if (node.type === "Text") {
|
|
1227
|
+
const content = node.content;
|
|
1228
|
+
if (typeof content === "string" && !isRef4(content) && !isExpr3(content)) {
|
|
1229
|
+
textContentBytes += utf8ByteLength(content);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (node.style != null && typeof node.style === "object") {
|
|
1233
|
+
let styleForBytes = node.style;
|
|
1234
|
+
if (typeof node.style.$style === "string" && card.cardStyles && node.style.$style.trim() in card.cardStyles) {
|
|
1235
|
+
const refName = node.style.$style.trim();
|
|
1236
|
+
const merged = { ...card.cardStyles[refName] };
|
|
1237
|
+
for (const [key, value] of Object.entries(node.style)) {
|
|
1238
|
+
if (key !== "$style") {
|
|
1239
|
+
merged[key] = value;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
styleForBytes = merged;
|
|
1243
|
+
}
|
|
1244
|
+
const serialized = JSON.stringify(styleForBytes);
|
|
1245
|
+
styleObjectsBytes += utf8ByteLength(serialized);
|
|
1246
|
+
}
|
|
1247
|
+
const children = node.children;
|
|
1248
|
+
if (children != null && typeof children === "object" && !Array.isArray(children) && "for" in children && "in" in children && "template" in children) {
|
|
1249
|
+
const forLoop = children;
|
|
1250
|
+
const inValue = forLoop.in;
|
|
1251
|
+
if (typeof inValue === "string" && inValue.startsWith("$")) {
|
|
1252
|
+
if (card.state == null) {
|
|
1253
|
+
} else {
|
|
1254
|
+
const source = resolveRefFromState2(inValue, card.state);
|
|
1255
|
+
if (source === void 0) {
|
|
1256
|
+
const pathAfterDollar = inValue.slice(1);
|
|
1257
|
+
if (!pathAfterDollar.includes(".") && context.loopDepth === 0) {
|
|
1258
|
+
errors.push(
|
|
1259
|
+
createError(
|
|
1260
|
+
"LOOP_SOURCE_MISSING",
|
|
1261
|
+
`Loop source "${inValue}" not found in card state`,
|
|
1262
|
+
`${context.path}.children`
|
|
1263
|
+
)
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
} else if (!Array.isArray(source)) {
|
|
1267
|
+
errors.push(
|
|
1268
|
+
createError(
|
|
1269
|
+
"LOOP_SOURCE_NOT_ARRAY",
|
|
1270
|
+
`Loop source "${inValue}" is not an array`,
|
|
1271
|
+
`${context.path}.children`
|
|
1272
|
+
)
|
|
1273
|
+
);
|
|
1274
|
+
} else if (source.length > MAX_LOOP_ITERATIONS) {
|
|
1275
|
+
errors.push(
|
|
1276
|
+
createError(
|
|
1277
|
+
"LOOP_ITERATIONS_EXCEEDED",
|
|
1278
|
+
`Loop source "${inValue}" has ${source.length} items, max is ${MAX_LOOP_ITERATIONS}`,
|
|
1279
|
+
`${context.path}.children`
|
|
1280
|
+
)
|
|
1281
|
+
);
|
|
1282
|
+
} else if (source.length > 1) {
|
|
1283
|
+
const tplMetrics = countTemplateMetrics(forLoop.template, card.cardStyles);
|
|
1284
|
+
const multiplier = source.length - 1;
|
|
1285
|
+
nodeCount += tplMetrics.nodes * multiplier;
|
|
1286
|
+
textContentBytes += tplMetrics.textBytes * multiplier;
|
|
1287
|
+
styleObjectsBytes += tplMetrics.styleBytes * multiplier;
|
|
1288
|
+
overflowAutoCount += tplMetrics.overflowAutoCount * multiplier;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
if (context.loopDepth >= MAX_NESTED_LOOPS) {
|
|
1293
|
+
errors.push(
|
|
1294
|
+
createError(
|
|
1295
|
+
"NESTED_LOOPS_EXCEEDED",
|
|
1296
|
+
`Loop nesting depth ${context.loopDepth + 1} exceeds maximum of ${MAX_NESTED_LOOPS}`,
|
|
1297
|
+
`${context.path}.children`
|
|
1298
|
+
)
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
{
|
|
1303
|
+
let effectiveOverflow = node.style?.overflow;
|
|
1304
|
+
if (node.style && typeof node.style.$style === "string" && card.cardStyles && node.style.$style.trim() in card.cardStyles) {
|
|
1305
|
+
const refName = node.style.$style.trim();
|
|
1306
|
+
if (!("overflow" in node.style) || node.style.overflow === void 0) {
|
|
1307
|
+
effectiveOverflow = card.cardStyles[refName].overflow;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (effectiveOverflow === "auto") {
|
|
1311
|
+
overflowAutoCount++;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
if (node.type === "Stack" && context.stackDepth >= MAX_STACK_NESTING) {
|
|
1315
|
+
errors.push(
|
|
1316
|
+
createError(
|
|
1317
|
+
"STACK_NESTING_EXCEEDED",
|
|
1318
|
+
`Stack nesting depth ${context.stackDepth + 1} exceeds maximum of ${MAX_STACK_NESTING}`,
|
|
1319
|
+
context.path
|
|
1320
|
+
)
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1324
|
+
if (nodeCount > MAX_NODE_COUNT) {
|
|
1325
|
+
errors.push(
|
|
1326
|
+
createError(
|
|
1327
|
+
"NODE_COUNT_EXCEEDED",
|
|
1328
|
+
`Card has ${nodeCount} nodes, max is ${MAX_NODE_COUNT}`,
|
|
1329
|
+
"views"
|
|
1330
|
+
)
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
if (textContentBytes > TEXT_CONTENT_TOTAL_MAX_BYTES) {
|
|
1334
|
+
errors.push(
|
|
1335
|
+
createError(
|
|
1336
|
+
"TEXT_CONTENT_SIZE_EXCEEDED",
|
|
1337
|
+
`Total text content is ${textContentBytes} bytes, max is ${TEXT_CONTENT_TOTAL_MAX_BYTES}`,
|
|
1338
|
+
"views"
|
|
1339
|
+
)
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
if (styleObjectsBytes > STYLE_OBJECTS_TOTAL_MAX_BYTES) {
|
|
1343
|
+
errors.push(
|
|
1344
|
+
createError(
|
|
1345
|
+
"STYLE_SIZE_EXCEEDED",
|
|
1346
|
+
`Total style objects size is ${styleObjectsBytes} bytes, max is ${STYLE_OBJECTS_TOTAL_MAX_BYTES}`,
|
|
1347
|
+
"views"
|
|
1348
|
+
)
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
if (overflowAutoCount > MAX_OVERFLOW_AUTO_COUNT) {
|
|
1352
|
+
errors.push(
|
|
1353
|
+
createError(
|
|
1354
|
+
"OVERFLOW_AUTO_COUNT_EXCEEDED",
|
|
1355
|
+
`Card has ${overflowAutoCount} elements with overflow:auto, max is ${MAX_OVERFLOW_AUTO_COUNT}`,
|
|
1356
|
+
"views"
|
|
1357
|
+
)
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
return errors;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// src/expr-constraints.ts
|
|
1364
|
+
import {
|
|
1365
|
+
EXPR_MAX_LENGTH,
|
|
1366
|
+
EXPR_MAX_TOKENS,
|
|
1367
|
+
EXPR_MAX_NESTING,
|
|
1368
|
+
EXPR_MAX_CONDITION_NESTING,
|
|
1369
|
+
EXPR_MAX_REF_DEPTH,
|
|
1370
|
+
EXPR_MAX_ARRAY_INDEX,
|
|
1371
|
+
EXPR_MAX_STRING_LITERAL,
|
|
1372
|
+
EXPR_MAX_FRACTIONAL_DIGITS,
|
|
1373
|
+
isRef as isRef5,
|
|
1374
|
+
isExpr as isExpr4
|
|
1375
|
+
} from "@safe-ugc-ui/types";
|
|
1376
|
+
var FORBIDDEN_KEYWORDS = [
|
|
1377
|
+
"typeof",
|
|
1378
|
+
"instanceof",
|
|
1379
|
+
"new",
|
|
1380
|
+
"delete",
|
|
1381
|
+
"function",
|
|
1382
|
+
"return",
|
|
1383
|
+
"var",
|
|
1384
|
+
"let",
|
|
1385
|
+
"const"
|
|
1386
|
+
];
|
|
1387
|
+
var FORBIDDEN_KEYWORD_SET = new Set(FORBIDDEN_KEYWORDS);
|
|
1388
|
+
var PROTOTYPE_POLLUTION_SEGMENTS3 = /* @__PURE__ */ new Set([
|
|
1389
|
+
"__proto__",
|
|
1390
|
+
"constructor",
|
|
1391
|
+
"prototype"
|
|
1392
|
+
]);
|
|
1393
|
+
function tokenize(expr, path) {
|
|
1394
|
+
const tokens = [];
|
|
1395
|
+
const errors = [];
|
|
1396
|
+
let i = 0;
|
|
1397
|
+
while (i < expr.length) {
|
|
1398
|
+
if (/\s/.test(expr[i])) {
|
|
1399
|
+
i++;
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
if (i + 2 < expr.length) {
|
|
1403
|
+
const three = expr.slice(i, i + 3);
|
|
1404
|
+
if (three === "===" || three === "!==") {
|
|
1405
|
+
tokens.push({ type: "comparison", value: three, position: i });
|
|
1406
|
+
i += 3;
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
if (i + 1 < expr.length) {
|
|
1411
|
+
const two = expr.slice(i, i + 2);
|
|
1412
|
+
if (two === "==" || two === "!=" || two === "<=" || two === ">=") {
|
|
1413
|
+
tokens.push({ type: "comparison", value: two, position: i });
|
|
1414
|
+
i += 2;
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
if (two === "&&" || two === "||") {
|
|
1418
|
+
tokens.push({ type: "logic_keyword", value: two, position: i });
|
|
1419
|
+
i += 2;
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
if (expr[i] === "'" || expr[i] === '"') {
|
|
1424
|
+
const quote = expr[i];
|
|
1425
|
+
let j = i + 1;
|
|
1426
|
+
while (j < expr.length && expr[j] !== quote) {
|
|
1427
|
+
if (expr[j] === "\\" && j + 1 < expr.length) {
|
|
1428
|
+
j += 2;
|
|
1429
|
+
} else {
|
|
1430
|
+
j++;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
const innerValue = expr.slice(i + 1, j);
|
|
1434
|
+
tokens.push({ type: "string", value: innerValue, position: i });
|
|
1435
|
+
i = j + 1;
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
if (/[0-9]/.test(expr[i]) || expr[i] === "-" && i + 1 < expr.length && /[0-9]/.test(expr[i + 1]) && isNegativeSign(tokens)) {
|
|
1439
|
+
let j = i;
|
|
1440
|
+
if (expr[j] === "-") j++;
|
|
1441
|
+
while (j < expr.length && /[0-9]/.test(expr[j])) j++;
|
|
1442
|
+
if (j < expr.length && expr[j] === ".") {
|
|
1443
|
+
j++;
|
|
1444
|
+
while (j < expr.length && /[0-9]/.test(expr[j])) j++;
|
|
1445
|
+
}
|
|
1446
|
+
const numStr = expr.slice(i, j);
|
|
1447
|
+
const dotIdx = numStr.indexOf(".");
|
|
1448
|
+
if (dotIdx !== -1) {
|
|
1449
|
+
const fractionalPart = numStr.slice(dotIdx + 1);
|
|
1450
|
+
if (fractionalPart.length > EXPR_MAX_FRACTIONAL_DIGITS) {
|
|
1451
|
+
errors.push(
|
|
1452
|
+
createError(
|
|
1453
|
+
"EXPR_INVALID_TOKEN",
|
|
1454
|
+
`Number literal "${numStr}" at position ${i} has ${fractionalPart.length} fractional digits, maximum is ${EXPR_MAX_FRACTIONAL_DIGITS}.`,
|
|
1455
|
+
path
|
|
1456
|
+
)
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
tokens.push({ type: "number", value: numStr, position: i });
|
|
1461
|
+
i = j;
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
if (expr[i] === "$" || /[a-zA-Z_]/.test(expr[i])) {
|
|
1465
|
+
let j = i;
|
|
1466
|
+
if (expr[j] === "$") j++;
|
|
1467
|
+
while (j < expr.length && /[\w]/.test(expr[j])) j++;
|
|
1468
|
+
const word = expr.slice(i, j);
|
|
1469
|
+
if (word === "true" || word === "false") {
|
|
1470
|
+
tokens.push({ type: "boolean", value: word, position: i });
|
|
1471
|
+
} else if (word === "and" || word === "or" || word === "not") {
|
|
1472
|
+
tokens.push({ type: "logic_keyword", value: word, position: i });
|
|
1473
|
+
} else if (word === "if" || word === "then" || word === "else") {
|
|
1474
|
+
tokens.push({ type: "condition_keyword", value: word, position: i });
|
|
1475
|
+
} else {
|
|
1476
|
+
tokens.push({ type: "identifier", value: word, position: i });
|
|
1477
|
+
}
|
|
1478
|
+
i = j;
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
const ch = expr[i];
|
|
1482
|
+
if ("+-*/%".includes(ch)) {
|
|
1483
|
+
tokens.push({ type: "arithmetic", value: ch, position: i });
|
|
1484
|
+
i++;
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
if (ch === "<" || ch === ">") {
|
|
1488
|
+
tokens.push({ type: "comparison", value: ch, position: i });
|
|
1489
|
+
i++;
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
if ("().[]".includes(ch)) {
|
|
1493
|
+
tokens.push({ type: "separator", value: ch, position: i });
|
|
1494
|
+
i++;
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
if (ch === "!") {
|
|
1498
|
+
tokens.push({ type: "comparison", value: ch, position: i });
|
|
1499
|
+
i++;
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
errors.push(
|
|
1503
|
+
createError(
|
|
1504
|
+
"EXPR_INVALID_TOKEN",
|
|
1505
|
+
`Unrecognized character "${ch}" at position ${i} in expression.`,
|
|
1506
|
+
path
|
|
1507
|
+
)
|
|
1508
|
+
);
|
|
1509
|
+
i++;
|
|
1510
|
+
}
|
|
1511
|
+
for (let t = 0; t < tokens.length; t++) {
|
|
1512
|
+
const tok = tokens[t];
|
|
1513
|
+
if (tok.value === "===" || tok.value === "!==" || (tok.value === "&&" || tok.value === "||")) {
|
|
1514
|
+
errors.push(
|
|
1515
|
+
createError(
|
|
1516
|
+
"EXPR_FORBIDDEN_TOKEN",
|
|
1517
|
+
`Forbidden operator "${tok.value}" at position ${tok.position}. Use "==" / "!=" or "and" / "or" instead.`,
|
|
1518
|
+
path
|
|
1519
|
+
)
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
if (tok.value === "!") {
|
|
1523
|
+
errors.push(
|
|
1524
|
+
createError(
|
|
1525
|
+
"EXPR_FORBIDDEN_TOKEN",
|
|
1526
|
+
`Forbidden operator "!" at position ${tok.position}. Use "not" instead.`,
|
|
1527
|
+
path
|
|
1528
|
+
)
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
if (tok.type === "identifier" && FORBIDDEN_KEYWORD_SET.has(tok.value)) {
|
|
1532
|
+
errors.push(
|
|
1533
|
+
createError(
|
|
1534
|
+
"EXPR_FORBIDDEN_TOKEN",
|
|
1535
|
+
`Forbidden keyword "${tok.value}" at position ${tok.position}.`,
|
|
1536
|
+
path
|
|
1537
|
+
)
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
if (tok.type === "identifier" && !tok.value.startsWith("$") && !FORBIDDEN_KEYWORD_SET.has(tok.value)) {
|
|
1541
|
+
errors.push(
|
|
1542
|
+
createError(
|
|
1543
|
+
"EXPR_FORBIDDEN_TOKEN",
|
|
1544
|
+
`Identifier "${tok.value}" at position ${tok.position} must start with "$". Use "$${tok.value}" for variable references.`,
|
|
1545
|
+
path
|
|
1546
|
+
)
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
if (tok.type === "identifier") {
|
|
1550
|
+
const next = tokens[t + 1];
|
|
1551
|
+
if (next && next.type === "separator" && next.value === "(") {
|
|
1552
|
+
errors.push(
|
|
1553
|
+
createError(
|
|
1554
|
+
"EXPR_FUNCTION_CALL",
|
|
1555
|
+
`Function call pattern detected: "${tok.value}(" at position ${tok.position}. Function calls are not allowed.`,
|
|
1556
|
+
path
|
|
1557
|
+
)
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
return { tokens, errors };
|
|
1563
|
+
}
|
|
1564
|
+
function isNegativeSign(tokens) {
|
|
1565
|
+
if (tokens.length === 0) return true;
|
|
1566
|
+
const prev = tokens[tokens.length - 1];
|
|
1567
|
+
if (prev.type === "arithmetic" || prev.type === "comparison" || prev.type === "logic_keyword" || prev.type === "condition_keyword") {
|
|
1568
|
+
return true;
|
|
1569
|
+
}
|
|
1570
|
+
if (prev.type === "separator" && (prev.value === "(" || prev.value === "[")) {
|
|
1571
|
+
return true;
|
|
1572
|
+
}
|
|
1573
|
+
return false;
|
|
1574
|
+
}
|
|
1575
|
+
function scanForDynamicValues(obj, basePath, callback) {
|
|
1576
|
+
if (obj === null || obj === void 0 || typeof obj !== "object") {
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
if (isRef5(obj) || isExpr4(obj)) {
|
|
1580
|
+
callback(obj, basePath);
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
if (Array.isArray(obj)) {
|
|
1584
|
+
for (let i = 0; i < obj.length; i++) {
|
|
1585
|
+
scanForDynamicValues(obj[i], `${basePath}[${i}]`, callback);
|
|
1586
|
+
}
|
|
1587
|
+
} else {
|
|
1588
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1589
|
+
scanForDynamicValues(value, `${basePath}.${key}`, callback);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
function validateRef(refValue, path) {
|
|
1594
|
+
const errors = [];
|
|
1595
|
+
if (refValue.length > 500) {
|
|
1596
|
+
errors.push(
|
|
1597
|
+
createError(
|
|
1598
|
+
"REF_TOO_LONG",
|
|
1599
|
+
`$ref value exceeds maximum length of 500 characters (got ${refValue.length}).`,
|
|
1600
|
+
path
|
|
1601
|
+
)
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
const segments = refValue.split(".");
|
|
1605
|
+
if (segments.length > EXPR_MAX_REF_DEPTH) {
|
|
1606
|
+
errors.push(
|
|
1607
|
+
createError(
|
|
1608
|
+
"EXPR_REF_DEPTH_EXCEEDED",
|
|
1609
|
+
`$ref path depth ${segments.length} exceeds maximum of ${EXPR_MAX_REF_DEPTH}.`,
|
|
1610
|
+
path
|
|
1611
|
+
)
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
const arrayIndexPattern = /\[(\d+)\]/g;
|
|
1615
|
+
let match;
|
|
1616
|
+
while ((match = arrayIndexPattern.exec(refValue)) !== null) {
|
|
1617
|
+
const index = parseInt(match[1], 10);
|
|
1618
|
+
if (index > EXPR_MAX_ARRAY_INDEX) {
|
|
1619
|
+
errors.push(
|
|
1620
|
+
createError(
|
|
1621
|
+
"EXPR_ARRAY_INDEX_EXCEEDED",
|
|
1622
|
+
`Array index ${index} in $ref exceeds maximum of ${EXPR_MAX_ARRAY_INDEX}.`,
|
|
1623
|
+
path
|
|
1624
|
+
)
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
for (const segment of segments) {
|
|
1629
|
+
const cleanSegment = segment.replace(/\[\d+\]/g, "");
|
|
1630
|
+
if (PROTOTYPE_POLLUTION_SEGMENTS3.has(cleanSegment)) {
|
|
1631
|
+
errors.push(
|
|
1632
|
+
createError(
|
|
1633
|
+
"PROTOTYPE_POLLUTION",
|
|
1634
|
+
`$ref path contains forbidden segment "${cleanSegment}".`,
|
|
1635
|
+
path
|
|
1636
|
+
)
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
return errors;
|
|
1641
|
+
}
|
|
1642
|
+
function validateExpr(exprValue, path) {
|
|
1643
|
+
const errors = [];
|
|
1644
|
+
if (exprValue.length > EXPR_MAX_LENGTH) {
|
|
1645
|
+
errors.push(
|
|
1646
|
+
createError(
|
|
1647
|
+
"EXPR_TOO_LONG",
|
|
1648
|
+
`Expression exceeds maximum length of ${EXPR_MAX_LENGTH} characters (got ${exprValue.length}).`,
|
|
1649
|
+
path
|
|
1650
|
+
)
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
const { tokens, errors: tokenErrors } = tokenize(exprValue, path);
|
|
1654
|
+
errors.push(...tokenErrors);
|
|
1655
|
+
if (tokens.length > EXPR_MAX_TOKENS) {
|
|
1656
|
+
errors.push(
|
|
1657
|
+
createError(
|
|
1658
|
+
"EXPR_TOO_MANY_TOKENS",
|
|
1659
|
+
`Expression has ${tokens.length} tokens, exceeding maximum of ${EXPR_MAX_TOKENS}.`,
|
|
1660
|
+
path
|
|
1661
|
+
)
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
for (const tok of tokens) {
|
|
1665
|
+
if (tok.type === "string" && tok.value.length > EXPR_MAX_STRING_LITERAL) {
|
|
1666
|
+
errors.push(
|
|
1667
|
+
createError(
|
|
1668
|
+
"EXPR_STRING_LITERAL_TOO_LONG",
|
|
1669
|
+
`String literal at position ${tok.position} has ${tok.value.length} characters, exceeding maximum of ${EXPR_MAX_STRING_LITERAL}.`,
|
|
1670
|
+
path
|
|
1671
|
+
)
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
let parenDepth = 0;
|
|
1676
|
+
let maxParenDepth = 0;
|
|
1677
|
+
let ifCount = 0;
|
|
1678
|
+
for (const tok of tokens) {
|
|
1679
|
+
if (tok.type === "separator" && tok.value === "(") {
|
|
1680
|
+
parenDepth++;
|
|
1681
|
+
if (parenDepth > maxParenDepth) {
|
|
1682
|
+
maxParenDepth = parenDepth;
|
|
1683
|
+
}
|
|
1684
|
+
} else if (tok.type === "separator" && tok.value === ")") {
|
|
1685
|
+
parenDepth--;
|
|
1686
|
+
} else if (tok.type === "condition_keyword" && tok.value === "if") {
|
|
1687
|
+
ifCount++;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
if (maxParenDepth > EXPR_MAX_NESTING) {
|
|
1691
|
+
errors.push(
|
|
1692
|
+
createError(
|
|
1693
|
+
"EXPR_NESTING_TOO_DEEP",
|
|
1694
|
+
`Expression parenthesis nesting depth ${maxParenDepth} exceeds maximum of ${EXPR_MAX_NESTING}.`,
|
|
1695
|
+
path
|
|
1696
|
+
)
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
if (ifCount > EXPR_MAX_CONDITION_NESTING) {
|
|
1700
|
+
errors.push(
|
|
1701
|
+
createError(
|
|
1702
|
+
"EXPR_CONDITION_NESTING_TOO_DEEP",
|
|
1703
|
+
`Expression has ${ifCount} nested if-conditions, exceeding maximum of ${EXPR_MAX_CONDITION_NESTING}.`,
|
|
1704
|
+
path
|
|
1705
|
+
)
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
for (let t = 0; t < tokens.length; t++) {
|
|
1709
|
+
const tok = tokens[t];
|
|
1710
|
+
if (tok.type === "identifier" && tok.value.startsWith("$")) {
|
|
1711
|
+
let depth = 1;
|
|
1712
|
+
let j = t + 1;
|
|
1713
|
+
while (j + 1 < tokens.length) {
|
|
1714
|
+
if (tokens[j].type === "separator" && tokens[j].value === "." && tokens[j + 1].type === "identifier") {
|
|
1715
|
+
depth++;
|
|
1716
|
+
j += 2;
|
|
1717
|
+
} else {
|
|
1718
|
+
break;
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
if (depth > EXPR_MAX_REF_DEPTH) {
|
|
1722
|
+
errors.push(
|
|
1723
|
+
createError(
|
|
1724
|
+
"EXPR_REF_DEPTH_EXCEEDED",
|
|
1725
|
+
`Variable reference "${tok.value}" has path depth ${depth}, exceeding maximum of ${EXPR_MAX_REF_DEPTH}.`,
|
|
1726
|
+
path
|
|
1727
|
+
)
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
for (let t = 0; t + 2 < tokens.length; t++) {
|
|
1733
|
+
if (tokens[t].type === "separator" && tokens[t].value === "[" && tokens[t + 1].type === "number" && tokens[t + 2].type === "separator" && tokens[t + 2].value === "]") {
|
|
1734
|
+
const indexValue = parseFloat(tokens[t + 1].value);
|
|
1735
|
+
if (indexValue > EXPR_MAX_ARRAY_INDEX) {
|
|
1736
|
+
errors.push(
|
|
1737
|
+
createError(
|
|
1738
|
+
"EXPR_ARRAY_INDEX_EXCEEDED",
|
|
1739
|
+
`Array index ${indexValue} in expression exceeds maximum of ${EXPR_MAX_ARRAY_INDEX}.`,
|
|
1740
|
+
path
|
|
1741
|
+
)
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return errors;
|
|
1747
|
+
}
|
|
1748
|
+
function validateExprConstraints(views) {
|
|
1749
|
+
const errors = [];
|
|
1750
|
+
traverseCard(views, (node, context) => {
|
|
1751
|
+
const nodeFields = { ...node };
|
|
1752
|
+
delete nodeFields.type;
|
|
1753
|
+
delete nodeFields.style;
|
|
1754
|
+
delete nodeFields.children;
|
|
1755
|
+
delete nodeFields.condition;
|
|
1756
|
+
scanForDynamicValues(nodeFields, context.path, (value, valuePath) => {
|
|
1757
|
+
if (isRef5(value)) {
|
|
1758
|
+
errors.push(...validateRef(value.$ref, valuePath));
|
|
1759
|
+
} else if (isExpr4(value)) {
|
|
1760
|
+
errors.push(...validateExpr(value.$expr, valuePath));
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1763
|
+
if (node.style) {
|
|
1764
|
+
scanForDynamicValues(node.style, `${context.path}.style`, (value, valuePath) => {
|
|
1765
|
+
if (isRef5(value)) {
|
|
1766
|
+
errors.push(...validateRef(value.$ref, valuePath));
|
|
1767
|
+
} else if (isExpr4(value)) {
|
|
1768
|
+
errors.push(...validateExpr(value.$expr, valuePath));
|
|
1769
|
+
}
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
if (node.condition !== void 0) {
|
|
1773
|
+
scanForDynamicValues(node.condition, `${context.path}.condition`, (value, valuePath) => {
|
|
1774
|
+
if (isRef5(value)) {
|
|
1775
|
+
errors.push(...validateRef(value.$ref, valuePath));
|
|
1776
|
+
} else if (isExpr4(value)) {
|
|
1777
|
+
errors.push(...validateExpr(value.$expr, valuePath));
|
|
1778
|
+
}
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
});
|
|
1782
|
+
return errors;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// src/index.ts
|
|
1786
|
+
function utf8ByteLength2(str) {
|
|
1787
|
+
let bytes = 0;
|
|
1788
|
+
for (let i = 0; i < str.length; i++) {
|
|
1789
|
+
const code = str.charCodeAt(i);
|
|
1790
|
+
if (code <= 127) {
|
|
1791
|
+
bytes += 1;
|
|
1792
|
+
} else if (code <= 2047) {
|
|
1793
|
+
bytes += 2;
|
|
1794
|
+
} else if (code >= 55296 && code <= 56319) {
|
|
1795
|
+
bytes += 4;
|
|
1796
|
+
i++;
|
|
1797
|
+
} else {
|
|
1798
|
+
bytes += 3;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
return bytes;
|
|
1802
|
+
}
|
|
1803
|
+
function runAllChecks(input) {
|
|
1804
|
+
const obj = input;
|
|
1805
|
+
const views = obj.views;
|
|
1806
|
+
const cardStyles = obj.styles;
|
|
1807
|
+
const errors = [];
|
|
1808
|
+
errors.push(...validateNodes(views));
|
|
1809
|
+
errors.push(...validateValueTypes(views));
|
|
1810
|
+
errors.push(...validateStyles(views, cardStyles));
|
|
1811
|
+
errors.push(...validateSecurity({
|
|
1812
|
+
views,
|
|
1813
|
+
state: obj.state,
|
|
1814
|
+
cardAssets: obj.assets,
|
|
1815
|
+
cardStyles
|
|
1816
|
+
}));
|
|
1817
|
+
errors.push(...validateLimits({ state: obj.state, views, cardStyles }));
|
|
1818
|
+
errors.push(...validateExprConstraints(views));
|
|
1819
|
+
return errors;
|
|
1820
|
+
}
|
|
1821
|
+
function validate(input) {
|
|
1822
|
+
const schemaResult = validateSchema(input);
|
|
1823
|
+
if (!schemaResult.valid) {
|
|
1824
|
+
return schemaResult;
|
|
1825
|
+
}
|
|
1826
|
+
const errors = runAllChecks(input);
|
|
1827
|
+
return toResult(errors);
|
|
1828
|
+
}
|
|
1829
|
+
function validateRaw(rawJson) {
|
|
1830
|
+
const byteSize = utf8ByteLength2(rawJson);
|
|
1831
|
+
if (byteSize > CARD_JSON_MAX_BYTES) {
|
|
1832
|
+
return toResult([
|
|
1833
|
+
createError(
|
|
1834
|
+
"CARD_SIZE_EXCEEDED",
|
|
1835
|
+
`Card JSON is ${byteSize} bytes, maximum is ${CARD_JSON_MAX_BYTES} bytes.`,
|
|
1836
|
+
""
|
|
1837
|
+
)
|
|
1838
|
+
]);
|
|
1839
|
+
}
|
|
1840
|
+
let parsed;
|
|
1841
|
+
try {
|
|
1842
|
+
parsed = JSON.parse(rawJson);
|
|
1843
|
+
} catch (e) {
|
|
1844
|
+
const message = e instanceof Error ? e.message : "Invalid JSON";
|
|
1845
|
+
return toResult([
|
|
1846
|
+
createError("INVALID_JSON", `Failed to parse JSON: ${message}`, "")
|
|
1847
|
+
]);
|
|
1848
|
+
}
|
|
1849
|
+
return validate(parsed);
|
|
1850
|
+
}
|
|
1851
|
+
export {
|
|
1852
|
+
createError,
|
|
1853
|
+
invalidResult,
|
|
1854
|
+
merge,
|
|
1855
|
+
parseCard,
|
|
1856
|
+
toResult,
|
|
1857
|
+
traverseCard,
|
|
1858
|
+
traverseNode,
|
|
1859
|
+
validResult,
|
|
1860
|
+
validate,
|
|
1861
|
+
validateExprConstraints,
|
|
1862
|
+
validateLimits,
|
|
1863
|
+
validateNodes,
|
|
1864
|
+
validateRaw,
|
|
1865
|
+
validateSchema,
|
|
1866
|
+
validateSecurity,
|
|
1867
|
+
validateStyles,
|
|
1868
|
+
validateValueTypes
|
|
1869
|
+
};
|
|
1870
|
+
//# sourceMappingURL=index.js.map
|