@kumwe/studio-core 0.1.0-alpha.7 → 0.1.0-alpha.8
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/README.md +3 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/profile-validator.d.ts +2 -2
- package/dist/profile-validator.d.ts.map +1 -1
- package/dist/profile-validator.js +144 -40
- package/dist/profile-validator.js.map +1 -1
- package/dist/schema-profile.d.ts +23 -5
- package/dist/schema-profile.d.ts.map +1 -1
- package/dist/schema-profile.js +785 -160
- package/dist/schema-profile.js.map +1 -1
- package/package.json +2 -2
package/dist/schema-profile.js
CHANGED
|
@@ -1,34 +1,62 @@
|
|
|
1
|
+
import { canonicalStringify } from './canonical.js';
|
|
1
2
|
import { compileProfileSchema } from './profile-validator.js';
|
|
2
3
|
const DRAFT_2020_12 = 'https://json-schema.org/draft/2020-12/schema';
|
|
3
4
|
const MAX_ALTERNATIVES = 64;
|
|
5
|
+
const MAX_DESCRIPTION_LENGTH = 10_000;
|
|
4
6
|
const MAX_ENUM_MEMBERS = 1_024;
|
|
7
|
+
const MAX_EXAMPLES = 100;
|
|
5
8
|
const MAX_JSON_DEPTH = 64;
|
|
6
9
|
const MAX_JSON_ITEMS = 10_000;
|
|
7
10
|
const MAX_JSON_PROPERTIES = 1_000;
|
|
8
11
|
const MAX_REFERENCES = 128;
|
|
12
|
+
const MAX_REFERENCE_LENGTH = 500;
|
|
9
13
|
const MAX_SCHEMA_BYTES = 262_144;
|
|
10
14
|
const MAX_SCHEMA_DEPTH = 32;
|
|
11
15
|
const MAX_SCHEMA_MAP_PROPERTIES = 512;
|
|
12
16
|
const MAX_SCHEMA_NODES = 1_024;
|
|
13
17
|
const MAX_OBJECT_KEY_LENGTH = 200;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* same values; a parity test keeps the two from drifting.
|
|
18
|
-
*/
|
|
18
|
+
const MAX_PROPERTY_NAMES = 512;
|
|
19
|
+
const MAX_TITLE_LENGTH = 1_000;
|
|
20
|
+
/** Published complexity limits, pinned to `$defs/limits` in the meta-schema. */
|
|
19
21
|
export const STUDIO_SCHEMA_PROFILE_LIMITS = Object.freeze({
|
|
20
22
|
maxAlternatives: MAX_ALTERNATIVES,
|
|
23
|
+
maxDescriptionLength: MAX_DESCRIPTION_LENGTH,
|
|
21
24
|
maxEnumMembers: MAX_ENUM_MEMBERS,
|
|
25
|
+
maxExamples: MAX_EXAMPLES,
|
|
22
26
|
maxJsonDepth: MAX_JSON_DEPTH,
|
|
23
27
|
maxJsonItems: MAX_JSON_ITEMS,
|
|
24
28
|
maxJsonProperties: MAX_JSON_PROPERTIES,
|
|
25
29
|
maxObjectKeyLength: MAX_OBJECT_KEY_LENGTH,
|
|
30
|
+
maxPropertyNames: MAX_PROPERTY_NAMES,
|
|
31
|
+
maxReferenceLength: MAX_REFERENCE_LENGTH,
|
|
26
32
|
maxReferences: MAX_REFERENCES,
|
|
27
33
|
maxSchemaBytes: MAX_SCHEMA_BYTES,
|
|
28
34
|
maxSchemaDepth: MAX_SCHEMA_DEPTH,
|
|
29
35
|
maxSchemaMapProperties: MAX_SCHEMA_MAP_PROPERTIES,
|
|
30
36
|
maxSchemaNodes: MAX_SCHEMA_NODES,
|
|
37
|
+
maxTitleLength: MAX_TITLE_LENGTH,
|
|
31
38
|
});
|
|
39
|
+
export const STUDIO_SCHEMA_PROFILE_ERROR_CODES = Object.freeze([
|
|
40
|
+
'invalid-root',
|
|
41
|
+
'unsupported-keyword',
|
|
42
|
+
'invalid-keyword-value',
|
|
43
|
+
'unsafe-member',
|
|
44
|
+
'limit-exceeded',
|
|
45
|
+
'invalid-reference',
|
|
46
|
+
'recursive-schema',
|
|
47
|
+
]);
|
|
48
|
+
/** A deterministic admission failure suitable for cross-runtime corpus comparison. */
|
|
49
|
+
export class StudioSchemaProfileError extends TypeError {
|
|
50
|
+
code;
|
|
51
|
+
/** JSON Pointer to the rejected schema location; the empty string is the root. */
|
|
52
|
+
schemaPath;
|
|
53
|
+
constructor(code, schemaPath, message, options) {
|
|
54
|
+
super(message, options);
|
|
55
|
+
this.name = 'StudioSchemaProfileError';
|
|
56
|
+
this.code = code;
|
|
57
|
+
this.schemaPath = schemaPath;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
32
60
|
const allowedKeywords = new Set([
|
|
33
61
|
'$defs',
|
|
34
62
|
'$ref',
|
|
@@ -69,55 +97,83 @@ const allowedKeywords = new Set([
|
|
|
69
97
|
'uniqueItems',
|
|
70
98
|
'writeOnly',
|
|
71
99
|
]);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
100
|
+
const typeNames = new Set(['array', 'boolean', 'integer', 'null', 'number', 'object', 'string']);
|
|
101
|
+
class SchemaByteLimitError extends RangeError {
|
|
102
|
+
}
|
|
103
|
+
class SchemaBytePreflightDeferred extends TypeError {
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Admit and compile one contributed block property schema. The alpha profile
|
|
107
|
+
* is deliberately object-rooted, closed, local-reference-only, non-recursive,
|
|
108
|
+
* and format-free. The returned interpreter performs no code generation.
|
|
109
|
+
*/
|
|
110
|
+
export function compileStudioPropertySchema(schema) {
|
|
111
|
+
if (!isRecord(schema)) {
|
|
112
|
+
reject('invalid-root', '', 'Studio property schema root must be a JSON Schema object.');
|
|
113
|
+
}
|
|
114
|
+
// Measure before sorting or recursively interpreting attacker-controlled
|
|
115
|
+
// maps. Canonical member order does not affect encoded length, so this
|
|
116
|
+
// bounded, iterative pass can fail oversized inputs without first
|
|
117
|
+
// allocating the canonical document or doing O(n log n) work.
|
|
77
118
|
try {
|
|
78
|
-
|
|
119
|
+
assertCanonicalSchemaByteBudget(schema);
|
|
79
120
|
}
|
|
80
121
|
catch (error) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
122
|
+
if (error instanceof SchemaByteLimitError) {
|
|
123
|
+
reject('limit-exceeded', '', `Studio property schema exceeds ${MAX_SCHEMA_BYTES} canonical UTF-8 bytes.`);
|
|
124
|
+
}
|
|
125
|
+
if (error instanceof SchemaBytePreflightDeferred) {
|
|
126
|
+
// Precise structural admission below owns diagnostics for JavaScript
|
|
127
|
+
// values that cannot have come from decoded JSON (cycles, aliases,
|
|
128
|
+
// sparse arrays, undefined, or exotic prototypes).
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
reject('invalid-root', '', 'Studio property schema must be a bounded canonical JSON document.', error);
|
|
132
|
+
}
|
|
84
133
|
}
|
|
85
|
-
|
|
86
|
-
|
|
134
|
+
const state = {
|
|
135
|
+
references: 0,
|
|
136
|
+
schemaNodes: 0,
|
|
137
|
+
seen: new WeakSet(),
|
|
138
|
+
};
|
|
139
|
+
const admissionFailures = [];
|
|
140
|
+
captureAdmissionFailure(() => visitSchema(schema, '', 1, state), admissionFailures);
|
|
141
|
+
captureAdmissionFailure(() => assertNonRecursiveSchema(schema), admissionFailures);
|
|
142
|
+
captureAdmissionFailure(() => assertClosedObjectRoot(schema), admissionFailures);
|
|
143
|
+
const admissionFailure = firstAdmissionFailure(schema, admissionFailures);
|
|
144
|
+
if (admissionFailure !== undefined) {
|
|
145
|
+
throw admissionFailure;
|
|
87
146
|
}
|
|
88
147
|
try {
|
|
89
|
-
|
|
90
|
-
// keyword operands, compiles bounded lexical patterns, and resolves local
|
|
91
|
-
// references, so a schema that fails to compile is rejected atomically.
|
|
92
|
-
compileProfileSchema(schema);
|
|
148
|
+
return compileProfileSchema(schema);
|
|
93
149
|
}
|
|
94
150
|
catch (error) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
151
|
+
// All public operand checks run above. Reaching this branch means the
|
|
152
|
+
// interpreter found an inconsistency, so fail closed without exposing an
|
|
153
|
+
// implementation-specific error taxonomy.
|
|
154
|
+
reject('invalid-keyword-value', '', 'Studio property schema does not compile under the strict profile.', error);
|
|
98
155
|
}
|
|
99
156
|
}
|
|
157
|
+
/** Assert that a value is an admitted Studio property schema. */
|
|
158
|
+
export function assertStudioPropertySchema(schema) {
|
|
159
|
+
compileStudioPropertySchema(schema);
|
|
160
|
+
}
|
|
100
161
|
function visitSchema(value, path, depth, state) {
|
|
101
162
|
if (!isRecord(value)) {
|
|
102
|
-
|
|
163
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} must be a JSON Schema object.`);
|
|
103
164
|
}
|
|
104
165
|
trackObject(value, path, state);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
state.schemaNodes += 1;
|
|
109
|
-
if (state.schemaNodes > MAX_SCHEMA_NODES) {
|
|
110
|
-
throw new RangeError(`Studio property schema exceeds ${MAX_SCHEMA_NODES} schema nodes.`);
|
|
111
|
-
}
|
|
112
|
-
for (const [keyword, keywordValue] of Object.entries(value)) {
|
|
166
|
+
trackSchemaNode(path, depth, state);
|
|
167
|
+
for (const [keyword, operand] of boundedSchemaEntries(value)) {
|
|
168
|
+
const keywordPath = appendPointer(path, keyword);
|
|
113
169
|
assertSafeObjectKey(keyword, path);
|
|
114
170
|
if (!allowedKeywords.has(keyword)) {
|
|
115
|
-
|
|
171
|
+
reject('unsupported-keyword', keywordPath, `${displayPath(keywordPath)} uses keyword ${JSON.stringify(keyword)}, which is not allowed by the Studio Schema Profile.`);
|
|
116
172
|
}
|
|
117
173
|
switch (keyword) {
|
|
118
174
|
case '$defs':
|
|
119
175
|
case 'properties':
|
|
120
|
-
visitSchemaMap(
|
|
176
|
+
visitSchemaMap(operand, keywordPath, depth + 1, state);
|
|
121
177
|
break;
|
|
122
178
|
case 'additionalProperties':
|
|
123
179
|
case 'else':
|
|
@@ -126,72 +182,235 @@ function visitSchema(value, path, depth, state) {
|
|
|
126
182
|
case 'not':
|
|
127
183
|
case 'propertyNames':
|
|
128
184
|
case 'then':
|
|
129
|
-
visitSubschema(
|
|
185
|
+
visitSubschema(operand, keywordPath, depth + 1, state);
|
|
130
186
|
break;
|
|
131
187
|
case 'allOf':
|
|
132
188
|
case 'anyOf':
|
|
133
189
|
case 'oneOf':
|
|
134
190
|
case 'prefixItems':
|
|
135
|
-
visitSchemaArray(
|
|
191
|
+
visitSchemaArray(operand, keywordPath, depth + 1, state);
|
|
136
192
|
break;
|
|
137
193
|
case '$ref':
|
|
138
|
-
visitReference(
|
|
194
|
+
visitReference(operand, keywordPath, state);
|
|
139
195
|
break;
|
|
140
196
|
case '$schema':
|
|
141
|
-
if (
|
|
142
|
-
|
|
197
|
+
if (operand !== DRAFT_2020_12) {
|
|
198
|
+
reject('invalid-keyword-value', keywordPath, `${displayPath(keywordPath)} must declare JSON Schema Draft 2020-12.`);
|
|
143
199
|
}
|
|
144
200
|
break;
|
|
145
201
|
case 'enum':
|
|
146
|
-
|
|
147
|
-
|
|
202
|
+
visitEnum(operand, keywordPath, 1, state);
|
|
203
|
+
break;
|
|
204
|
+
case 'examples':
|
|
205
|
+
visitExamples(operand, keywordPath, 1, state);
|
|
206
|
+
break;
|
|
207
|
+
case 'dependentRequired':
|
|
208
|
+
visitDependentRequired(operand, keywordPath, state);
|
|
209
|
+
break;
|
|
210
|
+
case 'required':
|
|
211
|
+
visitNameArray(operand, keywordPath, MAX_PROPERTY_NAMES, state);
|
|
212
|
+
break;
|
|
213
|
+
case 'type':
|
|
214
|
+
visitType(operand, keywordPath, state);
|
|
215
|
+
break;
|
|
216
|
+
case 'description':
|
|
217
|
+
visitBoundedString(operand, keywordPath, MAX_DESCRIPTION_LENGTH);
|
|
218
|
+
break;
|
|
219
|
+
case 'title':
|
|
220
|
+
visitBoundedString(operand, keywordPath, MAX_TITLE_LENGTH);
|
|
221
|
+
break;
|
|
222
|
+
case 'maxItems':
|
|
223
|
+
case 'maxLength':
|
|
224
|
+
case 'maxProperties':
|
|
225
|
+
case 'minItems':
|
|
226
|
+
case 'minLength':
|
|
227
|
+
case 'minProperties':
|
|
228
|
+
visitNonNegativeInteger(operand, keywordPath);
|
|
229
|
+
break;
|
|
230
|
+
case 'exclusiveMaximum':
|
|
231
|
+
case 'exclusiveMinimum':
|
|
232
|
+
case 'maximum':
|
|
233
|
+
case 'minimum':
|
|
234
|
+
visitFiniteNumber(operand, keywordPath);
|
|
235
|
+
break;
|
|
236
|
+
case 'multipleOf':
|
|
237
|
+
visitFiniteNumber(operand, keywordPath);
|
|
238
|
+
if (operand <= 0) {
|
|
239
|
+
reject('invalid-keyword-value', keywordPath, `${displayPath(keywordPath)} must be greater than zero.`);
|
|
148
240
|
}
|
|
149
|
-
visitJsonValue(keywordValue, `${path}.enum`, depth + 1, state);
|
|
150
241
|
break;
|
|
151
|
-
|
|
152
|
-
|
|
242
|
+
case 'readOnly':
|
|
243
|
+
case 'uniqueItems':
|
|
244
|
+
case 'writeOnly':
|
|
245
|
+
if (typeof operand !== 'boolean') {
|
|
246
|
+
reject('invalid-keyword-value', keywordPath, `${displayPath(keywordPath)} must be a boolean.`);
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
case 'const':
|
|
250
|
+
case 'default':
|
|
251
|
+
visitJsonValue(operand, keywordPath, 1, state);
|
|
252
|
+
break;
|
|
153
253
|
}
|
|
154
254
|
}
|
|
155
255
|
}
|
|
156
256
|
function visitSchemaMap(value, path, depth, state) {
|
|
157
257
|
if (!isRecord(value)) {
|
|
158
|
-
|
|
258
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} must be an object of schemas.`);
|
|
159
259
|
}
|
|
160
260
|
trackObject(value, path, state);
|
|
161
|
-
const
|
|
162
|
-
if (
|
|
163
|
-
|
|
261
|
+
const keys = Object.keys(value);
|
|
262
|
+
if (keys.length > MAX_SCHEMA_MAP_PROPERTIES) {
|
|
263
|
+
reject('limit-exceeded', path, `${displayPath(path)} exceeds ${MAX_SCHEMA_MAP_PROPERTIES} schema entries.`);
|
|
164
264
|
}
|
|
165
|
-
for (const
|
|
265
|
+
for (const name of keys.sort(compareCodeUnits)) {
|
|
166
266
|
assertSafeObjectKey(name, path);
|
|
167
|
-
visitSchema(
|
|
267
|
+
visitSchema(value[name], appendPointer(path, name), depth, state);
|
|
168
268
|
}
|
|
169
269
|
}
|
|
170
270
|
function visitSchemaArray(value, path, depth, state) {
|
|
171
|
-
if (!Array.isArray(value) || value
|
|
172
|
-
|
|
271
|
+
if (!Array.isArray(value) || !isDenseArray(value)) {
|
|
272
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} must be a dense JSON array of schemas.`);
|
|
273
|
+
}
|
|
274
|
+
if (value.length === 0) {
|
|
275
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} must contain at least one schema.`);
|
|
276
|
+
}
|
|
277
|
+
if (value.length > MAX_ALTERNATIVES) {
|
|
278
|
+
reject('limit-exceeded', path, `${displayPath(path)} must contain at most ${MAX_ALTERNATIVES} schemas.`);
|
|
173
279
|
}
|
|
174
280
|
trackObject(value, path, state);
|
|
175
|
-
assertDenseArray(value, path);
|
|
176
281
|
for (const [index, schema] of value.entries()) {
|
|
177
|
-
visitSubschema(schema,
|
|
282
|
+
visitSubschema(schema, appendPointer(path, String(index)), depth, state);
|
|
178
283
|
}
|
|
179
284
|
}
|
|
180
285
|
function visitSubschema(value, path, depth, state) {
|
|
181
286
|
if (typeof value === 'boolean') {
|
|
287
|
+
trackSchemaNode(path, depth, state);
|
|
182
288
|
return;
|
|
183
289
|
}
|
|
184
290
|
visitSchema(value, path, depth, state);
|
|
185
291
|
}
|
|
292
|
+
function trackSchemaNode(path, depth, state) {
|
|
293
|
+
if (depth > MAX_SCHEMA_DEPTH) {
|
|
294
|
+
reject('limit-exceeded', path, `${displayPath(path)} exceeds the Studio Schema Profile depth limit.`);
|
|
295
|
+
}
|
|
296
|
+
state.schemaNodes += 1;
|
|
297
|
+
if (state.schemaNodes > MAX_SCHEMA_NODES) {
|
|
298
|
+
reject('limit-exceeded', path, `Studio property schema exceeds ${MAX_SCHEMA_NODES} schema nodes.`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
186
301
|
function visitReference(value, path, state) {
|
|
187
|
-
if (
|
|
188
|
-
(
|
|
189
|
-
value.length > 500) {
|
|
190
|
-
throw new TypeError(`${path} must be a bounded local JSON Pointer reference.`);
|
|
302
|
+
if (!isPortableLocalReference(value)) {
|
|
303
|
+
reject('invalid-reference', path, `${displayPath(path)} must be a bounded local JSON Pointer reference.`);
|
|
191
304
|
}
|
|
192
305
|
state.references += 1;
|
|
193
306
|
if (state.references > MAX_REFERENCES) {
|
|
194
|
-
|
|
307
|
+
reject('limit-exceeded', path, `Studio property schema exceeds ${MAX_REFERENCES} references.`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function visitEnum(value, path, depth, state) {
|
|
311
|
+
if (!Array.isArray(value) || !isDenseArray(value)) {
|
|
312
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} must be a dense JSON array.`);
|
|
313
|
+
}
|
|
314
|
+
if (value.length === 0) {
|
|
315
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} must contain at least one value.`);
|
|
316
|
+
}
|
|
317
|
+
if (value.length > MAX_ENUM_MEMBERS) {
|
|
318
|
+
reject('limit-exceeded', path, `${displayPath(path)} exceeds ${MAX_ENUM_MEMBERS} members.`);
|
|
319
|
+
}
|
|
320
|
+
trackObject(value, path, state);
|
|
321
|
+
const members = new Set();
|
|
322
|
+
for (const [index, member] of value.entries()) {
|
|
323
|
+
visitJsonValue(member, appendPointer(path, String(index)), depth, state);
|
|
324
|
+
const canonical = canonicalStringify(member, {
|
|
325
|
+
maximumDepth: MAX_JSON_DEPTH + 1,
|
|
326
|
+
});
|
|
327
|
+
if (members.has(canonical)) {
|
|
328
|
+
reject('invalid-keyword-value', appendPointer(path, String(index)), `${displayPath(path)} must contain unique JSON values.`);
|
|
329
|
+
}
|
|
330
|
+
members.add(canonical);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function visitExamples(value, path, depth, state) {
|
|
334
|
+
if (!Array.isArray(value) || !isDenseArray(value)) {
|
|
335
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} must be a dense JSON array.`);
|
|
336
|
+
}
|
|
337
|
+
if (value.length > MAX_EXAMPLES) {
|
|
338
|
+
reject('limit-exceeded', path, `${displayPath(path)} exceeds ${MAX_EXAMPLES} examples.`);
|
|
339
|
+
}
|
|
340
|
+
trackObject(value, path, state);
|
|
341
|
+
for (const [index, example] of value.entries()) {
|
|
342
|
+
visitJsonValue(example, appendPointer(path, String(index)), depth, state);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function visitDependentRequired(value, path, state) {
|
|
346
|
+
if (!isRecord(value)) {
|
|
347
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} must be an object of property-name arrays.`);
|
|
348
|
+
}
|
|
349
|
+
trackObject(value, path, state);
|
|
350
|
+
const keys = Object.keys(value);
|
|
351
|
+
if (keys.length > MAX_SCHEMA_MAP_PROPERTIES) {
|
|
352
|
+
reject('limit-exceeded', path, `${displayPath(path)} exceeds ${MAX_SCHEMA_MAP_PROPERTIES} dependency entries.`);
|
|
353
|
+
}
|
|
354
|
+
for (const name of keys.sort(compareCodeUnits)) {
|
|
355
|
+
assertSafeObjectKey(name, path);
|
|
356
|
+
visitNameArray(value[name], appendPointer(path, name), MAX_PROPERTY_NAMES, state);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function visitNameArray(value, path, maximum, state) {
|
|
360
|
+
if (!Array.isArray(value) || !isDenseArray(value)) {
|
|
361
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} must be a dense array of property names.`);
|
|
362
|
+
}
|
|
363
|
+
if (value.length > maximum) {
|
|
364
|
+
reject('limit-exceeded', path, `${displayPath(path)} exceeds ${maximum} property names.`);
|
|
365
|
+
}
|
|
366
|
+
trackObject(value, path, state);
|
|
367
|
+
const names = new Set();
|
|
368
|
+
for (const [index, name] of value.entries()) {
|
|
369
|
+
if (typeof name !== 'string') {
|
|
370
|
+
reject('invalid-keyword-value', appendPointer(path, String(index)), `${displayPath(path)} must contain only property-name strings.`);
|
|
371
|
+
}
|
|
372
|
+
assertSafeObjectKey(name, path, appendPointer(path, String(index)));
|
|
373
|
+
if (names.has(name)) {
|
|
374
|
+
reject('invalid-keyword-value', appendPointer(path, String(index)), `${displayPath(path)} must list unique property names.`);
|
|
375
|
+
}
|
|
376
|
+
names.add(name);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function visitType(value, path, state) {
|
|
380
|
+
if (typeof value === 'string') {
|
|
381
|
+
if (!typeNames.has(value)) {
|
|
382
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} names an unknown JSON Schema type.`);
|
|
383
|
+
}
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (!Array.isArray(value) || !isDenseArray(value) || value.length === 0 || value.length > 7) {
|
|
387
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} must be a type name or a non-empty array of at most seven names.`);
|
|
388
|
+
}
|
|
389
|
+
trackObject(value, path, state);
|
|
390
|
+
const names = new Set();
|
|
391
|
+
for (const [index, name] of value.entries()) {
|
|
392
|
+
if (typeof name !== 'string' || !typeNames.has(name) || names.has(name)) {
|
|
393
|
+
reject('invalid-keyword-value', appendPointer(path, String(index)), `${displayPath(path)} must list unique, known JSON Schema type names.`);
|
|
394
|
+
}
|
|
395
|
+
names.add(name);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function visitBoundedString(value, path, maximum) {
|
|
399
|
+
if (typeof value !== 'string') {
|
|
400
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} must be a string.`);
|
|
401
|
+
}
|
|
402
|
+
if (codePointLength(value) > maximum) {
|
|
403
|
+
reject('limit-exceeded', path, `${displayPath(path)} exceeds ${maximum} characters.`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function visitNonNegativeInteger(value, path) {
|
|
407
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
408
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} must be a non-negative integer.`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function visitFiniteNumber(value, path) {
|
|
412
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
413
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} must be a finite number.`);
|
|
195
414
|
}
|
|
196
415
|
}
|
|
197
416
|
function visitJsonValue(value, path, depth, state) {
|
|
@@ -202,136 +421,398 @@ function visitJsonValue(value, path, depth, state) {
|
|
|
202
421
|
return;
|
|
203
422
|
}
|
|
204
423
|
if (depth > MAX_JSON_DEPTH) {
|
|
205
|
-
|
|
424
|
+
reject('limit-exceeded', path, `${displayPath(path)} exceeds the Studio Schema Profile JSON depth limit.`);
|
|
206
425
|
}
|
|
207
426
|
if (Array.isArray(value)) {
|
|
427
|
+
if (!isDenseArray(value)) {
|
|
428
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} must be a dense JSON array.`);
|
|
429
|
+
}
|
|
208
430
|
trackObject(value, path, state);
|
|
209
|
-
assertDenseArray(value, path);
|
|
210
431
|
if (value.length > MAX_JSON_ITEMS) {
|
|
211
|
-
|
|
432
|
+
reject('limit-exceeded', path, `${displayPath(path)} exceeds ${MAX_JSON_ITEMS} JSON items.`);
|
|
212
433
|
}
|
|
213
434
|
for (const [index, entry] of value.entries()) {
|
|
214
|
-
visitJsonValue(entry,
|
|
435
|
+
visitJsonValue(entry, appendPointer(path, String(index)), depth + 1, state);
|
|
215
436
|
}
|
|
216
437
|
return;
|
|
217
438
|
}
|
|
218
439
|
if (isRecord(value)) {
|
|
219
440
|
trackObject(value, path, state);
|
|
220
|
-
const
|
|
221
|
-
if (
|
|
222
|
-
|
|
441
|
+
const keys = Object.keys(value);
|
|
442
|
+
if (keys.length > MAX_JSON_PROPERTIES) {
|
|
443
|
+
reject('limit-exceeded', path, `${displayPath(path)} exceeds ${MAX_JSON_PROPERTIES} JSON properties.`);
|
|
223
444
|
}
|
|
224
|
-
for (const
|
|
445
|
+
for (const key of keys.sort(compareCodeUnits)) {
|
|
225
446
|
assertSafeObjectKey(key, path);
|
|
226
|
-
visitJsonValue(
|
|
447
|
+
visitJsonValue(value[key], appendPointer(path, key), depth + 1, state);
|
|
227
448
|
}
|
|
228
449
|
return;
|
|
229
450
|
}
|
|
230
|
-
|
|
451
|
+
reject('invalid-keyword-value', path, `${displayPath(path)} is not JSON-compatible.`);
|
|
452
|
+
}
|
|
453
|
+
function assertClosedObjectRoot(schema) {
|
|
454
|
+
// Root invariants follow the same UTF-16 member precedence as the general
|
|
455
|
+
// admission walk: additionalProperties sorts before type.
|
|
456
|
+
if (schema.additionalProperties !== false) {
|
|
457
|
+
reject('invalid-root', '/additionalProperties', 'Studio property schema root must declare additionalProperties: false.');
|
|
458
|
+
}
|
|
459
|
+
if (schema.type !== 'object') {
|
|
460
|
+
reject('invalid-root', '/type', 'Studio property schema root must declare exactly type "object".');
|
|
461
|
+
}
|
|
231
462
|
}
|
|
232
|
-
function
|
|
463
|
+
function captureAdmissionFailure(action, failures) {
|
|
464
|
+
try {
|
|
465
|
+
action();
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
if (error instanceof StudioSchemaProfileError) {
|
|
469
|
+
failures.push(error);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function firstAdmissionFailure(root, failures) {
|
|
476
|
+
let first;
|
|
477
|
+
for (const failure of failures) {
|
|
478
|
+
if (first === undefined ||
|
|
479
|
+
compareAdmissionPaths(root, failure.schemaPath, first.schemaPath) < 0) {
|
|
480
|
+
first = failure;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return first;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Compare two diagnostic locations in the same order the admission grammar
|
|
487
|
+
* visits them: object members by UTF-16 code unit, array members by numeric
|
|
488
|
+
* index, and a container before any descendant. Missing root invariants are
|
|
489
|
+
* virtual object members, so they participate without a special-case pass
|
|
490
|
+
* precedence.
|
|
491
|
+
*/
|
|
492
|
+
function compareAdmissionPaths(root, left, right) {
|
|
493
|
+
const leftTokens = pointerTokens(left);
|
|
494
|
+
const rightTokens = pointerTokens(right);
|
|
495
|
+
let parent = root;
|
|
496
|
+
const sharedLength = Math.min(leftTokens.length, rightTokens.length);
|
|
497
|
+
for (let index = 0; index < sharedLength; index += 1) {
|
|
498
|
+
const leftToken = leftTokens[index];
|
|
499
|
+
const rightToken = rightTokens[index];
|
|
500
|
+
if (leftToken === undefined || rightToken === undefined) {
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
if (leftToken !== rightToken) {
|
|
504
|
+
if (Array.isArray(parent)) {
|
|
505
|
+
const leftIndex = Number(leftToken);
|
|
506
|
+
const rightIndex = Number(rightToken);
|
|
507
|
+
if (Number.isSafeInteger(leftIndex) && Number.isSafeInteger(rightIndex)) {
|
|
508
|
+
return leftIndex - rightIndex;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return compareCodeUnits(leftToken, rightToken);
|
|
512
|
+
}
|
|
513
|
+
if ((isRecord(parent) || Array.isArray(parent)) && Object.hasOwn(parent, leftToken)) {
|
|
514
|
+
parent = parent[leftToken];
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
parent = undefined;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return leftTokens.length - rightTokens.length;
|
|
521
|
+
}
|
|
522
|
+
function pointerTokens(pointer) {
|
|
523
|
+
if (pointer === '') {
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
526
|
+
return pointer
|
|
527
|
+
.slice(1)
|
|
528
|
+
.split('/')
|
|
529
|
+
.map((token) => token.replaceAll('~1', '/').replaceAll('~0', '~'));
|
|
530
|
+
}
|
|
531
|
+
function assertSafeObjectKey(key, path, rejectionPath = appendPointer(path, key)) {
|
|
532
|
+
if (codePointLength(key) > MAX_OBJECT_KEY_LENGTH) {
|
|
533
|
+
reject('limit-exceeded', rejectionPath, `${displayPath(path)} contains an object member name longer than ${MAX_OBJECT_KEY_LENGTH} characters.`);
|
|
534
|
+
}
|
|
233
535
|
if (key.length === 0 ||
|
|
234
|
-
key.length > MAX_OBJECT_KEY_LENGTH ||
|
|
235
536
|
key === '__proto__' ||
|
|
236
537
|
key === 'constructor' ||
|
|
237
538
|
key === 'prototype' ||
|
|
238
539
|
containsControlCharacter(key)) {
|
|
239
|
-
|
|
540
|
+
reject('unsafe-member', rejectionPath, `${displayPath(path)} contains forbidden object member name ${JSON.stringify(key)}.`);
|
|
240
541
|
}
|
|
241
542
|
}
|
|
242
543
|
function assertNonRecursiveSchema(root) {
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
const
|
|
544
|
+
const failures = [];
|
|
545
|
+
const indexes = new Map();
|
|
546
|
+
const adjacency = [];
|
|
547
|
+
const reverseAdjacency = [];
|
|
548
|
+
const referenceSites = [];
|
|
549
|
+
const expanded = new WeakSet();
|
|
550
|
+
let eligibleReferences = 0;
|
|
551
|
+
const appendGraphPath = (parent, token) => ({
|
|
552
|
+
parent,
|
|
553
|
+
token,
|
|
554
|
+
});
|
|
555
|
+
const graphPathPointer = (path) => {
|
|
556
|
+
const tokens = [];
|
|
557
|
+
let current = path;
|
|
558
|
+
while (current !== undefined) {
|
|
559
|
+
tokens.push(current.token);
|
|
560
|
+
current = current.parent;
|
|
561
|
+
}
|
|
562
|
+
let pointer = '';
|
|
563
|
+
for (let index = tokens.length - 1; index >= 0; index -= 1) {
|
|
564
|
+
const token = tokens[index];
|
|
565
|
+
if (token !== undefined) {
|
|
566
|
+
pointer = appendPointer(pointer, token);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return pointer;
|
|
570
|
+
};
|
|
571
|
+
const ensureNode = (node) => {
|
|
572
|
+
const existing = indexes.get(node);
|
|
573
|
+
if (existing !== undefined) {
|
|
574
|
+
return existing;
|
|
575
|
+
}
|
|
576
|
+
const index = adjacency.length;
|
|
577
|
+
indexes.set(node, index);
|
|
578
|
+
adjacency.push([]);
|
|
579
|
+
reverseAdjacency.push([]);
|
|
580
|
+
return index;
|
|
581
|
+
};
|
|
582
|
+
const connect = (source, target) => {
|
|
583
|
+
adjacency[source]?.push(target);
|
|
584
|
+
reverseAdjacency[target]?.push(source);
|
|
585
|
+
};
|
|
586
|
+
ensureNode(root);
|
|
587
|
+
const stack = [
|
|
588
|
+
{ depth: 1, diagnosticsEligible: true, node: root, path: undefined },
|
|
589
|
+
];
|
|
246
590
|
while (stack.length > 0) {
|
|
247
591
|
const frame = stack.pop();
|
|
248
|
-
if (frame === undefined ||
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
if (frame.entered) {
|
|
252
|
-
active.delete(frame.node);
|
|
253
|
-
finished.add(frame.node);
|
|
592
|
+
if (frame === undefined || expanded.has(frame.node)) {
|
|
254
593
|
continue;
|
|
255
594
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
for (let index = dependencies.length - 1; index >= 0; index -= 1) {
|
|
263
|
-
const dependency = dependencies[index];
|
|
264
|
-
if (dependency === undefined || finished.has(dependency)) {
|
|
265
|
-
continue;
|
|
595
|
+
expanded.add(frame.node);
|
|
596
|
+
const source = ensureNode(frame.node);
|
|
597
|
+
const children = [];
|
|
598
|
+
const addChild = (value, path, diagnosticsEligible = frame.diagnosticsEligible) => {
|
|
599
|
+
if (!isRecord(value)) {
|
|
600
|
+
return;
|
|
266
601
|
}
|
|
267
|
-
|
|
268
|
-
|
|
602
|
+
const target = ensureNode(value);
|
|
603
|
+
connect(source, target);
|
|
604
|
+
const depth = frame.depth + 1;
|
|
605
|
+
children.push({
|
|
606
|
+
depth,
|
|
607
|
+
diagnosticsEligible: diagnosticsEligible && depth <= MAX_SCHEMA_DEPTH,
|
|
608
|
+
node: value,
|
|
609
|
+
path,
|
|
610
|
+
});
|
|
611
|
+
};
|
|
612
|
+
for (const [keyword, operand] of boundedSchemaEntries(frame.node)) {
|
|
613
|
+
const keywordPath = appendGraphPath(frame.path, keyword);
|
|
614
|
+
switch (keyword) {
|
|
615
|
+
case '$defs':
|
|
616
|
+
case 'properties':
|
|
617
|
+
if (isRecord(operand)) {
|
|
618
|
+
const names = Object.keys(operand);
|
|
619
|
+
const childrenEligible = names.length <= MAX_SCHEMA_MAP_PROPERTIES;
|
|
620
|
+
if (childrenEligible) {
|
|
621
|
+
names.sort(compareCodeUnits);
|
|
622
|
+
}
|
|
623
|
+
for (const name of names) {
|
|
624
|
+
addChild(operand[name], appendGraphPath(keywordPath, name), frame.diagnosticsEligible && childrenEligible);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
break;
|
|
628
|
+
case '$ref':
|
|
629
|
+
if (isPortableLocalReference(operand)) {
|
|
630
|
+
const reportsDiagnostic = frame.diagnosticsEligible && (eligibleReferences += 1) <= MAX_REFERENCES;
|
|
631
|
+
const referencePath = reportsDiagnostic ? graphPathPointer(keywordPath) : '';
|
|
632
|
+
try {
|
|
633
|
+
const target = resolveLocalReference(root, operand, referencePath);
|
|
634
|
+
if (!target.schemaPosition) {
|
|
635
|
+
if (reportsDiagnostic) {
|
|
636
|
+
failures.push(new StudioSchemaProfileError('invalid-reference', referencePath, `Local schema reference ${operand} does not resolve to a schema position.`));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
else if (isRecord(target.value)) {
|
|
640
|
+
const targetIndex = ensureNode(target.value);
|
|
641
|
+
connect(source, targetIndex);
|
|
642
|
+
if (reportsDiagnostic) {
|
|
643
|
+
referenceSites.push({ path: referencePath, source, target: targetIndex });
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
if (error instanceof StudioSchemaProfileError) {
|
|
649
|
+
if (reportsDiagnostic) {
|
|
650
|
+
failures.push(error);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
throw error;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
break;
|
|
659
|
+
case 'additionalProperties':
|
|
660
|
+
case 'else':
|
|
661
|
+
case 'if':
|
|
662
|
+
case 'items':
|
|
663
|
+
case 'not':
|
|
664
|
+
case 'propertyNames':
|
|
665
|
+
case 'then':
|
|
666
|
+
addChild(operand, keywordPath);
|
|
667
|
+
break;
|
|
668
|
+
case 'allOf':
|
|
669
|
+
case 'anyOf':
|
|
670
|
+
case 'oneOf':
|
|
671
|
+
case 'prefixItems':
|
|
672
|
+
if (Array.isArray(operand)) {
|
|
673
|
+
const childrenEligible = operand.length > 0 && operand.length <= MAX_ALTERNATIVES && isDenseArray(operand);
|
|
674
|
+
for (let index = 0; index < operand.length; index += 1) {
|
|
675
|
+
if (Object.hasOwn(operand, index)) {
|
|
676
|
+
addChild(operand[index], appendGraphPath(keywordPath, String(index)), frame.diagnosticsEligible && childrenEligible);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
break;
|
|
269
681
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const dependencies = [];
|
|
276
|
-
for (const keyword of [
|
|
277
|
-
'additionalProperties',
|
|
278
|
-
'else',
|
|
279
|
-
'if',
|
|
280
|
-
'items',
|
|
281
|
-
'not',
|
|
282
|
-
'propertyNames',
|
|
283
|
-
'then',
|
|
284
|
-
]) {
|
|
285
|
-
appendSchemaDependency(dependencies, schema[keyword]);
|
|
286
|
-
}
|
|
287
|
-
for (const keyword of ['allOf', 'anyOf', 'oneOf', 'prefixItems']) {
|
|
288
|
-
const candidates = schema[keyword];
|
|
289
|
-
if (Array.isArray(candidates)) {
|
|
290
|
-
for (const candidate of candidates) {
|
|
291
|
-
appendSchemaDependency(dependencies, candidate);
|
|
682
|
+
}
|
|
683
|
+
for (let index = children.length - 1; index >= 0; index -= 1) {
|
|
684
|
+
const child = children[index];
|
|
685
|
+
if (child !== undefined) {
|
|
686
|
+
stack.push(child);
|
|
292
687
|
}
|
|
293
688
|
}
|
|
294
689
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (
|
|
298
|
-
|
|
299
|
-
appendSchemaDependency(dependencies, candidate);
|
|
300
|
-
}
|
|
690
|
+
const components = stronglyConnectedComponents(adjacency, reverseAdjacency);
|
|
691
|
+
for (const site of referenceSites) {
|
|
692
|
+
if (components[site.source] === components[site.target]) {
|
|
693
|
+
failures.push(new StudioSchemaProfileError('recursive-schema', site.path, 'Recursive contributed schemas are not admitted by the alpha profile.'));
|
|
301
694
|
}
|
|
302
695
|
}
|
|
303
|
-
|
|
304
|
-
|
|
696
|
+
const failure = firstAdmissionFailure(root, failures);
|
|
697
|
+
if (failure !== undefined) {
|
|
698
|
+
throw failure;
|
|
305
699
|
}
|
|
306
|
-
return dependencies;
|
|
307
700
|
}
|
|
308
|
-
function
|
|
309
|
-
|
|
310
|
-
|
|
701
|
+
function stronglyConnectedComponents(adjacency, reverseAdjacency) {
|
|
702
|
+
const visited = new Uint8Array(adjacency.length);
|
|
703
|
+
const finishOrder = [];
|
|
704
|
+
for (let start = 0; start < adjacency.length; start += 1) {
|
|
705
|
+
if (visited[start] !== 0) {
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
visited[start] = 1;
|
|
709
|
+
const stack = [{ edge: 0, node: start }];
|
|
710
|
+
while (stack.length > 0) {
|
|
711
|
+
const frame = stack[stack.length - 1];
|
|
712
|
+
if (frame === undefined) {
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
const edges = adjacency[frame.node] ?? [];
|
|
716
|
+
const target = edges[frame.edge];
|
|
717
|
+
if (target !== undefined) {
|
|
718
|
+
frame.edge += 1;
|
|
719
|
+
if (visited[target] === 0) {
|
|
720
|
+
visited[target] = 1;
|
|
721
|
+
stack.push({ edge: 0, node: target });
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
finishOrder.push(frame.node);
|
|
726
|
+
stack.pop();
|
|
727
|
+
}
|
|
728
|
+
}
|
|
311
729
|
}
|
|
730
|
+
const components = new Int32Array(adjacency.length);
|
|
731
|
+
components.fill(-1);
|
|
732
|
+
let component = 0;
|
|
733
|
+
for (let order = finishOrder.length - 1; order >= 0; order -= 1) {
|
|
734
|
+
const start = finishOrder[order];
|
|
735
|
+
if (start === undefined || components[start] !== -1) {
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
components[start] = component;
|
|
739
|
+
const stack = [start];
|
|
740
|
+
while (stack.length > 0) {
|
|
741
|
+
const node = stack.pop();
|
|
742
|
+
if (node === undefined) {
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
for (const source of reverseAdjacency[node] ?? []) {
|
|
746
|
+
if (components[source] === -1) {
|
|
747
|
+
components[source] = component;
|
|
748
|
+
stack.push(source);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
component += 1;
|
|
753
|
+
}
|
|
754
|
+
return components;
|
|
312
755
|
}
|
|
313
|
-
function resolveLocalReference(root, reference) {
|
|
756
|
+
function resolveLocalReference(root, reference, path) {
|
|
314
757
|
if (reference === '#') {
|
|
315
|
-
return root;
|
|
758
|
+
return { schemaPosition: true, value: root };
|
|
316
759
|
}
|
|
317
760
|
let current = root;
|
|
761
|
+
let position = 'schema';
|
|
318
762
|
for (const encodedToken of reference.slice(2).split('/')) {
|
|
319
|
-
if (/(?:~[^01]|~$)/u.test(encodedToken)) {
|
|
320
|
-
throw new TypeError(`Local schema reference ${reference} is not a valid JSON Pointer.`);
|
|
321
|
-
}
|
|
322
763
|
const token = encodedToken.replaceAll('~1', '/').replaceAll('~0', '~');
|
|
764
|
+
let nextPosition = 'other';
|
|
765
|
+
if (position === 'schema' && isRecord(current)) {
|
|
766
|
+
switch (token) {
|
|
767
|
+
case '$defs':
|
|
768
|
+
case 'properties':
|
|
769
|
+
nextPosition = 'schema-map';
|
|
770
|
+
break;
|
|
771
|
+
case 'additionalProperties':
|
|
772
|
+
case 'else':
|
|
773
|
+
case 'if':
|
|
774
|
+
case 'items':
|
|
775
|
+
case 'not':
|
|
776
|
+
case 'propertyNames':
|
|
777
|
+
case 'then':
|
|
778
|
+
nextPosition = 'schema';
|
|
779
|
+
break;
|
|
780
|
+
case 'allOf':
|
|
781
|
+
case 'anyOf':
|
|
782
|
+
case 'oneOf':
|
|
783
|
+
case 'prefixItems':
|
|
784
|
+
nextPosition = 'schema-array';
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
else if (position === 'schema-map' && isRecord(current)) {
|
|
789
|
+
nextPosition = 'schema';
|
|
790
|
+
}
|
|
791
|
+
else if (position === 'schema-array' && Array.isArray(current)) {
|
|
792
|
+
nextPosition = 'schema';
|
|
793
|
+
}
|
|
323
794
|
if (!isRecord(current) && !Array.isArray(current)) {
|
|
324
|
-
|
|
795
|
+
reject('invalid-reference', path, `Local schema reference ${reference} does not resolve to a schema.`);
|
|
325
796
|
}
|
|
326
797
|
if (!Object.hasOwn(current, token)) {
|
|
327
|
-
|
|
798
|
+
reject('invalid-reference', path, `Local schema reference ${reference} does not resolve to a schema.`);
|
|
328
799
|
}
|
|
329
800
|
current = current[token];
|
|
801
|
+
position = nextPosition;
|
|
330
802
|
}
|
|
331
803
|
if (typeof current !== 'boolean' && !isRecord(current)) {
|
|
332
|
-
|
|
804
|
+
reject('invalid-reference', path, `Local schema reference ${reference} does not resolve to a schema.`);
|
|
333
805
|
}
|
|
334
|
-
return current;
|
|
806
|
+
return { schemaPosition: position === 'schema', value: current };
|
|
807
|
+
}
|
|
808
|
+
function reject(code, schemaPath, message, cause) {
|
|
809
|
+
throw new StudioSchemaProfileError(code, schemaPath, message, cause === undefined ? undefined : { cause });
|
|
810
|
+
}
|
|
811
|
+
function appendPointer(pointer, token) {
|
|
812
|
+
return `${pointer}/${token.replaceAll('~', '~0').replaceAll('/', '~1')}`;
|
|
813
|
+
}
|
|
814
|
+
function displayPath(path) {
|
|
815
|
+
return path === '' ? 'schema root' : path;
|
|
335
816
|
}
|
|
336
817
|
function containsControlCharacter(value) {
|
|
337
818
|
for (let index = 0; index < value.length; index += 1) {
|
|
@@ -342,49 +823,193 @@ function containsControlCharacter(value) {
|
|
|
342
823
|
}
|
|
343
824
|
return false;
|
|
344
825
|
}
|
|
345
|
-
function
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
826
|
+
function isPortableLocalReference(value) {
|
|
827
|
+
return (typeof value === 'string' &&
|
|
828
|
+
codePointLength(value) <= MAX_REFERENCE_LENGTH &&
|
|
829
|
+
!containsControlCharacter(value) &&
|
|
830
|
+
/^#(?:\/(?:[A-Za-z0-9._!$&'()*+,;=:@-]|~[01])*)*$/u.test(value));
|
|
350
831
|
}
|
|
351
|
-
function
|
|
352
|
-
|
|
353
|
-
|
|
832
|
+
function codePointLength(value) {
|
|
833
|
+
let length = 0;
|
|
834
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
835
|
+
length += 1;
|
|
836
|
+
const code = value.charCodeAt(index);
|
|
837
|
+
if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) {
|
|
838
|
+
const next = value.charCodeAt(index + 1);
|
|
839
|
+
if ((next & 0xfc00) === 0xdc00) {
|
|
840
|
+
index += 1;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
354
843
|
}
|
|
355
|
-
|
|
844
|
+
return length;
|
|
356
845
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
846
|
+
/**
|
|
847
|
+
* Enforce the canonical byte ceiling without materialising or sorting the
|
|
848
|
+
* canonical document. Object order cannot change encoded length. This pass
|
|
849
|
+
* is iterative so an over-deep untrusted value cannot exhaust the JavaScript
|
|
850
|
+
* call stack before the published schema/JSON depth checks run.
|
|
851
|
+
*/
|
|
852
|
+
function assertCanonicalSchemaByteBudget(root) {
|
|
853
|
+
const stack = [root];
|
|
854
|
+
const seen = new WeakSet();
|
|
855
|
+
let bytes = 0;
|
|
856
|
+
const consume = (amount) => {
|
|
857
|
+
bytes += amount;
|
|
858
|
+
if (bytes > MAX_SCHEMA_BYTES) {
|
|
859
|
+
throw new SchemaByteLimitError();
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
while (stack.length > 0) {
|
|
863
|
+
const value = stack.pop();
|
|
864
|
+
if (value === null) {
|
|
865
|
+
consume(4);
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
switch (typeof value) {
|
|
869
|
+
case 'boolean':
|
|
870
|
+
consume(value ? 4 : 5);
|
|
871
|
+
continue;
|
|
872
|
+
case 'number': {
|
|
873
|
+
if (!Number.isFinite(value)) {
|
|
874
|
+
throw new SchemaBytePreflightDeferred();
|
|
875
|
+
}
|
|
876
|
+
const encoded = JSON.stringify(Object.is(value, -0) ? 0 : value);
|
|
877
|
+
consume(encoded.length);
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
case 'string':
|
|
881
|
+
consumeCanonicalJsonString(value, consume);
|
|
882
|
+
continue;
|
|
883
|
+
case 'object':
|
|
884
|
+
break;
|
|
885
|
+
default:
|
|
886
|
+
throw new SchemaBytePreflightDeferred();
|
|
887
|
+
}
|
|
888
|
+
if (seen.has(value)) {
|
|
889
|
+
throw new SchemaBytePreflightDeferred();
|
|
890
|
+
}
|
|
891
|
+
seen.add(value);
|
|
892
|
+
if (Array.isArray(value)) {
|
|
893
|
+
const members = value;
|
|
894
|
+
// Length alone accounts for brackets and separators. Reject an
|
|
895
|
+
// obviously oversized array before Object.keys allocates an entry for
|
|
896
|
+
// every member while checking density.
|
|
897
|
+
consume(2 + Math.max(0, members.length - 1));
|
|
898
|
+
if (!isDenseArray(members)) {
|
|
899
|
+
throw new SchemaBytePreflightDeferred();
|
|
900
|
+
}
|
|
901
|
+
for (let index = members.length - 1; index >= 0; index -= 1) {
|
|
902
|
+
const member = members[index];
|
|
903
|
+
if (member === undefined) {
|
|
904
|
+
throw new SchemaBytePreflightDeferred();
|
|
905
|
+
}
|
|
906
|
+
stack.push(member);
|
|
907
|
+
}
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
if (!isRecord(value)) {
|
|
911
|
+
throw new SchemaBytePreflightDeferred();
|
|
912
|
+
}
|
|
913
|
+
const keys = Object.keys(value);
|
|
914
|
+
consume(2 + Math.max(0, keys.length - 1));
|
|
915
|
+
for (let index = keys.length - 1; index >= 0; index -= 1) {
|
|
916
|
+
const key = keys[index];
|
|
917
|
+
if (key === undefined) {
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
const member = value[key];
|
|
921
|
+
if (member === undefined) {
|
|
922
|
+
throw new SchemaBytePreflightDeferred();
|
|
923
|
+
}
|
|
924
|
+
consumeCanonicalJsonString(key, consume);
|
|
925
|
+
consume(1);
|
|
926
|
+
stack.push(member);
|
|
927
|
+
}
|
|
360
928
|
}
|
|
361
|
-
const prototype = Object.getPrototypeOf(value);
|
|
362
|
-
return prototype === Object.prototype || prototype === null;
|
|
363
929
|
}
|
|
364
|
-
function
|
|
365
|
-
|
|
930
|
+
function consumeCanonicalJsonString(value, consume) {
|
|
931
|
+
consume(2); // opening and closing quotes
|
|
366
932
|
for (let index = 0; index < value.length; index += 1) {
|
|
367
933
|
const code = value.charCodeAt(index);
|
|
368
|
-
if (code
|
|
369
|
-
|
|
934
|
+
if (code === 0x22 ||
|
|
935
|
+
code === 0x5c ||
|
|
936
|
+
code === 0x08 ||
|
|
937
|
+
code === 0x09 ||
|
|
938
|
+
code === 0x0a ||
|
|
939
|
+
code === 0x0c ||
|
|
940
|
+
code === 0x0d) {
|
|
941
|
+
consume(2);
|
|
942
|
+
}
|
|
943
|
+
else if (code <= 0x1f) {
|
|
944
|
+
consume(6);
|
|
945
|
+
}
|
|
946
|
+
else if (code <= 0x7f) {
|
|
947
|
+
consume(1);
|
|
370
948
|
}
|
|
371
949
|
else if (code <= 0x7ff) {
|
|
372
|
-
|
|
950
|
+
consume(2);
|
|
373
951
|
}
|
|
374
952
|
else if (code >= 0xd800 && code <= 0xdbff) {
|
|
375
953
|
const next = value.charCodeAt(index + 1);
|
|
376
|
-
if (
|
|
377
|
-
|
|
954
|
+
if (index + 1 < value.length && (next & 0xfc00) === 0xdc00) {
|
|
955
|
+
consume(4);
|
|
378
956
|
index += 1;
|
|
379
957
|
}
|
|
380
958
|
else {
|
|
381
|
-
|
|
959
|
+
consume(6);
|
|
382
960
|
}
|
|
383
961
|
}
|
|
962
|
+
else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
963
|
+
consume(6);
|
|
964
|
+
}
|
|
384
965
|
else {
|
|
385
|
-
|
|
966
|
+
consume(3);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Sort at most the closed keyword set plus the first invalid member. This
|
|
972
|
+
* preserves deterministic first-error precedence without sorting an
|
|
973
|
+
* arbitrarily large attacker-controlled schema object.
|
|
974
|
+
*/
|
|
975
|
+
function boundedSchemaEntries(value) {
|
|
976
|
+
const keys = Object.keys(value);
|
|
977
|
+
if (keys.length <= allowedKeywords.size) {
|
|
978
|
+
return keys.sort(compareCodeUnits).map((key) => [key, value[key]]);
|
|
979
|
+
}
|
|
980
|
+
const candidates = [];
|
|
981
|
+
let firstInvalid;
|
|
982
|
+
for (const key of keys) {
|
|
983
|
+
if (allowedKeywords.has(key)) {
|
|
984
|
+
candidates.push(key);
|
|
985
|
+
}
|
|
986
|
+
else if (firstInvalid === undefined || compareCodeUnits(key, firstInvalid) < 0) {
|
|
987
|
+
firstInvalid = key;
|
|
386
988
|
}
|
|
387
989
|
}
|
|
388
|
-
|
|
990
|
+
if (firstInvalid !== undefined) {
|
|
991
|
+
candidates.push(firstInvalid);
|
|
992
|
+
}
|
|
993
|
+
return candidates.sort(compareCodeUnits).map((key) => [key, value[key]]);
|
|
994
|
+
}
|
|
995
|
+
function isDenseArray(value) {
|
|
996
|
+
const keys = Object.keys(value);
|
|
997
|
+
return keys.length === value.length && keys.every((key, index) => key === String(index));
|
|
998
|
+
}
|
|
999
|
+
function compareCodeUnits(left, right) {
|
|
1000
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
1001
|
+
}
|
|
1002
|
+
function trackObject(value, path, state) {
|
|
1003
|
+
if (state.seen.has(value)) {
|
|
1004
|
+
reject('invalid-root', path, `${displayPath(path)} reuses or cycles a JSON object.`);
|
|
1005
|
+
}
|
|
1006
|
+
state.seen.add(value);
|
|
1007
|
+
}
|
|
1008
|
+
function isRecord(value) {
|
|
1009
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1013
|
+
return prototype === Object.prototype || prototype === null;
|
|
389
1014
|
}
|
|
390
1015
|
//# sourceMappingURL=schema-profile.js.map
|