@huggingface/transformers-structured-output 4.3.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 +202 -0
- package/README.md +56 -0
- package/dist/index.cjs +2425 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.js +2402 -0
- package/dist/index.js.map +7 -0
- package/package.json +65 -0
- package/src/StructuredOutputProcessor.ts +103 -0
- package/src/engine/constraint.ts +331 -0
- package/src/engine/index.ts +2 -0
- package/src/engine/json.ts +1853 -0
- package/src/engine/regex.ts +419 -0
- package/src/engine/tokenizer.ts +205 -0
- package/src/engine/types.ts +12 -0
- package/src/index.ts +2 -0
- package/src/utils/mask.ts +30 -0
- package/types/StructuredOutputProcessor.d.ts +22 -0
- package/types/StructuredOutputProcessor.d.ts.map +1 -0
- package/types/engine/constraint.d.ts +30 -0
- package/types/engine/constraint.d.ts.map +1 -0
- package/types/engine/index.d.ts +3 -0
- package/types/engine/index.d.ts.map +1 -0
- package/types/engine/json.d.ts +70 -0
- package/types/engine/json.d.ts.map +1 -0
- package/types/engine/regex.d.ts +3 -0
- package/types/engine/regex.d.ts.map +1 -0
- package/types/engine/tokenizer.d.ts +8 -0
- package/types/engine/tokenizer.d.ts.map +1 -0
- package/types/engine/types.d.ts +16 -0
- package/types/engine/types.d.ts.map +1 -0
- package/types/index.d.ts +3 -0
- package/types/index.d.ts.map +1 -0
- package/types/utils/mask.d.ts +3 -0
- package/types/utils/mask.d.ts.map +1 -0
|
@@ -0,0 +1,1853 @@
|
|
|
1
|
+
import type { ConstraintState, JSONSchema } from './types';
|
|
2
|
+
|
|
3
|
+
type Schema = boolean | Record<string, unknown>;
|
|
4
|
+
type Kind = 'null' | 'boolean' | 'number' | 'integer' | 'string' | 'array' | 'object';
|
|
5
|
+
type JsonNode =
|
|
6
|
+
| { kind: 'null'; value: null }
|
|
7
|
+
| { kind: 'boolean'; value: boolean }
|
|
8
|
+
| { kind: 'number'; value: number; raw: string }
|
|
9
|
+
| { kind: 'string'; value: string }
|
|
10
|
+
| { kind: 'array'; value: unknown[]; items: JsonNode[] }
|
|
11
|
+
| { kind: 'object'; value: Record<string, unknown>; entries: Array<{ key: string; node: JsonNode }> };
|
|
12
|
+
type Guidance = {
|
|
13
|
+
itemSeparator: string;
|
|
14
|
+
keySeparator: string;
|
|
15
|
+
itemBytes: Uint8Array;
|
|
16
|
+
keyBytes: Uint8Array;
|
|
17
|
+
whitespaceFlexible: boolean;
|
|
18
|
+
};
|
|
19
|
+
type Mode =
|
|
20
|
+
| 'value'
|
|
21
|
+
| 'array-value'
|
|
22
|
+
| 'object-key'
|
|
23
|
+
| 'colon'
|
|
24
|
+
| 'item-separator'
|
|
25
|
+
| 'after-value'
|
|
26
|
+
| 'string'
|
|
27
|
+
| 'key-string'
|
|
28
|
+
| 'escape'
|
|
29
|
+
| 'key-escape'
|
|
30
|
+
| 'unicode'
|
|
31
|
+
| 'key-unicode'
|
|
32
|
+
| 'number'
|
|
33
|
+
| 'literal'
|
|
34
|
+
| 'done'
|
|
35
|
+
| 'dead';
|
|
36
|
+
|
|
37
|
+
type ObjectFrame = {
|
|
38
|
+
kind: 'object';
|
|
39
|
+
schema: Schema;
|
|
40
|
+
seen: ReadonlySet<string>;
|
|
41
|
+
entries: ReadonlyArray<{ key: string; node: JsonNode }>;
|
|
42
|
+
key?: string;
|
|
43
|
+
childSchema?: Schema;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type ArrayFrame = {
|
|
47
|
+
kind: 'array';
|
|
48
|
+
schema: Schema;
|
|
49
|
+
length: number;
|
|
50
|
+
items: readonly JsonNode[];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type Frame = ObjectFrame | ArrayFrame;
|
|
54
|
+
|
|
55
|
+
type JsonState = {
|
|
56
|
+
mode: Mode;
|
|
57
|
+
schema: Schema;
|
|
58
|
+
stack: readonly Frame[];
|
|
59
|
+
bytes?: readonly number[];
|
|
60
|
+
// Code points started in the current string and UTF-8 continuation bytes
|
|
61
|
+
// still expected (-1 once the byte sequence can no longer decode). Both are
|
|
62
|
+
// pure functions of `bytes`, maintained incrementally so that length checks
|
|
63
|
+
// do not have to re-decode the prefix on every byte.
|
|
64
|
+
stringLength?: number;
|
|
65
|
+
stringPending?: number;
|
|
66
|
+
text?: string;
|
|
67
|
+
literal?: string;
|
|
68
|
+
literalValue?: null | boolean;
|
|
69
|
+
index?: number;
|
|
70
|
+
highSurrogate?: number;
|
|
71
|
+
guidance: Guidance;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const DEFAULT_GUIDANCE: Guidance = {
|
|
75
|
+
itemSeparator: ',',
|
|
76
|
+
keySeparator: ':',
|
|
77
|
+
itemBytes: Uint8Array.of(0x2c),
|
|
78
|
+
keyBytes: Uint8Array.of(0x3a),
|
|
79
|
+
whitespaceFlexible: true,
|
|
80
|
+
};
|
|
81
|
+
const DEAD: JsonState = { mode: 'dead', schema: false, stack: [], guidance: DEFAULT_GUIDANCE };
|
|
82
|
+
const encoder = new TextEncoder();
|
|
83
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
84
|
+
const schemaIds = new WeakMap<object, number>();
|
|
85
|
+
const propertyKeyBytes = new WeakMap<object, ReadonlyArray<{ key: string; bytes: Uint8Array }> | null>();
|
|
86
|
+
const finiteStringCache = new WeakMap<object, ReadonlyArray<{ value: string; bytes: Uint8Array }> | null>();
|
|
87
|
+
let nextSchemaId = 0;
|
|
88
|
+
const SIMPLE_ESCAPES: Record<number, string> = {
|
|
89
|
+
0x22: '"',
|
|
90
|
+
0x2f: '/',
|
|
91
|
+
0x5c: '\\',
|
|
92
|
+
0x62: '\b',
|
|
93
|
+
0x66: '\f',
|
|
94
|
+
0x6e: '\n',
|
|
95
|
+
0x72: '\r',
|
|
96
|
+
0x74: '\t',
|
|
97
|
+
};
|
|
98
|
+
const ASSERTED_FORMATS = new Set([
|
|
99
|
+
'date',
|
|
100
|
+
'time',
|
|
101
|
+
'date-time',
|
|
102
|
+
'duration',
|
|
103
|
+
'email',
|
|
104
|
+
'hostname',
|
|
105
|
+
'ipv4',
|
|
106
|
+
'ipv6',
|
|
107
|
+
'uuid',
|
|
108
|
+
'uri',
|
|
109
|
+
'uri-reference',
|
|
110
|
+
'regex',
|
|
111
|
+
'json-pointer',
|
|
112
|
+
'relative-json-pointer',
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
export function compileJsonSchema(schema: JSONSchema, stringKeyClamp = Infinity): ConstraintState<JsonState> {
|
|
116
|
+
registerSchemaContext(schema, schema);
|
|
117
|
+
checkSchema(schema, '$');
|
|
118
|
+
checkReferences(schema);
|
|
119
|
+
const guidance = guidanceFrom(schema);
|
|
120
|
+
return {
|
|
121
|
+
initial: { mode: 'value', schema, stack: [], guidance },
|
|
122
|
+
transition,
|
|
123
|
+
viable: (state) => state !== DEAD,
|
|
124
|
+
accepting: isAccepting,
|
|
125
|
+
stringCapacity,
|
|
126
|
+
maskKey: (state) => stateMaskKey(state, stringKeyClamp),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function stringCapacity(state: JsonState): number | undefined {
|
|
131
|
+
if (state.mode !== 'string' || state.highSurrogate !== undefined || finiteStringValues(state.schema) !== null)
|
|
132
|
+
return undefined;
|
|
133
|
+
if ((state.stringPending ?? 0) !== 0) return undefined;
|
|
134
|
+
const maximum = directStringMaxLength(state.schema);
|
|
135
|
+
if (maximum === undefined) return Infinity;
|
|
136
|
+
return maximum - (state.stringLength ?? 0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function stateMaskKey(state: JsonState, stringKeyClamp: number): string | undefined {
|
|
140
|
+
if (state === DEAD) return undefined;
|
|
141
|
+
const frames = state.stack.map((frame) => {
|
|
142
|
+
if (frame.kind === 'object') {
|
|
143
|
+
const entries = [...frame.entries]
|
|
144
|
+
.sort((left, right) => left.key.localeCompare(right.key))
|
|
145
|
+
.map((entry) => `${JSON.stringify(entry.key)}:${nodeKey(entry.node)}`)
|
|
146
|
+
.join(',');
|
|
147
|
+
return `o${schemaId(frame.schema)}[${entries}]${frame.key === undefined ? '' : `:${JSON.stringify(frame.key)}:${schemaId(frame.childSchema!)}`}`;
|
|
148
|
+
}
|
|
149
|
+
return `a${schemaId(frame.schema)}[${frame.items.map(nodeKey).join(',')}]`;
|
|
150
|
+
});
|
|
151
|
+
const key = [
|
|
152
|
+
state.mode,
|
|
153
|
+
schemaId(state.schema),
|
|
154
|
+
state.bytes === undefined ? '' : (stringLengthKey(state, stringKeyClamp) ?? bytesKey(state.bytes)),
|
|
155
|
+
state.text ?? '',
|
|
156
|
+
state.literal ?? '',
|
|
157
|
+
state.index ?? '',
|
|
158
|
+
state.highSurrogate ?? '',
|
|
159
|
+
...frames,
|
|
160
|
+
].join('|');
|
|
161
|
+
return key.length <= 2048 ? key : undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// While inside a plain length-constrained string, the token mask depends only on
|
|
165
|
+
// the remaining capacity (clamped to the longest token) and whether minLength is
|
|
166
|
+
// already satisfied — not on the actual bytes typed so far. Keying on that makes
|
|
167
|
+
// every string-content step after the first share one cached mask. This is only
|
|
168
|
+
// sound when nothing can inspect the string's content: neither the string schema
|
|
169
|
+
// itself (pattern/format/const/enum or composition keywords) nor any enclosing
|
|
170
|
+
// frame (cross-value keywords such as uniqueItems, contains, or conditionals).
|
|
171
|
+
const CONTENT_DEPENDENT_STRING_KEYWORDS = [
|
|
172
|
+
'pattern',
|
|
173
|
+
'format',
|
|
174
|
+
'const',
|
|
175
|
+
'enum',
|
|
176
|
+
'$ref',
|
|
177
|
+
'allOf',
|
|
178
|
+
'anyOf',
|
|
179
|
+
'oneOf',
|
|
180
|
+
'not',
|
|
181
|
+
'if',
|
|
182
|
+
];
|
|
183
|
+
const CONTENT_DEPENDENT_FRAME_KEYWORDS = [
|
|
184
|
+
'$ref',
|
|
185
|
+
'allOf',
|
|
186
|
+
'anyOf',
|
|
187
|
+
'oneOf',
|
|
188
|
+
'not',
|
|
189
|
+
'if',
|
|
190
|
+
'uniqueItems',
|
|
191
|
+
'contains',
|
|
192
|
+
'dependentSchemas',
|
|
193
|
+
'dependencies',
|
|
194
|
+
];
|
|
195
|
+
const contentIndependentStrings = new WeakMap<object, boolean>();
|
|
196
|
+
const contentNeutralFrames = new WeakMap<object, boolean>();
|
|
197
|
+
|
|
198
|
+
function isContentIndependent(schema: Schema, keywords: readonly string[], cache: WeakMap<object, boolean>): boolean {
|
|
199
|
+
if (schema === true) return true;
|
|
200
|
+
if (schema === false) return false;
|
|
201
|
+
let result = cache.get(schema);
|
|
202
|
+
if (result === undefined) {
|
|
203
|
+
result = keywords.every((keyword) => schema[keyword] === undefined);
|
|
204
|
+
cache.set(schema, result);
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function stringLengthKey(state: JsonState, clamp: number): string | undefined {
|
|
210
|
+
if (state.mode !== 'string' || state.highSurrogate !== undefined) return undefined;
|
|
211
|
+
if (!isContentIndependent(state.schema, CONTENT_DEPENDENT_STRING_KEYWORDS, contentIndependentStrings)) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
for (const frame of state.stack) {
|
|
215
|
+
if (!isContentIndependent(frame.schema, CONTENT_DEPENDENT_FRAME_KEYWORDS, contentNeutralFrames)) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if ((state.stringPending ?? 0) !== 0) return undefined;
|
|
220
|
+
const length = state.stringLength ?? 0;
|
|
221
|
+
const maximum = directStringMaxLength(state.schema);
|
|
222
|
+
const remaining = Math.min(maximum === undefined ? Infinity : maximum - length, clamp);
|
|
223
|
+
const minimum =
|
|
224
|
+
state.schema !== true && state.schema !== false && typeof state.schema.minLength === 'number'
|
|
225
|
+
? state.schema.minLength
|
|
226
|
+
: 0;
|
|
227
|
+
return `#${remaining === Infinity ? 'inf' : remaining}:${length >= minimum ? '' : length}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function nodeKey(node: JsonNode): string {
|
|
231
|
+
if (node.kind === 'number') return `n${node.raw}`;
|
|
232
|
+
if (node.kind === 'string') return `s${JSON.stringify(node.value)}`;
|
|
233
|
+
if (node.kind === 'boolean') return node.value ? 't' : 'f';
|
|
234
|
+
if (node.kind === 'null') return 'z';
|
|
235
|
+
if (node.kind === 'array') return `[${node.items.map(nodeKey).join(',')}]`;
|
|
236
|
+
return `{${[...node.entries]
|
|
237
|
+
.sort((left, right) => left.key.localeCompare(right.key))
|
|
238
|
+
.map((entry) => `${JSON.stringify(entry.key)}:${nodeKey(entry.node)}`)
|
|
239
|
+
.join(',')}}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function bytesKey(bytes: readonly number[]): string {
|
|
243
|
+
let result = '';
|
|
244
|
+
for (const byte of bytes) result += byte.toString(16).padStart(2, '0');
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function schemaId(schema: Schema): string | number {
|
|
249
|
+
if (schema === true) return 't';
|
|
250
|
+
if (schema === false) return 'f';
|
|
251
|
+
let id = schemaIds.get(schema);
|
|
252
|
+
if (id === undefined) {
|
|
253
|
+
id = nextSchemaId++;
|
|
254
|
+
schemaIds.set(schema, id);
|
|
255
|
+
}
|
|
256
|
+
return id;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function transition(state: JsonState, byte: number): JsonState {
|
|
260
|
+
if (state === DEAD) return DEAD;
|
|
261
|
+
if (
|
|
262
|
+
state.guidance.whitespaceFlexible &&
|
|
263
|
+
isWhitespace(byte) &&
|
|
264
|
+
allowsWhitespace(state.mode) &&
|
|
265
|
+
!separatorExpects(state, byte)
|
|
266
|
+
)
|
|
267
|
+
return state;
|
|
268
|
+
|
|
269
|
+
switch (state.mode) {
|
|
270
|
+
case 'value':
|
|
271
|
+
case 'array-value':
|
|
272
|
+
if (state.mode === 'array-value' && byte === 0x5d) return closeArray(state);
|
|
273
|
+
if (state.mode === 'array-value' && exceedsMaxItems(state.stack.at(-1) as ArrayFrame)) return DEAD;
|
|
274
|
+
return startValue(state, byte);
|
|
275
|
+
case 'object-key':
|
|
276
|
+
if (byte === 0x7d) return closeObject(state);
|
|
277
|
+
return byte === 0x22
|
|
278
|
+
? { ...state, mode: 'key-string', bytes: [], stringLength: 0, stringPending: 0 }
|
|
279
|
+
: DEAD;
|
|
280
|
+
case 'colon':
|
|
281
|
+
return separatorByte(state, byte, state.guidance.keyBytes, 'value');
|
|
282
|
+
case 'item-separator':
|
|
283
|
+
return separatorByte(
|
|
284
|
+
state,
|
|
285
|
+
byte,
|
|
286
|
+
state.guidance.itemBytes,
|
|
287
|
+
state.stack.at(-1)?.kind === 'object' ? 'object-key' : 'array-value',
|
|
288
|
+
);
|
|
289
|
+
case 'after-value':
|
|
290
|
+
return afterValue(state, byte);
|
|
291
|
+
case 'string':
|
|
292
|
+
case 'key-string':
|
|
293
|
+
return stringByte(state, byte);
|
|
294
|
+
case 'escape':
|
|
295
|
+
case 'key-escape':
|
|
296
|
+
return escapeByte(state, byte);
|
|
297
|
+
case 'unicode':
|
|
298
|
+
case 'key-unicode':
|
|
299
|
+
return unicodeByte(state, byte);
|
|
300
|
+
case 'number':
|
|
301
|
+
return numberByte(state, byte);
|
|
302
|
+
case 'literal':
|
|
303
|
+
return literalByte(state, byte);
|
|
304
|
+
case 'done':
|
|
305
|
+
case 'dead':
|
|
306
|
+
return DEAD;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function startValue(state: JsonState, byte: number): JsonState {
|
|
311
|
+
const kind = byteKind(byte);
|
|
312
|
+
if (kind === undefined) return DEAD;
|
|
313
|
+
const schema = selectSchema(state.schema, kind);
|
|
314
|
+
if (schema === null) return DEAD;
|
|
315
|
+
|
|
316
|
+
if (byte === 0x22) return { ...state, mode: 'string', schema, bytes: [], stringLength: 0, stringPending: 0 };
|
|
317
|
+
if (byte === 0x7b) {
|
|
318
|
+
return {
|
|
319
|
+
mode: 'object-key',
|
|
320
|
+
schema,
|
|
321
|
+
stack: [...state.stack, { kind: 'object', schema, seen: new Set<string>(), entries: [] }],
|
|
322
|
+
guidance: state.guidance,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
if (byte === 0x5b) {
|
|
326
|
+
return {
|
|
327
|
+
mode: 'array-value',
|
|
328
|
+
schema: itemSchema(schema, 0),
|
|
329
|
+
stack: [
|
|
330
|
+
...state.stack,
|
|
331
|
+
{
|
|
332
|
+
kind: 'array',
|
|
333
|
+
schema,
|
|
334
|
+
length: 0,
|
|
335
|
+
items: [],
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
guidance: state.guidance,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (byte === 0x74) return { ...state, mode: 'literal', schema, literal: 'true', literalValue: true, index: 1 };
|
|
342
|
+
if (byte === 0x66) return { ...state, mode: 'literal', schema, literal: 'false', literalValue: false, index: 1 };
|
|
343
|
+
if (byte === 0x6e) return { ...state, mode: 'literal', schema, literal: 'null', literalValue: null, index: 1 };
|
|
344
|
+
const text = String.fromCharCode(byte);
|
|
345
|
+
if (!integerPrefixViable(schema, text)) return DEAD;
|
|
346
|
+
return { ...state, mode: 'number', schema, text };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function isAccepting(state: JsonState): boolean {
|
|
350
|
+
if (state.mode === 'done') return true;
|
|
351
|
+
return (
|
|
352
|
+
state.mode === 'number' &&
|
|
353
|
+
state.stack.length === 0 &&
|
|
354
|
+
numberComplete(state.text!) &&
|
|
355
|
+
validateNode(state.schema, { kind: 'number', value: Number(state.text), raw: state.text! })
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function stringByte(state: JsonState, byte: number): JsonState {
|
|
360
|
+
const isKey = state.mode === 'key-string';
|
|
361
|
+
if (state.highSurrogate !== undefined && byte !== 0x5c) return DEAD;
|
|
362
|
+
if (byte === 0x22) {
|
|
363
|
+
const value = decodeString(state.bytes ?? []);
|
|
364
|
+
if (value === null) return DEAD;
|
|
365
|
+
if (isKey) return completeKey(state, value);
|
|
366
|
+
return completeValue(state, { kind: 'string', value });
|
|
367
|
+
}
|
|
368
|
+
if (byte === 0x5c) {
|
|
369
|
+
if (isKey ? !keyEscapeAllowed(state) : !stringEscapeAllowed(state)) return DEAD;
|
|
370
|
+
return { ...state, mode: isKey ? 'key-escape' : 'escape' };
|
|
371
|
+
}
|
|
372
|
+
if (byte < 0x20) return DEAD;
|
|
373
|
+
let length = state.stringLength ?? 0;
|
|
374
|
+
let pending = state.stringPending ?? 0;
|
|
375
|
+
if ((byte & 0xc0) === 0x80) {
|
|
376
|
+
pending = pending > 0 ? pending - 1 : -1;
|
|
377
|
+
} else {
|
|
378
|
+
pending = pending === 0 ? utf8Continuations(byte) : -1;
|
|
379
|
+
length++;
|
|
380
|
+
}
|
|
381
|
+
const bytes = [...(state.bytes ?? []), byte];
|
|
382
|
+
if (isKey && !keyPrefixAllowed(state, bytes)) return DEAD;
|
|
383
|
+
if (!isKey && !stringContentAllowed(state.schema, bytes, length)) return DEAD;
|
|
384
|
+
return { ...state, bytes, stringLength: length, stringPending: pending };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function utf8Continuations(lead: number): number {
|
|
388
|
+
if (lead < 0x80) return 0;
|
|
389
|
+
if (lead < 0xc2) return -1;
|
|
390
|
+
if (lead < 0xe0) return 1;
|
|
391
|
+
if (lead < 0xf0) return 2;
|
|
392
|
+
if (lead < 0xf5) return 3;
|
|
393
|
+
return -1;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function escapeByte(state: JsonState, byte: number): JsonState {
|
|
397
|
+
const isKey = state.mode === 'key-escape';
|
|
398
|
+
if (state.highSurrogate !== undefined && byte !== 0x75) return DEAD;
|
|
399
|
+
if (byte === 0x75) {
|
|
400
|
+
return { ...state, mode: isKey ? 'key-unicode' : 'unicode', text: '' };
|
|
401
|
+
}
|
|
402
|
+
const escaped = SIMPLE_ESCAPES[byte];
|
|
403
|
+
if (escaped === undefined) return DEAD;
|
|
404
|
+
const bytes = [...(state.bytes ?? []), ...encoder.encode(escaped)];
|
|
405
|
+
const length = (state.stringLength ?? 0) + 1;
|
|
406
|
+
const pending = (state.stringPending ?? 0) === 0 ? 0 : -1;
|
|
407
|
+
if (isKey && !keyPrefixAllowed(state, bytes)) return DEAD;
|
|
408
|
+
if (!isKey && !stringContentAllowed(state.schema, bytes, length)) return DEAD;
|
|
409
|
+
return { ...state, mode: isKey ? 'key-string' : 'string', bytes, stringLength: length, stringPending: pending };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function unicodeByte(state: JsonState, byte: number): JsonState {
|
|
413
|
+
if (!isHex(byte)) return DEAD;
|
|
414
|
+
const text = `${state.text ?? ''}${String.fromCharCode(byte)}`;
|
|
415
|
+
if (text.length < 4) {
|
|
416
|
+
if (state.mode === 'key-unicode' && !unicodeKeyPrefixAllowed(state, text)) return DEAD;
|
|
417
|
+
return { ...state, text };
|
|
418
|
+
}
|
|
419
|
+
const codeUnit = Number.parseInt(text, 16);
|
|
420
|
+
const isKey = state.mode === 'key-unicode';
|
|
421
|
+
const mode = isKey ? 'key-string' : 'string';
|
|
422
|
+
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
|
|
423
|
+
if (state.highSurrogate !== undefined) return DEAD;
|
|
424
|
+
return { ...state, mode, text: undefined, highSurrogate: codeUnit };
|
|
425
|
+
}
|
|
426
|
+
let value: string;
|
|
427
|
+
if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
|
|
428
|
+
if (state.highSurrogate === undefined) return DEAD;
|
|
429
|
+
const codePoint = 0x10000 + ((state.highSurrogate - 0xd800) << 10) + (codeUnit - 0xdc00);
|
|
430
|
+
value = String.fromCodePoint(codePoint);
|
|
431
|
+
} else {
|
|
432
|
+
if (state.highSurrogate !== undefined) return DEAD;
|
|
433
|
+
value = String.fromCharCode(codeUnit);
|
|
434
|
+
}
|
|
435
|
+
const bytes = [...(state.bytes ?? []), ...encoder.encode(value)];
|
|
436
|
+
const length = (state.stringLength ?? 0) + 1;
|
|
437
|
+
const pending = (state.stringPending ?? 0) === 0 ? 0 : -1;
|
|
438
|
+
if (isKey && !keyPrefixAllowed(state, bytes)) return DEAD;
|
|
439
|
+
if (!isKey && !stringContentAllowed(state.schema, bytes, length)) return DEAD;
|
|
440
|
+
return {
|
|
441
|
+
...state,
|
|
442
|
+
mode,
|
|
443
|
+
bytes,
|
|
444
|
+
text: undefined,
|
|
445
|
+
highSurrogate: undefined,
|
|
446
|
+
stringLength: length,
|
|
447
|
+
stringPending: pending,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function completeKey(state: JsonState, key: string): JsonState {
|
|
452
|
+
const frame = topObject(state);
|
|
453
|
+
if (frame.seen.has(key)) return DEAD;
|
|
454
|
+
const childSchema = propertySchema(frame.schema, key, frame.entries);
|
|
455
|
+
if (childSchema === null) return DEAD;
|
|
456
|
+
return {
|
|
457
|
+
...state,
|
|
458
|
+
mode: 'colon',
|
|
459
|
+
bytes: undefined,
|
|
460
|
+
index: 0,
|
|
461
|
+
stack: replaceTop(state.stack, { ...frame, key, childSchema }),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function literalByte(state: JsonState, byte: number): JsonState {
|
|
466
|
+
const index = state.index ?? 0;
|
|
467
|
+
const literal = state.literal!;
|
|
468
|
+
if (byte !== literal.charCodeAt(index)) return DEAD;
|
|
469
|
+
if (index + 1 < literal.length) return { ...state, index: index + 1 };
|
|
470
|
+
const node: JsonNode =
|
|
471
|
+
state.literalValue === null ? { kind: 'null', value: null } : { kind: 'boolean', value: state.literalValue! };
|
|
472
|
+
return completeValue(state, node);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function numberByte(state: JsonState, byte: number): JsonState {
|
|
476
|
+
if (isNumberByte(byte)) {
|
|
477
|
+
const text = `${state.text}${String.fromCharCode(byte)}`;
|
|
478
|
+
if (numberPrefixValid(text) && integerPrefixViable(state.schema, text)) return { ...state, text };
|
|
479
|
+
}
|
|
480
|
+
const value = finishNumber(state);
|
|
481
|
+
return value === DEAD ? DEAD : transition(value, byte);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// JSON Schema accepts any number encoding for integer fields ("0.9e1" is 9),
|
|
485
|
+
// but during generation that freedom creates traps a model cannot escape:
|
|
486
|
+
// after "0.9" under {type: "integer"} only digits and exponents stay viable,
|
|
487
|
+
// continuations such as "0.9e-" can never close the value, and zero-padding
|
|
488
|
+
// like "0.0e0000…" stays legal forever, so a stuck model streams digits until
|
|
489
|
+
// it hits the token limit. Integer-only fields therefore use the constrained
|
|
490
|
+
// shape -?(0|[1-9]\d*)(\.0+)?([eE]+?\d+)? — the value stays
|
|
491
|
+
// ±digits×10^exponent, so every prefix can still close — with fraction zeros
|
|
492
|
+
// and exponent digits capped (they carry no information beyond two digits), and
|
|
493
|
+
// bytes from which no in-range integer remains reachable are pruned.
|
|
494
|
+
const SCALE_DIGIT_LIMIT = 3;
|
|
495
|
+
const integerRestrictions = new WeakMap<object, { lo: number; hi: number } | null>();
|
|
496
|
+
|
|
497
|
+
function integerPrefixViable(schema: Schema, text: string): boolean {
|
|
498
|
+
const restriction = integerRestriction(schema);
|
|
499
|
+
if (restriction === null) return true;
|
|
500
|
+
const { lo, hi } = restriction;
|
|
501
|
+
if (text === '-') return lo <= Math.min(hi, 0);
|
|
502
|
+
const match = /^(-?)(0|[1-9]\d*)(\.0{0,3})?(?:[eE]\+?(\d{0,3}))?$/.exec(text);
|
|
503
|
+
if (match === null) return false;
|
|
504
|
+
// "1.e" can never complete (the fraction needs a digit before the exponent)
|
|
505
|
+
if (match[3] === '.' && match[4] !== undefined) return false;
|
|
506
|
+
const negative = match[1] === '-';
|
|
507
|
+
if (match[3] === undefined && match[4] === undefined) {
|
|
508
|
+
return integerDigitsReachable(lo, hi, negative, match[2]);
|
|
509
|
+
}
|
|
510
|
+
return integerScaleReachable(lo, hi, negative, match[2], match[4] ?? '');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function integerRestriction(schema: Schema): { lo: number; hi: number } | null {
|
|
514
|
+
if (schema === true || schema === false) return null;
|
|
515
|
+
let restriction = integerRestrictions.get(schema);
|
|
516
|
+
if (restriction === undefined) {
|
|
517
|
+
restriction = computeIntegerRestriction(schema);
|
|
518
|
+
integerRestrictions.set(schema, restriction);
|
|
519
|
+
}
|
|
520
|
+
return restriction;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function computeIntegerRestriction(schema: Record<string, unknown>): { lo: number; hi: number } | null {
|
|
524
|
+
let integerOnly = false;
|
|
525
|
+
if (schema.type !== undefined) {
|
|
526
|
+
const types = Array.isArray(schema.type) ? schema.type.map(String) : [String(schema.type)];
|
|
527
|
+
integerOnly = types.includes('integer') && !types.includes('number');
|
|
528
|
+
} else if (typeof schema.const === 'number') {
|
|
529
|
+
integerOnly = Number.isInteger(schema.const);
|
|
530
|
+
} else if (Array.isArray(schema.enum)) {
|
|
531
|
+
const numbers = schema.enum.filter((value): value is number => typeof value === 'number');
|
|
532
|
+
integerOnly = numbers.length > 0 && numbers.every((value) => Number.isInteger(value));
|
|
533
|
+
}
|
|
534
|
+
if (!integerOnly) return null;
|
|
535
|
+
let lo = -Infinity;
|
|
536
|
+
let hi = Infinity;
|
|
537
|
+
if (typeof schema.minimum === 'number') lo = Math.ceil(schema.minimum);
|
|
538
|
+
if (typeof schema.exclusiveMinimum === 'number') lo = Math.max(lo, Math.floor(schema.exclusiveMinimum) + 1);
|
|
539
|
+
if (typeof schema.maximum === 'number') hi = Math.floor(schema.maximum);
|
|
540
|
+
if (typeof schema.exclusiveMaximum === 'number') hi = Math.min(hi, Math.ceil(schema.exclusiveMaximum) - 1);
|
|
541
|
+
// Pruning must never reject a value the validator would accept; when a
|
|
542
|
+
// bound is beyond exact integer arithmetic, drop it instead of guessing.
|
|
543
|
+
if (!Number.isSafeInteger(lo)) lo = -Infinity;
|
|
544
|
+
if (!Number.isSafeInteger(hi)) hi = Infinity;
|
|
545
|
+
return { lo, hi };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Whether some integer whose decimal representation starts with `digits`
|
|
549
|
+
// (extended by zero or more digits, or scaled by a later ".0…"/exponent) lies
|
|
550
|
+
// in [lo, hi]. The reachable values are ±[|P|·10^k, (|P|+1)·10^k - 1] for
|
|
551
|
+
// k ≥ 0, except that "0"/"-0" cannot take further digits.
|
|
552
|
+
function integerDigitsReachable(lo: number, hi: number, negative: boolean, digits: string): boolean {
|
|
553
|
+
if (digits.length > 16) return negative ? lo === -Infinity : hi === Infinity;
|
|
554
|
+
let low = Number(digits);
|
|
555
|
+
let high = low + 1;
|
|
556
|
+
const extensible = low !== 0;
|
|
557
|
+
for (;;) {
|
|
558
|
+
if (negative) {
|
|
559
|
+
if (-(high - 1) <= hi && -low >= lo) return true;
|
|
560
|
+
if (-low < lo) return false;
|
|
561
|
+
} else {
|
|
562
|
+
if (low <= hi && high - 1 >= lo) return true;
|
|
563
|
+
if (low > hi) return false;
|
|
564
|
+
}
|
|
565
|
+
if (!extensible) return false;
|
|
566
|
+
low *= 10;
|
|
567
|
+
high *= 10;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Once a fraction or exponent starts, the digits are frozen and the value is
|
|
572
|
+
// exactly ±digits×10^E for a still-typeable exponent E ≥ 0. When exponent
|
|
573
|
+
// digits have been typed, reachable exponents are those obtainable by extending
|
|
574
|
+
// the typed prefix within the digit limit (leading zeros allowed).
|
|
575
|
+
function integerScaleReachable(
|
|
576
|
+
lo: number,
|
|
577
|
+
hi: number,
|
|
578
|
+
negative: boolean,
|
|
579
|
+
digits: string,
|
|
580
|
+
exponentPrefix: string,
|
|
581
|
+
): boolean {
|
|
582
|
+
if (digits.length > 16) return negative ? lo === -Infinity : hi === Infinity;
|
|
583
|
+
const base = Number(digits);
|
|
584
|
+
if (base === 0) return lo <= 0 && 0 <= hi;
|
|
585
|
+
let magnitude = base;
|
|
586
|
+
// A typeable exponent has at most SCALE_DIGIT_LIMIT digits, so 999 bounds
|
|
587
|
+
// the search; finite bounds exit early via the overshoot check.
|
|
588
|
+
for (let exponent = 0; exponent <= 999; ++exponent) {
|
|
589
|
+
const value = negative ? -magnitude : magnitude;
|
|
590
|
+
const inRange = (lo === -Infinity || value >= lo) && (hi === Infinity || value <= hi);
|
|
591
|
+
if (inRange && exponentTypeable(exponent, exponentPrefix)) return true;
|
|
592
|
+
if (negative ? lo !== -Infinity && value < lo : hi !== Infinity && value > hi) return false;
|
|
593
|
+
magnitude *= 10;
|
|
594
|
+
}
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Whether exponent value `exponent` can be written by extending the typed
|
|
599
|
+
// digits `prefix` to a full exponent of at most SCALE_DIGIT_LIMIT digits
|
|
600
|
+
// (at least one digit total; leading zeros allowed).
|
|
601
|
+
function exponentTypeable(exponent: number, prefix: string): boolean {
|
|
602
|
+
const typed = prefix === '' ? 0 : Number(prefix);
|
|
603
|
+
for (let extra = Math.max(0, 1 - prefix.length); extra <= SCALE_DIGIT_LIMIT - prefix.length; ++extra) {
|
|
604
|
+
const scale = 10 ** extra;
|
|
605
|
+
if (exponent >= typed * scale && exponent < (typed + 1) * scale) return true;
|
|
606
|
+
}
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function finishNumber(state: JsonState): JsonState {
|
|
611
|
+
const text = state.text!;
|
|
612
|
+
if (!numberComplete(text)) return DEAD;
|
|
613
|
+
return completeValue(state, { kind: 'number', value: Number(text), raw: text });
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function completeValue(state: JsonState, node: JsonNode): JsonState {
|
|
617
|
+
if (!validateNode(state.schema, node)) return DEAD;
|
|
618
|
+
if (state.stack.length === 0) return { mode: 'done', schema: state.schema, stack: [], guidance: state.guidance };
|
|
619
|
+
const frame = state.stack.at(-1)!;
|
|
620
|
+
if (frame.kind === 'object') {
|
|
621
|
+
if (frame.key === undefined) return DEAD;
|
|
622
|
+
const seen = new Set(frame.seen);
|
|
623
|
+
seen.add(frame.key);
|
|
624
|
+
const entries = [...frame.entries, { key: frame.key, node }];
|
|
625
|
+
if (!partialObjectValid(frame.schema, entries, new Set())) return DEAD;
|
|
626
|
+
return {
|
|
627
|
+
mode: 'after-value',
|
|
628
|
+
schema: frame.schema,
|
|
629
|
+
stack: replaceTop(state.stack, { kind: 'object', schema: frame.schema, seen, entries }),
|
|
630
|
+
guidance: state.guidance,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
mode: 'after-value',
|
|
635
|
+
schema: frame.schema,
|
|
636
|
+
stack: replaceTop(state.stack, { ...frame, length: frame.length + 1, items: [...frame.items, node] }),
|
|
637
|
+
guidance: state.guidance,
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function afterValue(state: JsonState, byte: number): JsonState {
|
|
642
|
+
const frame = state.stack.at(-1);
|
|
643
|
+
if (frame === undefined) return DEAD;
|
|
644
|
+
if (frame.kind === 'object') {
|
|
645
|
+
if (byte === 0x7d) return closeObject(state);
|
|
646
|
+
if (!objectCanAddProperty(frame)) return DEAD;
|
|
647
|
+
return separatorByte(
|
|
648
|
+
{ ...state, mode: 'item-separator', index: 0 },
|
|
649
|
+
byte,
|
|
650
|
+
state.guidance.itemBytes,
|
|
651
|
+
'object-key',
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
if (byte === 0x5d) return closeArray(state);
|
|
655
|
+
if (exceedsMaxItems(frame)) return DEAD;
|
|
656
|
+
return separatorByte({ ...state, mode: 'item-separator', index: 0 }, byte, state.guidance.itemBytes, 'array-value');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function separatorByte(state: JsonState, byte: number, separator: Uint8Array, completedMode: Mode): JsonState {
|
|
660
|
+
const index = state.index ?? 0;
|
|
661
|
+
if (byte !== separator[index]) return DEAD;
|
|
662
|
+
if (index + 1 < separator.length) return { ...state, index: index + 1 };
|
|
663
|
+
const frame = state.stack.at(-1);
|
|
664
|
+
return {
|
|
665
|
+
...state,
|
|
666
|
+
mode: completedMode,
|
|
667
|
+
schema:
|
|
668
|
+
completedMode === 'value'
|
|
669
|
+
? topObject(state).childSchema!
|
|
670
|
+
: completedMode === 'array-value' && frame?.kind === 'array'
|
|
671
|
+
? itemSchema(frame.schema, frame.length)
|
|
672
|
+
: state.schema,
|
|
673
|
+
index: undefined,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function separatorExpects(state: JsonState, byte: number): boolean {
|
|
678
|
+
if (state.mode === 'colon') return state.guidance.keyBytes[state.index ?? 0] === byte;
|
|
679
|
+
if (state.mode === 'item-separator') return state.guidance.itemBytes[state.index ?? 0] === byte;
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function closeObject(state: JsonState): JsonState {
|
|
684
|
+
const frame = state.stack.at(-1);
|
|
685
|
+
if (frame?.kind !== 'object') return DEAD;
|
|
686
|
+
const value: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
687
|
+
for (const entry of frame.entries) value[entry.key] = entry.node.value;
|
|
688
|
+
return completeValue(
|
|
689
|
+
{ ...state, schema: frame.schema, stack: state.stack.slice(0, -1) },
|
|
690
|
+
{ kind: 'object', value, entries: [...frame.entries] },
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function closeArray(state: JsonState): JsonState {
|
|
695
|
+
const frame = state.stack.at(-1);
|
|
696
|
+
if (frame?.kind !== 'array') return DEAD;
|
|
697
|
+
return completeValue(
|
|
698
|
+
{ ...state, schema: frame.schema, stack: state.stack.slice(0, -1) },
|
|
699
|
+
{ kind: 'array', value: frame.items.map((item) => item.value), items: [...frame.items] },
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function selectSchema(schema: Schema, kind: Kind): Schema | null {
|
|
704
|
+
return schemaMayAcceptKind(schema, kind, new Set()) ? schema : null;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function propertySchema(
|
|
708
|
+
schema: Schema,
|
|
709
|
+
key: string,
|
|
710
|
+
entries: ReadonlyArray<{ key: string; node: JsonNode }> = [],
|
|
711
|
+
): Schema | null {
|
|
712
|
+
if (schema === true) return true;
|
|
713
|
+
if (schema === false) return null;
|
|
714
|
+
const schemas: Schema[] = [];
|
|
715
|
+
const direct = directPropertySchema(schema, key);
|
|
716
|
+
if (direct === null) return null;
|
|
717
|
+
if (direct !== true) schemas.push(direct);
|
|
718
|
+
for (const candidate of [schema, ...(Array.isArray(schema.allOf) ? schema.allOf.filter(isSchema) : [])]) {
|
|
719
|
+
if (candidate === true || candidate === false) continue;
|
|
720
|
+
const branch = selectedConditionalBranch(candidate, entries);
|
|
721
|
+
if (branch === undefined || branch === true) continue;
|
|
722
|
+
if (branch === false) return null;
|
|
723
|
+
const branchSchema = directPropertySchema(branch, key);
|
|
724
|
+
if (branchSchema === null) return null;
|
|
725
|
+
if (branchSchema !== true) schemas.push(branchSchema);
|
|
726
|
+
}
|
|
727
|
+
if (schemas.length === 0) return true;
|
|
728
|
+
return schemas.length === 1 ? schemas[0] : { allOf: schemas };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function directPropertySchema(schema: Record<string, unknown>, key: string): Schema | null {
|
|
732
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
733
|
+
const schemas: Schema[] = [];
|
|
734
|
+
if (isSchema(properties[key])) schemas.push(properties[key]);
|
|
735
|
+
if (isRecord(schema.patternProperties)) {
|
|
736
|
+
for (const [pattern, candidate] of Object.entries(schema.patternProperties)) {
|
|
737
|
+
if (new RegExp(pattern, 'u').test(key) && isSchema(candidate)) schemas.push(candidate);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (schemas.length === 0) {
|
|
741
|
+
if (schema.additionalProperties === false) return null;
|
|
742
|
+
return isSchema(schema.additionalProperties) ? schema.additionalProperties : true;
|
|
743
|
+
}
|
|
744
|
+
return schemas.length === 1 ? schemas[0] : { allOf: schemas };
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function selectedConditionalBranch(
|
|
748
|
+
schema: Record<string, unknown>,
|
|
749
|
+
entries: ReadonlyArray<{ key: string; node: JsonNode }>,
|
|
750
|
+
): Schema | undefined {
|
|
751
|
+
if (!isSchema(schema.if) || schema.if === true || schema.if === false) return undefined;
|
|
752
|
+
const required = Array.isArray(schema.if.required) ? schema.if.required : [];
|
|
753
|
+
if (required.length === 0 || !required.every((key) => entries.some((entry) => entry.key === key))) return undefined;
|
|
754
|
+
const node = objectNode(entries);
|
|
755
|
+
const branch = validateNode(schema.if, node) ? schema.then : schema.else;
|
|
756
|
+
return isSchema(branch) ? branch : undefined;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function partialObjectValid(
|
|
760
|
+
schema: Schema,
|
|
761
|
+
entries: ReadonlyArray<{ key: string; node: JsonNode }>,
|
|
762
|
+
seen: Set<object>,
|
|
763
|
+
): boolean {
|
|
764
|
+
if (schema === true) return true;
|
|
765
|
+
if (schema === false) return false;
|
|
766
|
+
if (seen.has(schema)) return true;
|
|
767
|
+
seen.add(schema);
|
|
768
|
+
try {
|
|
769
|
+
for (const entry of entries) {
|
|
770
|
+
const child = directPropertySchema(schema, entry.key);
|
|
771
|
+
if (child === null || (child !== true && !validateNode(child, entry.node))) return false;
|
|
772
|
+
}
|
|
773
|
+
if (
|
|
774
|
+
typeof schema.$ref === 'string' &&
|
|
775
|
+
!partialObjectValid(resolveReference(schema, schema.$ref), entries, seen)
|
|
776
|
+
)
|
|
777
|
+
return false;
|
|
778
|
+
if (
|
|
779
|
+
Array.isArray(schema.allOf) &&
|
|
780
|
+
!schema.allOf.every((child) => !isSchema(child) || partialObjectValid(child, entries, seen))
|
|
781
|
+
)
|
|
782
|
+
return false;
|
|
783
|
+
const branch = selectedConditionalBranch(schema, entries);
|
|
784
|
+
return branch === undefined || partialObjectValid(branch, entries, seen);
|
|
785
|
+
} finally {
|
|
786
|
+
seen.delete(schema);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function objectNode(entries: ReadonlyArray<{ key: string; node: JsonNode }>): Extract<JsonNode, { kind: 'object' }> {
|
|
791
|
+
const value: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
792
|
+
for (const entry of entries) value[entry.key] = entry.node.value;
|
|
793
|
+
return { kind: 'object', value, entries: [...entries] };
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function itemSchema(schema: Schema, index: number): Schema {
|
|
797
|
+
if (schema === true || schema === false || hasDeferredStructure(schema)) return schema === false ? false : true;
|
|
798
|
+
if (Array.isArray(schema.prefixItems) && isSchema(schema.prefixItems[index])) return schema.prefixItems[index];
|
|
799
|
+
if (Array.isArray(schema.items)) {
|
|
800
|
+
if (isSchema(schema.items[index])) return schema.items[index];
|
|
801
|
+
return isSchema(schema.additionalItems) ? schema.additionalItems : true;
|
|
802
|
+
}
|
|
803
|
+
return isSchema(schema.items) ? schema.items : true;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function keyPrefixAllowed(state: JsonState, bytes: readonly number[]): boolean {
|
|
807
|
+
const frame = topObject(state);
|
|
808
|
+
if (frame.schema === true || frame.schema === false) return true;
|
|
809
|
+
const properties = encodedPropertyKeys(frame.schema);
|
|
810
|
+
if (properties === null) return true;
|
|
811
|
+
return properties.some(({ key, bytes: propertyBytes }) => {
|
|
812
|
+
if (frame.seen.has(key) || bytes.length > propertyBytes.length) return false;
|
|
813
|
+
for (let index = 0; index < bytes.length; ++index) {
|
|
814
|
+
if (bytes[index] !== propertyBytes[index]) return false;
|
|
815
|
+
}
|
|
816
|
+
return true;
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function keyEscapeAllowed(state: JsonState): boolean {
|
|
821
|
+
const frame = topObject(state);
|
|
822
|
+
if (frame.schema === true || frame.schema === false) return true;
|
|
823
|
+
const properties = encodedPropertyKeys(frame.schema);
|
|
824
|
+
if (properties === null) return true;
|
|
825
|
+
if (state.highSurrogate !== undefined) return true;
|
|
826
|
+
const bytes = state.bytes ?? [];
|
|
827
|
+
return properties.some(({ key, bytes: propertyBytes }) => {
|
|
828
|
+
if (frame.seen.has(key) || propertyBytes.length <= bytes.length) return false;
|
|
829
|
+
for (let index = 0; index < bytes.length; ++index) {
|
|
830
|
+
if (bytes[index] !== propertyBytes[index]) return false;
|
|
831
|
+
}
|
|
832
|
+
const next = propertyBytes[bytes.length];
|
|
833
|
+
return next < 0x20 || next === 0x22 || next === 0x5c || next >= 0x80;
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function objectCanAddProperty(frame: ObjectFrame): boolean {
|
|
838
|
+
if (frame.schema === true || frame.schema === false) return true;
|
|
839
|
+
const properties = encodedPropertyKeys(frame.schema);
|
|
840
|
+
return properties === null || properties.some(({ key }) => !frame.seen.has(key));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function stringContentAllowed(schema: Schema, bytes: readonly number[], length: number): boolean {
|
|
844
|
+
const values = finiteStringValues(schema);
|
|
845
|
+
if (
|
|
846
|
+
values !== null &&
|
|
847
|
+
!values.some(({ bytes: valueBytes }) => {
|
|
848
|
+
if (bytes.length > valueBytes.length) return false;
|
|
849
|
+
for (let index = 0; index < bytes.length; ++index) {
|
|
850
|
+
if (bytes[index] !== valueBytes[index]) return false;
|
|
851
|
+
}
|
|
852
|
+
return true;
|
|
853
|
+
})
|
|
854
|
+
)
|
|
855
|
+
return false;
|
|
856
|
+
const maximum = directStringMaxLength(schema);
|
|
857
|
+
return maximum === undefined || length <= maximum;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function stringEscapeAllowed(state: JsonState): boolean {
|
|
861
|
+
const values = finiteStringValues(state.schema);
|
|
862
|
+
const maximum = directStringMaxLength(state.schema);
|
|
863
|
+
if (maximum !== undefined && (state.stringLength ?? 0) >= maximum) return false;
|
|
864
|
+
if (values === null) return true;
|
|
865
|
+
if (state.highSurrogate !== undefined) return true;
|
|
866
|
+
const bytes = state.bytes ?? [];
|
|
867
|
+
return values.some(({ bytes: valueBytes }) => {
|
|
868
|
+
if (valueBytes.length <= bytes.length) return false;
|
|
869
|
+
for (let index = 0; index < bytes.length; ++index) {
|
|
870
|
+
if (bytes[index] !== valueBytes[index]) return false;
|
|
871
|
+
}
|
|
872
|
+
const next = valueBytes[bytes.length];
|
|
873
|
+
return next < 0x20 || next === 0x22 || next === 0x5c || next >= 0x80;
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function directStringMaxLength(schema: Schema): number | undefined {
|
|
878
|
+
return schema !== true && schema !== false && typeof schema.maxLength === 'number' ? schema.maxLength : undefined;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function finiteStringValues(schema: Schema): ReadonlyArray<{ value: string; bytes: Uint8Array }> | null {
|
|
882
|
+
if (schema === true || schema === false) return null;
|
|
883
|
+
let values = finiteStringCache.get(schema);
|
|
884
|
+
if (values !== undefined) return values;
|
|
885
|
+
let candidates: string[] | null = null;
|
|
886
|
+
if (typeof schema.const === 'string') candidates = [schema.const];
|
|
887
|
+
else if (Array.isArray(schema.enum))
|
|
888
|
+
candidates = schema.enum.filter((value): value is string => typeof value === 'string');
|
|
889
|
+
values =
|
|
890
|
+
candidates === null
|
|
891
|
+
? null
|
|
892
|
+
: [...new Set(candidates)]
|
|
893
|
+
.filter((value) => validateNode(schema, { kind: 'string', value }))
|
|
894
|
+
.map((value) => ({ value, bytes: encoder.encode(value) }));
|
|
895
|
+
finiteStringCache.set(schema, values);
|
|
896
|
+
return values;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function unicodeKeyPrefixAllowed(state: JsonState, hexPrefix: string): boolean {
|
|
900
|
+
const frame = topObject(state);
|
|
901
|
+
if (frame.schema === true || frame.schema === false) return true;
|
|
902
|
+
const properties = encodedPropertyKeys(frame.schema);
|
|
903
|
+
if (properties === null) return true;
|
|
904
|
+
const prefix = decodeString(state.bytes ?? []);
|
|
905
|
+
if (prefix === null) return true;
|
|
906
|
+
return properties.some(({ key }) => {
|
|
907
|
+
if (frame.seen.has(key) || !key.startsWith(prefix)) return false;
|
|
908
|
+
const offset = prefix.length;
|
|
909
|
+
let codeUnit = key.charCodeAt(offset);
|
|
910
|
+
if (state.highSurrogate !== undefined) {
|
|
911
|
+
if (codeUnit !== state.highSurrogate) return false;
|
|
912
|
+
codeUnit = key.charCodeAt(offset + 1);
|
|
913
|
+
}
|
|
914
|
+
return Number.isInteger(codeUnit) && codeUnit.toString(16).padStart(4, '0').startsWith(hexPrefix.toLowerCase());
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function encodedPropertyKeys(
|
|
919
|
+
schema: Record<string, unknown>,
|
|
920
|
+
): ReadonlyArray<{ key: string; bytes: Uint8Array }> | null {
|
|
921
|
+
let properties = propertyKeyBytes.get(schema);
|
|
922
|
+
if (properties === undefined) {
|
|
923
|
+
const keys = constrainedPropertyKeys(schema, new Set());
|
|
924
|
+
properties = keys === null ? null : [...keys].map((key) => ({ key, bytes: encoder.encode(key) }));
|
|
925
|
+
propertyKeyBytes.set(schema, properties);
|
|
926
|
+
}
|
|
927
|
+
return properties;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function constrainedPropertyKeys(schema: Schema, seen: Set<object>): Set<string> | null {
|
|
931
|
+
if (schema === true || schema === false || seen.has(schema)) return null;
|
|
932
|
+
seen.add(schema);
|
|
933
|
+
try {
|
|
934
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
935
|
+
let result =
|
|
936
|
+
schema.additionalProperties === false && !isRecord(schema.patternProperties)
|
|
937
|
+
? new Set(Object.keys(properties))
|
|
938
|
+
: null;
|
|
939
|
+
if (typeof schema.$ref === 'string') {
|
|
940
|
+
result = intersectKeySets(result, constrainedPropertyKeys(resolveReference(schema, schema.$ref), seen));
|
|
941
|
+
}
|
|
942
|
+
if (Array.isArray(schema.allOf)) {
|
|
943
|
+
for (const child of schema.allOf) {
|
|
944
|
+
if (isSchema(child)) result = intersectKeySets(result, constrainedPropertyKeys(child, seen));
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
for (const keyword of ['anyOf', 'oneOf'] as const) {
|
|
948
|
+
if (!Array.isArray(schema[keyword])) continue;
|
|
949
|
+
let union: Set<string> | null = new Set();
|
|
950
|
+
for (const child of schema[keyword]) {
|
|
951
|
+
if (!isSchema(child) || !schemaMayAcceptKind(child, 'object', new Set())) continue;
|
|
952
|
+
const childKeys = constrainedPropertyKeys(child, seen);
|
|
953
|
+
if (childKeys === null) {
|
|
954
|
+
union = null;
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
for (const key of childKeys) union.add(key);
|
|
958
|
+
}
|
|
959
|
+
result = intersectKeySets(result, union);
|
|
960
|
+
}
|
|
961
|
+
return result;
|
|
962
|
+
} finally {
|
|
963
|
+
seen.delete(schema);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function intersectKeySets(left: Set<string> | null, right: Set<string> | null): Set<string> | null {
|
|
968
|
+
if (left === null) return right;
|
|
969
|
+
if (right === null) return left;
|
|
970
|
+
return new Set([...left].filter((key) => right.has(key)));
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function exceedsMaxItems(frame: ArrayFrame): boolean {
|
|
974
|
+
return frame.schema !== true &&
|
|
975
|
+
frame.schema !== false &&
|
|
976
|
+
!hasDeferredStructure(frame.schema) &&
|
|
977
|
+
typeof frame.schema.maxItems === 'number'
|
|
978
|
+
? frame.length >= frame.schema.maxItems
|
|
979
|
+
: false;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const schemaContexts = new WeakMap<object, Schema>();
|
|
983
|
+
|
|
984
|
+
function validateNode(schema: Schema, node: JsonNode, active = new Map<object, Set<JsonNode>>()): boolean {
|
|
985
|
+
if (schema === true) return true;
|
|
986
|
+
if (schema === false) return false;
|
|
987
|
+
let nodes = active.get(schema);
|
|
988
|
+
if (nodes?.has(node)) return true;
|
|
989
|
+
if (nodes === undefined) {
|
|
990
|
+
nodes = new Set();
|
|
991
|
+
active.set(schema, nodes);
|
|
992
|
+
}
|
|
993
|
+
nodes.add(node);
|
|
994
|
+
try {
|
|
995
|
+
if (typeof schema.$ref === 'string' && !validateNode(resolveReference(schema, schema.$ref), node, active))
|
|
996
|
+
return false;
|
|
997
|
+
if (schema.type !== undefined) {
|
|
998
|
+
const types = Array.isArray(schema.type) ? schema.type.map(String) : [String(schema.type)];
|
|
999
|
+
if (!types.some((type) => nodeHasType(node, type))) return false;
|
|
1000
|
+
}
|
|
1001
|
+
if (schema.const !== undefined && !nodeEqualsValue(node, schema.const)) return false;
|
|
1002
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((value) => nodeEqualsValue(node, value))) return false;
|
|
1003
|
+
if (
|
|
1004
|
+
Array.isArray(schema.allOf) &&
|
|
1005
|
+
!schema.allOf.every((child) => isSchema(child) && validateNode(child, node, active))
|
|
1006
|
+
)
|
|
1007
|
+
return false;
|
|
1008
|
+
if (
|
|
1009
|
+
Array.isArray(schema.anyOf) &&
|
|
1010
|
+
!schema.anyOf.some((child) => isSchema(child) && validateNode(child, node, active))
|
|
1011
|
+
)
|
|
1012
|
+
return false;
|
|
1013
|
+
if (
|
|
1014
|
+
Array.isArray(schema.oneOf) &&
|
|
1015
|
+
schema.oneOf.filter((child) => isSchema(child) && validateNode(child, node, active)).length !== 1
|
|
1016
|
+
)
|
|
1017
|
+
return false;
|
|
1018
|
+
if (isSchema(schema.not) && validateNode(schema.not, node, active)) return false;
|
|
1019
|
+
if (isSchema(schema.if)) {
|
|
1020
|
+
const branch = validateNode(schema.if, node, active) ? schema.then : schema.else;
|
|
1021
|
+
if (isSchema(branch) && !validateNode(branch, node, active)) return false;
|
|
1022
|
+
}
|
|
1023
|
+
if (node.kind === 'number' && !validateNumericNode(schema, node)) return false;
|
|
1024
|
+
if (node.kind === 'string' && !validateStringNode(schema, node.value)) return false;
|
|
1025
|
+
if (node.kind === 'array' && !validateArrayNode(schema, node, active)) return false;
|
|
1026
|
+
if (node.kind === 'object' && !validateObjectNode(schema, node, active)) return false;
|
|
1027
|
+
return true;
|
|
1028
|
+
} finally {
|
|
1029
|
+
nodes.delete(node);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function validateNumericNode(schema: Record<string, unknown>, node: Extract<JsonNode, { kind: 'number' }>): boolean {
|
|
1034
|
+
const value = decimal(node.raw);
|
|
1035
|
+
if (schema.minimum !== undefined && compareDecimal(value, decimal(String(schema.minimum))) < 0) return false;
|
|
1036
|
+
if (schema.maximum !== undefined && compareDecimal(value, decimal(String(schema.maximum))) > 0) return false;
|
|
1037
|
+
if (schema.exclusiveMinimum !== undefined && compareDecimal(value, decimal(String(schema.exclusiveMinimum))) <= 0)
|
|
1038
|
+
return false;
|
|
1039
|
+
if (schema.exclusiveMaximum !== undefined && compareDecimal(value, decimal(String(schema.exclusiveMaximum))) >= 0)
|
|
1040
|
+
return false;
|
|
1041
|
+
return schema.multipleOf === undefined || decimalMultiple(value, decimal(String(schema.multipleOf)));
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function validateStringNode(schema: Record<string, unknown>, value: string): boolean {
|
|
1045
|
+
const length = [...value].length;
|
|
1046
|
+
if (typeof schema.minLength === 'number' && length < schema.minLength) return false;
|
|
1047
|
+
if (typeof schema.maxLength === 'number' && length > schema.maxLength) return false;
|
|
1048
|
+
if (typeof schema.pattern === 'string' && !new RegExp(schema.pattern, 'u').test(value)) return false;
|
|
1049
|
+
return typeof schema.format !== 'string' || formatMatches(schema.format, value);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function validateArrayNode(
|
|
1053
|
+
schema: Record<string, unknown>,
|
|
1054
|
+
node: Extract<JsonNode, { kind: 'array' }>,
|
|
1055
|
+
active: Map<object, Set<JsonNode>>,
|
|
1056
|
+
): boolean {
|
|
1057
|
+
if (typeof schema.minItems === 'number' && node.items.length < schema.minItems) return false;
|
|
1058
|
+
if (typeof schema.maxItems === 'number' && node.items.length > schema.maxItems) return false;
|
|
1059
|
+
if (schema.uniqueItems === true) {
|
|
1060
|
+
for (let index = 0; index < node.items.length; ++index) {
|
|
1061
|
+
if (node.items.slice(0, index).some((other) => nodesEqual(other, node.items[index]))) return false;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
const legacyTuple = Array.isArray(schema.items) ? schema.items : undefined;
|
|
1065
|
+
const prefix = (Array.isArray(schema.prefixItems) ? schema.prefixItems : legacyTuple) ?? [];
|
|
1066
|
+
for (let index = 0; index < Math.min(prefix.length, node.items.length); ++index) {
|
|
1067
|
+
if (!isSchema(prefix[index]) || !validateNode(prefix[index], node.items[index], active)) return false;
|
|
1068
|
+
}
|
|
1069
|
+
const remaining = legacyTuple ? schema.additionalItems : schema.items;
|
|
1070
|
+
if (isSchema(remaining)) {
|
|
1071
|
+
for (let index = prefix.length; index < node.items.length; ++index) {
|
|
1072
|
+
if (!validateNode(remaining, node.items[index], active)) return false;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (isSchema(schema.contains)) {
|
|
1076
|
+
const matches = node.items.filter((item) => validateNode(schema.contains as Schema, item, active)).length;
|
|
1077
|
+
const minimum = typeof schema.minContains === 'number' ? schema.minContains : 1;
|
|
1078
|
+
if (matches < minimum || (typeof schema.maxContains === 'number' && matches > schema.maxContains)) return false;
|
|
1079
|
+
}
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function validateObjectNode(
|
|
1084
|
+
schema: Record<string, unknown>,
|
|
1085
|
+
node: Extract<JsonNode, { kind: 'object' }>,
|
|
1086
|
+
active: Map<object, Set<JsonNode>>,
|
|
1087
|
+
): boolean {
|
|
1088
|
+
const entries = new Map(node.entries.map((entry) => [entry.key, entry.node]));
|
|
1089
|
+
if (entries.size !== node.entries.length) return false;
|
|
1090
|
+
if (typeof schema.minProperties === 'number' && entries.size < schema.minProperties) return false;
|
|
1091
|
+
if (typeof schema.maxProperties === 'number' && entries.size > schema.maxProperties) return false;
|
|
1092
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
1093
|
+
if (!required.every((key) => typeof key === 'string' && entries.has(key))) return false;
|
|
1094
|
+
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
1095
|
+
const patterns = isRecord(schema.patternProperties) ? schema.patternProperties : {};
|
|
1096
|
+
for (const [key, child] of entries) {
|
|
1097
|
+
if (
|
|
1098
|
+
isSchema(schema.propertyNames) &&
|
|
1099
|
+
!validateNode(schema.propertyNames, { kind: 'string', value: key }, active)
|
|
1100
|
+
)
|
|
1101
|
+
return false;
|
|
1102
|
+
let matched = false;
|
|
1103
|
+
if (isSchema(properties[key])) {
|
|
1104
|
+
matched = true;
|
|
1105
|
+
if (!validateNode(properties[key], child, active)) return false;
|
|
1106
|
+
}
|
|
1107
|
+
for (const [pattern, patternSchema] of Object.entries(patterns)) {
|
|
1108
|
+
if (new RegExp(pattern, 'u').test(key)) {
|
|
1109
|
+
matched = true;
|
|
1110
|
+
if (!isSchema(patternSchema) || !validateNode(patternSchema, child, active)) return false;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
if (!matched) {
|
|
1114
|
+
if (schema.additionalProperties === false) return false;
|
|
1115
|
+
if (isSchema(schema.additionalProperties) && !validateNode(schema.additionalProperties, child, active))
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
const dependentRequired = isRecord(schema.dependentRequired) ? schema.dependentRequired : {};
|
|
1120
|
+
for (const [key, dependencies] of Object.entries(dependentRequired)) {
|
|
1121
|
+
if (
|
|
1122
|
+
entries.has(key) &&
|
|
1123
|
+
Array.isArray(dependencies) &&
|
|
1124
|
+
!dependencies.every((dependency) => typeof dependency === 'string' && entries.has(dependency))
|
|
1125
|
+
)
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1128
|
+
const dependentSchemas = isRecord(schema.dependentSchemas) ? schema.dependentSchemas : {};
|
|
1129
|
+
for (const [key, dependency] of Object.entries(dependentSchemas)) {
|
|
1130
|
+
if (entries.has(key) && (!isSchema(dependency) || !validateNode(dependency, node, active))) return false;
|
|
1131
|
+
}
|
|
1132
|
+
const dependencies = isRecord(schema.dependencies) ? schema.dependencies : {};
|
|
1133
|
+
for (const [key, dependency] of Object.entries(dependencies)) {
|
|
1134
|
+
if (!entries.has(key)) continue;
|
|
1135
|
+
if (Array.isArray(dependency)) {
|
|
1136
|
+
if (!dependency.every((requiredKey) => typeof requiredKey === 'string' && entries.has(requiredKey)))
|
|
1137
|
+
return false;
|
|
1138
|
+
} else if (!isSchema(dependency) || !validateNode(dependency, node, active)) return false;
|
|
1139
|
+
}
|
|
1140
|
+
return true;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function nodeHasType(node: JsonNode, type: string): boolean {
|
|
1144
|
+
if (type === 'integer') return node.kind === 'number' && decimalInteger(decimal(node.raw));
|
|
1145
|
+
if (type === 'number') return node.kind === 'number';
|
|
1146
|
+
return node.kind === type;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function nodeEqualsValue(node: JsonNode, value: unknown): boolean {
|
|
1150
|
+
if (node.kind === 'number')
|
|
1151
|
+
return typeof value === 'number' && compareDecimal(decimal(node.raw), decimal(String(value))) === 0;
|
|
1152
|
+
if (node.kind === 'array')
|
|
1153
|
+
return (
|
|
1154
|
+
Array.isArray(value) &&
|
|
1155
|
+
node.items.length === value.length &&
|
|
1156
|
+
node.items.every((item, index) => nodeEqualsValue(item, value[index]))
|
|
1157
|
+
);
|
|
1158
|
+
if (node.kind === 'object') {
|
|
1159
|
+
if (!isRecord(value)) return false;
|
|
1160
|
+
const entries = new Map(node.entries.map((entry) => [entry.key, entry.node]));
|
|
1161
|
+
return (
|
|
1162
|
+
entries.size === Object.keys(value).length &&
|
|
1163
|
+
Object.entries(value).every(([key, child]) => entries.has(key) && nodeEqualsValue(entries.get(key)!, child))
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
return Object.is(node.value, value);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function nodesEqual(left: JsonNode, right: JsonNode): boolean {
|
|
1170
|
+
if (left.kind !== right.kind) return false;
|
|
1171
|
+
if (left.kind === 'number' && right.kind === 'number')
|
|
1172
|
+
return compareDecimal(decimal(left.raw), decimal(right.raw)) === 0;
|
|
1173
|
+
if (left.kind === 'array' && right.kind === 'array')
|
|
1174
|
+
return (
|
|
1175
|
+
left.items.length === right.items.length &&
|
|
1176
|
+
left.items.every((item, index) => nodesEqual(item, right.items[index]))
|
|
1177
|
+
);
|
|
1178
|
+
if (left.kind === 'object' && right.kind === 'object') {
|
|
1179
|
+
const rightEntries = new Map(right.entries.map((entry) => [entry.key, entry.node]));
|
|
1180
|
+
return (
|
|
1181
|
+
left.entries.length === rightEntries.size &&
|
|
1182
|
+
left.entries.every(
|
|
1183
|
+
(entry) => rightEntries.has(entry.key) && nodesEqual(entry.node, rightEntries.get(entry.key)!),
|
|
1184
|
+
)
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
return Object.is(left.value, right.value);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
type Decimal = { coefficient: bigint; exponent: bigint };
|
|
1191
|
+
|
|
1192
|
+
function decimal(raw: string): Decimal {
|
|
1193
|
+
const match = /^(-?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(raw)!;
|
|
1194
|
+
const fraction = match[3] ?? '';
|
|
1195
|
+
let digits = `${match[2]}${fraction}`.replace(/^0+/, '');
|
|
1196
|
+
if (digits === '') return { coefficient: 0n, exponent: 0n };
|
|
1197
|
+
const trailing = /0+$/.exec(digits)?.[0].length ?? 0;
|
|
1198
|
+
if (trailing > 0) digits = digits.slice(0, -trailing);
|
|
1199
|
+
return {
|
|
1200
|
+
coefficient: BigInt(`${match[1]}${digits}`),
|
|
1201
|
+
exponent: BigInt(match[4] ?? '0') - BigInt(fraction.length) + BigInt(trailing),
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function compareDecimal(left: Decimal, right: Decimal): number {
|
|
1206
|
+
if (left.coefficient === right.coefficient && left.exponent === right.exponent) return 0;
|
|
1207
|
+
if (left.coefficient === 0n) return right.coefficient < 0n ? 1 : -1;
|
|
1208
|
+
if (right.coefficient === 0n) return left.coefficient < 0n ? -1 : 1;
|
|
1209
|
+
if (left.coefficient < 0n && right.coefficient >= 0n) return -1;
|
|
1210
|
+
if (left.coefficient >= 0n && right.coefficient < 0n) return 1;
|
|
1211
|
+
const negative = left.coefficient < 0n;
|
|
1212
|
+
const leftAbsolute = left.coefficient < 0n ? -left.coefficient : left.coefficient;
|
|
1213
|
+
const rightAbsolute = right.coefficient < 0n ? -right.coefficient : right.coefficient;
|
|
1214
|
+
const leftMagnitude = BigInt(leftAbsolute.toString().length) + left.exponent;
|
|
1215
|
+
const rightMagnitude = BigInt(rightAbsolute.toString().length) + right.exponent;
|
|
1216
|
+
if (leftMagnitude !== rightMagnitude) {
|
|
1217
|
+
const comparison = leftMagnitude < rightMagnitude ? -1 : 1;
|
|
1218
|
+
return negative ? -comparison : comparison;
|
|
1219
|
+
}
|
|
1220
|
+
const exponent = left.exponent < right.exponent ? left.exponent : right.exponent;
|
|
1221
|
+
const scaledLeft = left.coefficient * 10n ** (left.exponent - exponent);
|
|
1222
|
+
const scaledRight = right.coefficient * 10n ** (right.exponent - exponent);
|
|
1223
|
+
return scaledLeft < scaledRight ? -1 : scaledLeft > scaledRight ? 1 : 0;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function decimalInteger(value: Decimal): boolean {
|
|
1227
|
+
return value.coefficient === 0n || value.exponent >= 0n;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function decimalMultiple(value: Decimal, divisor: Decimal): boolean {
|
|
1231
|
+
if (divisor.coefficient === 0n) return false;
|
|
1232
|
+
if (value.coefficient === 0n) return true;
|
|
1233
|
+
const numerator = value.coefficient < 0n ? -value.coefficient : value.coefficient;
|
|
1234
|
+
const denominator = divisor.coefficient < 0n ? -divisor.coefficient : divisor.coefficient;
|
|
1235
|
+
const exponent = value.exponent - divisor.exponent;
|
|
1236
|
+
if (exponent >= 0n) {
|
|
1237
|
+
return ((numerator % denominator) * modularPower(10n, exponent, denominator)) % denominator === 0n;
|
|
1238
|
+
}
|
|
1239
|
+
const places = -exponent;
|
|
1240
|
+
if (places >= BigInt(numerator.toString().length)) return false;
|
|
1241
|
+
return numerator % (denominator * 10n ** places) === 0n;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function modularPower(base: bigint, exponent: bigint, modulus: bigint): bigint {
|
|
1245
|
+
if (modulus === 1n) return 0n;
|
|
1246
|
+
let result = 1n;
|
|
1247
|
+
base %= modulus;
|
|
1248
|
+
while (exponent > 0n) {
|
|
1249
|
+
if (exponent & 1n) result = (result * base) % modulus;
|
|
1250
|
+
base = (base * base) % modulus;
|
|
1251
|
+
exponent >>= 1n;
|
|
1252
|
+
}
|
|
1253
|
+
return result;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function hasDeferredStructure(schema: Schema): boolean {
|
|
1257
|
+
return (
|
|
1258
|
+
schema !== true &&
|
|
1259
|
+
schema !== false &&
|
|
1260
|
+
['$ref', 'allOf', 'anyOf', 'oneOf', 'not', 'if'].some((key) => schema[key] !== undefined)
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function schemaMayAcceptKind(schema: Schema, kind: Kind, seen: Set<object>): boolean {
|
|
1265
|
+
if (schema === true) return true;
|
|
1266
|
+
if (schema === false) return false;
|
|
1267
|
+
if (seen.has(schema)) return true;
|
|
1268
|
+
seen.add(schema);
|
|
1269
|
+
if (typeof schema.$ref === 'string' && !schemaMayAcceptKind(resolveReference(schema, schema.$ref), kind, seen))
|
|
1270
|
+
return false;
|
|
1271
|
+
if (!allowsKind(schema, kind)) return false;
|
|
1272
|
+
if (
|
|
1273
|
+
Array.isArray(schema.allOf) &&
|
|
1274
|
+
!schema.allOf.every((child) => isSchema(child) && schemaMayAcceptKind(child, kind, seen))
|
|
1275
|
+
)
|
|
1276
|
+
return false;
|
|
1277
|
+
if (
|
|
1278
|
+
Array.isArray(schema.anyOf) &&
|
|
1279
|
+
!schema.anyOf.some((child) => isSchema(child) && schemaMayAcceptKind(child, kind, seen))
|
|
1280
|
+
)
|
|
1281
|
+
return false;
|
|
1282
|
+
if (
|
|
1283
|
+
Array.isArray(schema.oneOf) &&
|
|
1284
|
+
!schema.oneOf.some((child) => isSchema(child) && schemaMayAcceptKind(child, kind, seen))
|
|
1285
|
+
)
|
|
1286
|
+
return false;
|
|
1287
|
+
return true;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function resolveReference(owner: object, reference: string): Schema {
|
|
1291
|
+
if (!reference.startsWith('#'))
|
|
1292
|
+
throw new TypeError(`External JSON Schema reference ${JSON.stringify(reference)} is unsupported.`);
|
|
1293
|
+
const root = schemaContexts.get(owner);
|
|
1294
|
+
if (root === undefined) throw new Error('Missing JSON Schema context.');
|
|
1295
|
+
if (reference === '#') return root;
|
|
1296
|
+
if (!reference.startsWith('#/')) {
|
|
1297
|
+
throw new TypeError(`JSON Schema reference ${JSON.stringify(reference)} must contain a JSON Pointer.`);
|
|
1298
|
+
}
|
|
1299
|
+
let current: unknown = root;
|
|
1300
|
+
for (const encoded of reference.slice(2).split('/')) {
|
|
1301
|
+
const key = decodeURIComponent(encoded).replace(/~1/g, '/').replace(/~0/g, '~');
|
|
1302
|
+
if (!isRecord(current) || !(key in current))
|
|
1303
|
+
throw new TypeError(`JSON Schema reference ${JSON.stringify(reference)} does not resolve.`);
|
|
1304
|
+
current = current[key];
|
|
1305
|
+
}
|
|
1306
|
+
if (!isSchema(current))
|
|
1307
|
+
throw new TypeError(`JSON Schema reference ${JSON.stringify(reference)} does not resolve to a schema.`);
|
|
1308
|
+
return current;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function registerSchemaContext(schema: Schema, root: Schema, seen = new Set<object>()): void {
|
|
1312
|
+
if (!isRecord(schema) || seen.has(schema)) return;
|
|
1313
|
+
seen.add(schema);
|
|
1314
|
+
schemaContexts.set(schema, root);
|
|
1315
|
+
for (const child of schemaChildren(schema)) registerSchemaContext(child, root, seen);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function schemaChildren(schema: Record<string, unknown>): Schema[] {
|
|
1319
|
+
const result: Schema[] = [];
|
|
1320
|
+
for (const key of [
|
|
1321
|
+
'not',
|
|
1322
|
+
'if',
|
|
1323
|
+
'then',
|
|
1324
|
+
'else',
|
|
1325
|
+
'contains',
|
|
1326
|
+
'propertyNames',
|
|
1327
|
+
'additionalProperties',
|
|
1328
|
+
'additionalItems',
|
|
1329
|
+
]) {
|
|
1330
|
+
if (isSchema(schema[key])) result.push(schema[key] as Schema);
|
|
1331
|
+
}
|
|
1332
|
+
if (isSchema(schema.items)) result.push(schema.items);
|
|
1333
|
+
if (Array.isArray(schema.items)) result.push(...schema.items.filter(isSchema));
|
|
1334
|
+
for (const key of ['prefixItems', 'allOf', 'anyOf', 'oneOf']) {
|
|
1335
|
+
if (Array.isArray(schema[key])) result.push(...(schema[key] as unknown[]).filter(isSchema));
|
|
1336
|
+
}
|
|
1337
|
+
for (const key of ['properties', 'patternProperties', 'dependentSchemas', '$defs', 'definitions']) {
|
|
1338
|
+
if (isRecord(schema[key]))
|
|
1339
|
+
result.push(...Object.values(schema[key] as Record<string, unknown>).filter(isSchema));
|
|
1340
|
+
}
|
|
1341
|
+
if (isRecord(schema.dependencies)) {
|
|
1342
|
+
result.push(...Object.values(schema.dependencies).filter(isSchema));
|
|
1343
|
+
}
|
|
1344
|
+
return result;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function checkReferences(schema: Schema, seen = new Set<object>()): void {
|
|
1348
|
+
if (!isRecord(schema) || seen.has(schema)) return;
|
|
1349
|
+
seen.add(schema);
|
|
1350
|
+
if (typeof schema.$ref === 'string') resolveReference(schema, schema.$ref);
|
|
1351
|
+
for (const child of schemaChildren(schema)) checkReferences(child, seen);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function assertPattern(value: unknown, path: string): void {
|
|
1355
|
+
if (typeof value !== 'string') throw new TypeError(`${path} must be a string.`);
|
|
1356
|
+
try {
|
|
1357
|
+
new RegExp(value, 'u');
|
|
1358
|
+
} catch {
|
|
1359
|
+
throw new TypeError(`${path} must be a valid Unicode RegExp.`);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function guidanceFrom(schema: Schema): Guidance {
|
|
1364
|
+
if (!isRecord(schema) || schema['x-guidance'] === undefined) return DEFAULT_GUIDANCE;
|
|
1365
|
+
if (!isRecord(schema['x-guidance'])) throw new TypeError('x-guidance must be an object.');
|
|
1366
|
+
const source = schema['x-guidance'];
|
|
1367
|
+
for (const key of Object.keys(source)) {
|
|
1368
|
+
if (!['item_separator', 'key_separator', 'whitespace_flexible'].includes(key)) {
|
|
1369
|
+
throw new TypeError(`Unsupported x-guidance option ${JSON.stringify(key)}.`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
const itemSeparator = source.item_separator ?? ',';
|
|
1373
|
+
const keySeparator = source.key_separator ?? ':';
|
|
1374
|
+
const whitespaceFlexible = source.whitespace_flexible ?? true;
|
|
1375
|
+
if (typeof itemSeparator !== 'string' || itemSeparator.length === 0) {
|
|
1376
|
+
throw new TypeError('x-guidance.item_separator must be a non-empty string.');
|
|
1377
|
+
}
|
|
1378
|
+
if (typeof keySeparator !== 'string' || keySeparator.length === 0) {
|
|
1379
|
+
throw new TypeError('x-guidance.key_separator must be a non-empty string.');
|
|
1380
|
+
}
|
|
1381
|
+
if (typeof whitespaceFlexible !== 'boolean')
|
|
1382
|
+
throw new TypeError('x-guidance.whitespace_flexible must be a boolean.');
|
|
1383
|
+
return {
|
|
1384
|
+
itemSeparator,
|
|
1385
|
+
keySeparator,
|
|
1386
|
+
itemBytes: encoder.encode(itemSeparator),
|
|
1387
|
+
keyBytes: encoder.encode(keySeparator),
|
|
1388
|
+
whitespaceFlexible,
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function formatMatches(format: string, value: string): boolean {
|
|
1393
|
+
switch (format) {
|
|
1394
|
+
case 'date':
|
|
1395
|
+
return validDate(value);
|
|
1396
|
+
case 'time':
|
|
1397
|
+
return validTime(value);
|
|
1398
|
+
case 'date-time': {
|
|
1399
|
+
const separator = value.search(/[Tt]/);
|
|
1400
|
+
return separator > 0 && validDate(value.slice(0, separator)) && validTime(value.slice(separator + 1));
|
|
1401
|
+
}
|
|
1402
|
+
case 'duration':
|
|
1403
|
+
return /^(?:P\d+W|P(?=\d|T\d)(?:\d+Y)?(?:\d+M)?(?:\d+D)?(?:T(?=\d)(?:\d+H)?(?:\d+M)?(?:\d+S)?)?)$/.test(
|
|
1404
|
+
value,
|
|
1405
|
+
);
|
|
1406
|
+
case 'email': {
|
|
1407
|
+
const at = value.lastIndexOf('@');
|
|
1408
|
+
if (at <= 0 || at === value.length - 1) return false;
|
|
1409
|
+
const local = value.slice(0, at);
|
|
1410
|
+
const domain = value.slice(at + 1);
|
|
1411
|
+
if (
|
|
1412
|
+
local.startsWith('.') ||
|
|
1413
|
+
local.endsWith('.') ||
|
|
1414
|
+
local.includes('..') ||
|
|
1415
|
+
!/^[A-Za-z0-9!#$%&'*+\-/=?^_`{|}~.]+$/.test(local)
|
|
1416
|
+
)
|
|
1417
|
+
return false;
|
|
1418
|
+
return /^\[(.+)\]$/.test(domain) ? validIpv4(domain.slice(1, -1)) : validHostname(domain);
|
|
1419
|
+
}
|
|
1420
|
+
case 'hostname':
|
|
1421
|
+
return validHostname(value);
|
|
1422
|
+
case 'ipv4':
|
|
1423
|
+
return validIpv4(value);
|
|
1424
|
+
case 'ipv6':
|
|
1425
|
+
return validIpv6(value);
|
|
1426
|
+
case 'uuid':
|
|
1427
|
+
return /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i.test(value);
|
|
1428
|
+
case 'uri':
|
|
1429
|
+
return validUriText(value, true);
|
|
1430
|
+
case 'uri-reference':
|
|
1431
|
+
return validUriText(value, false);
|
|
1432
|
+
case 'regex':
|
|
1433
|
+
try {
|
|
1434
|
+
new RegExp(value, 'u');
|
|
1435
|
+
return true;
|
|
1436
|
+
} catch {
|
|
1437
|
+
return false;
|
|
1438
|
+
}
|
|
1439
|
+
case 'json-pointer':
|
|
1440
|
+
return /^(?:\/(?:[^~/]|~[01])*)*$/u.test(value);
|
|
1441
|
+
case 'relative-json-pointer':
|
|
1442
|
+
return /^(?:0|[1-9]\d*)(?:#|(?:\/(?:[^~/]|~[01])*)*)$/u.test(value);
|
|
1443
|
+
default:
|
|
1444
|
+
return true;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function validDate(value: string): boolean {
|
|
1449
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
1450
|
+
if (!match) return false;
|
|
1451
|
+
const year = Number(match[1]);
|
|
1452
|
+
const month = Number(match[2]);
|
|
1453
|
+
const day = Number(match[3]);
|
|
1454
|
+
const days = [
|
|
1455
|
+
31,
|
|
1456
|
+
year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28,
|
|
1457
|
+
31,
|
|
1458
|
+
30,
|
|
1459
|
+
31,
|
|
1460
|
+
30,
|
|
1461
|
+
31,
|
|
1462
|
+
31,
|
|
1463
|
+
30,
|
|
1464
|
+
31,
|
|
1465
|
+
30,
|
|
1466
|
+
31,
|
|
1467
|
+
];
|
|
1468
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= days[month - 1];
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function validTime(value: string): boolean {
|
|
1472
|
+
const match = /^(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:[Zz]|[+-](\d{2}):(\d{2}))$/.exec(value);
|
|
1473
|
+
if (!match) return false;
|
|
1474
|
+
const hour = Number(match[1]);
|
|
1475
|
+
const minute = Number(match[2]);
|
|
1476
|
+
const second = Number(match[3]);
|
|
1477
|
+
if (hour > 23 || minute > 59 || second > 60 || (second === 60 && (hour !== 23 || minute !== 59))) return false;
|
|
1478
|
+
return match[4] === undefined || (Number(match[4]) <= 23 && Number(match[5]) <= 59);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function validHostname(value: string): boolean {
|
|
1482
|
+
return (
|
|
1483
|
+
value.length > 0 &&
|
|
1484
|
+
value.length <= 253 &&
|
|
1485
|
+
!value.endsWith('.') &&
|
|
1486
|
+
value.split('.').every((label) => /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/.test(label))
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function validIpv4(value: string): boolean {
|
|
1491
|
+
const parts = value.split('.');
|
|
1492
|
+
return parts.length === 4 && parts.every((part) => /^(?:0|[1-9]\d{0,2})$/.test(part) && Number(part) <= 255);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function validIpv6(value: string): boolean {
|
|
1496
|
+
if (value.includes(':::') || value.split('::').length > 2) return false;
|
|
1497
|
+
const compressed = value.includes('::');
|
|
1498
|
+
const groups = value.split(':').filter(Boolean);
|
|
1499
|
+
if (
|
|
1500
|
+
!groups.every((group, index) =>
|
|
1501
|
+
group.includes('.') ? index === groups.length - 1 && validIpv4(group) : /^[\da-f]{1,4}$/i.test(group),
|
|
1502
|
+
)
|
|
1503
|
+
)
|
|
1504
|
+
return false;
|
|
1505
|
+
const count = groups.reduce((total, group) => total + (group.includes('.') ? 2 : 1), 0);
|
|
1506
|
+
return compressed ? count < 8 : count === 8;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function validUriText(value: string, absolute: boolean): boolean {
|
|
1510
|
+
if (/[\s\\<>^`{|}\0]/.test(value) || /%(?![\da-f]{2})/i.test(value)) return false;
|
|
1511
|
+
let rest = value;
|
|
1512
|
+
const scheme = /^[A-Za-z][A-Za-z0-9+.-]*:/.exec(rest);
|
|
1513
|
+
if (absolute && !scheme) return false;
|
|
1514
|
+
if (scheme) rest = rest.slice(scheme[0].length);
|
|
1515
|
+
const fragment = rest.indexOf('#');
|
|
1516
|
+
if (fragment !== -1) {
|
|
1517
|
+
if (!uriQuery.test(rest.slice(fragment + 1))) return false;
|
|
1518
|
+
rest = rest.slice(0, fragment);
|
|
1519
|
+
}
|
|
1520
|
+
const query = rest.indexOf('?');
|
|
1521
|
+
if (query !== -1) {
|
|
1522
|
+
if (!uriQuery.test(rest.slice(query + 1))) return false;
|
|
1523
|
+
rest = rest.slice(0, query);
|
|
1524
|
+
}
|
|
1525
|
+
if (rest.startsWith('//')) {
|
|
1526
|
+
const slash = rest.indexOf('/', 2);
|
|
1527
|
+
const authority = rest.slice(2, slash === -1 ? undefined : slash);
|
|
1528
|
+
const path = slash === -1 ? '' : rest.slice(slash);
|
|
1529
|
+
return validAuthority(authority) && uriPath.test(path);
|
|
1530
|
+
}
|
|
1531
|
+
if (!uriPath.test(rest)) return false;
|
|
1532
|
+
if (scheme || rest === '' || rest.startsWith('/')) return true;
|
|
1533
|
+
return !rest.slice(0, rest.indexOf('/') === -1 ? undefined : rest.indexOf('/')).includes(':');
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const uriEncoded = '%[\\da-fA-F]{2}';
|
|
1537
|
+
const uriPchar = `(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|${uriEncoded})`;
|
|
1538
|
+
const uriPath = new RegExp(`^(?:${uriPchar}|/)*$`);
|
|
1539
|
+
const uriQuery = new RegExp(`^(?:${uriPchar}|[/?])*$`);
|
|
1540
|
+
const uriRegName = new RegExp(`^(?:[A-Za-z0-9._~!$&'()*+,;=-]|${uriEncoded})*$`);
|
|
1541
|
+
const uriUserInfo = new RegExp(`^(?:[A-Za-z0-9._~!$&'()*+,;=:-]|${uriEncoded})*$`);
|
|
1542
|
+
|
|
1543
|
+
function validAuthority(authority: string): boolean {
|
|
1544
|
+
const at = authority.lastIndexOf('@');
|
|
1545
|
+
if (at !== -1) {
|
|
1546
|
+
if (authority.indexOf('@') !== at || !uriUserInfo.test(authority.slice(0, at))) return false;
|
|
1547
|
+
authority = authority.slice(at + 1);
|
|
1548
|
+
}
|
|
1549
|
+
if (authority.startsWith('[')) {
|
|
1550
|
+
const close = authority.indexOf(']');
|
|
1551
|
+
if (close === -1) return false;
|
|
1552
|
+
const host = authority.slice(1, close);
|
|
1553
|
+
const suffix = authority.slice(close + 1);
|
|
1554
|
+
return (
|
|
1555
|
+
(validIpv6(host) || /^v[\da-f]+\.[A-Za-z0-9._~!$&'()*+,;=:-]+$/i.test(host)) &&
|
|
1556
|
+
(suffix === '' || /^:\d*$/.test(suffix))
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
const colon = authority.lastIndexOf(':');
|
|
1560
|
+
let host = authority;
|
|
1561
|
+
if (colon !== -1) {
|
|
1562
|
+
if (authority.indexOf(':') !== colon || !/^\d*$/.test(authority.slice(colon + 1))) return false;
|
|
1563
|
+
host = authority.slice(0, colon);
|
|
1564
|
+
}
|
|
1565
|
+
return uriRegName.test(host);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function checkSchema(schema: Schema, path: string): void {
|
|
1569
|
+
if (typeof schema === 'boolean') return;
|
|
1570
|
+
if (!isRecord(schema)) throw new TypeError(`${path} must be a boolean or object schema.`);
|
|
1571
|
+
const supported = new Set([
|
|
1572
|
+
'type',
|
|
1573
|
+
'const',
|
|
1574
|
+
'enum',
|
|
1575
|
+
'properties',
|
|
1576
|
+
'required',
|
|
1577
|
+
'additionalProperties',
|
|
1578
|
+
'minProperties',
|
|
1579
|
+
'maxProperties',
|
|
1580
|
+
'items',
|
|
1581
|
+
'prefixItems',
|
|
1582
|
+
'minItems',
|
|
1583
|
+
'maxItems',
|
|
1584
|
+
'uniqueItems',
|
|
1585
|
+
'minLength',
|
|
1586
|
+
'maxLength',
|
|
1587
|
+
'pattern',
|
|
1588
|
+
'format',
|
|
1589
|
+
'minimum',
|
|
1590
|
+
'maximum',
|
|
1591
|
+
'exclusiveMinimum',
|
|
1592
|
+
'exclusiveMaximum',
|
|
1593
|
+
'multipleOf',
|
|
1594
|
+
'contains',
|
|
1595
|
+
'minContains',
|
|
1596
|
+
'maxContains',
|
|
1597
|
+
'patternProperties',
|
|
1598
|
+
'propertyNames',
|
|
1599
|
+
'dependentRequired',
|
|
1600
|
+
'dependentSchemas',
|
|
1601
|
+
'dependencies',
|
|
1602
|
+
'anyOf',
|
|
1603
|
+
'allOf',
|
|
1604
|
+
'oneOf',
|
|
1605
|
+
'not',
|
|
1606
|
+
'if',
|
|
1607
|
+
'then',
|
|
1608
|
+
'else',
|
|
1609
|
+
'$ref',
|
|
1610
|
+
'$defs',
|
|
1611
|
+
'definitions',
|
|
1612
|
+
'additionalItems',
|
|
1613
|
+
'x-guidance',
|
|
1614
|
+
'title',
|
|
1615
|
+
'description',
|
|
1616
|
+
'$schema',
|
|
1617
|
+
'$id',
|
|
1618
|
+
'$comment',
|
|
1619
|
+
'default',
|
|
1620
|
+
'examples',
|
|
1621
|
+
'deprecated',
|
|
1622
|
+
'readOnly',
|
|
1623
|
+
'writeOnly',
|
|
1624
|
+
'contentEncoding',
|
|
1625
|
+
'contentMediaType',
|
|
1626
|
+
'contentSchema',
|
|
1627
|
+
]);
|
|
1628
|
+
for (const key of Object.keys(schema)) {
|
|
1629
|
+
if (!supported.has(key))
|
|
1630
|
+
throw new TypeError(`${path}: unsupported JSON Schema keyword ${JSON.stringify(key)}.`);
|
|
1631
|
+
}
|
|
1632
|
+
const configuredTypes = schema.type === undefined ? [] : Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
1633
|
+
if (
|
|
1634
|
+
configuredTypes.some(
|
|
1635
|
+
(type) => !['null', 'boolean', 'number', 'integer', 'string', 'array', 'object'].includes(String(type)),
|
|
1636
|
+
)
|
|
1637
|
+
) {
|
|
1638
|
+
throw new TypeError(`${path}.type contains an unsupported JSON type.`);
|
|
1639
|
+
}
|
|
1640
|
+
for (const keyword of [
|
|
1641
|
+
'not',
|
|
1642
|
+
'if',
|
|
1643
|
+
'then',
|
|
1644
|
+
'else',
|
|
1645
|
+
'contains',
|
|
1646
|
+
'propertyNames',
|
|
1647
|
+
'additionalProperties',
|
|
1648
|
+
'additionalItems',
|
|
1649
|
+
]) {
|
|
1650
|
+
if (schema[keyword] !== undefined && !isSchema(schema[keyword])) {
|
|
1651
|
+
throw new TypeError(`${path}.${keyword} must be a boolean or object schema.`);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
for (const keyword of ['allOf', 'anyOf', 'oneOf', 'prefixItems']) {
|
|
1655
|
+
const value = schema[keyword];
|
|
1656
|
+
if (
|
|
1657
|
+
value !== undefined &&
|
|
1658
|
+
(!Array.isArray(value) ||
|
|
1659
|
+
(keyword !== 'prefixItems' && value.length === 0) ||
|
|
1660
|
+
value.some((child) => !isSchema(child)))
|
|
1661
|
+
) {
|
|
1662
|
+
throw new TypeError(`${path}.${keyword} must be an array of schemas.`);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
for (const keyword of ['properties', 'patternProperties', 'dependentSchemas', '$defs', 'definitions']) {
|
|
1666
|
+
const value = schema[keyword];
|
|
1667
|
+
if (value !== undefined && (!isRecord(value) || Object.values(value).some((child) => !isSchema(child)))) {
|
|
1668
|
+
throw new TypeError(`${path}.${keyword} must be an object containing schemas.`);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
if (
|
|
1672
|
+
schema.items !== undefined &&
|
|
1673
|
+
!isSchema(schema.items) &&
|
|
1674
|
+
!(Array.isArray(schema.items) && schema.items.every(isSchema))
|
|
1675
|
+
) {
|
|
1676
|
+
throw new TypeError(`${path}.items must be a schema or array of schemas.`);
|
|
1677
|
+
}
|
|
1678
|
+
if (
|
|
1679
|
+
schema.required !== undefined &&
|
|
1680
|
+
(!Array.isArray(schema.required) ||
|
|
1681
|
+
schema.required.some((key) => typeof key !== 'string') ||
|
|
1682
|
+
new Set(schema.required).size !== schema.required.length)
|
|
1683
|
+
) {
|
|
1684
|
+
throw new TypeError(`${path}.required must be an array of unique strings.`);
|
|
1685
|
+
}
|
|
1686
|
+
if (schema.dependentRequired !== undefined) {
|
|
1687
|
+
if (!isRecord(schema.dependentRequired)) throw new TypeError(`${path}.dependentRequired must be an object.`);
|
|
1688
|
+
for (const dependency of Object.values(schema.dependentRequired)) {
|
|
1689
|
+
if (
|
|
1690
|
+
!Array.isArray(dependency) ||
|
|
1691
|
+
dependency.some((key) => typeof key !== 'string') ||
|
|
1692
|
+
new Set(dependency).size !== dependency.length
|
|
1693
|
+
) {
|
|
1694
|
+
throw new TypeError(`${path}.dependentRequired values must be arrays of unique strings.`);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
if (schema.dependencies !== undefined && !isRecord(schema.dependencies)) {
|
|
1699
|
+
throw new TypeError(`${path}.dependencies must be an object.`);
|
|
1700
|
+
}
|
|
1701
|
+
if (schema.$ref !== undefined && typeof schema.$ref !== 'string')
|
|
1702
|
+
throw new TypeError(`${path}.$ref must be a string.`);
|
|
1703
|
+
if (schema.enum !== undefined && (!Array.isArray(schema.enum) || schema.enum.length === 0)) {
|
|
1704
|
+
throw new TypeError(`${path}.enum must be a non-empty array.`);
|
|
1705
|
+
}
|
|
1706
|
+
for (const keyword of ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf']) {
|
|
1707
|
+
const value = schema[keyword];
|
|
1708
|
+
if (
|
|
1709
|
+
value !== undefined &&
|
|
1710
|
+
(typeof value !== 'number' || !Number.isFinite(value) || (keyword === 'multipleOf' && value <= 0))
|
|
1711
|
+
) {
|
|
1712
|
+
throw new TypeError(
|
|
1713
|
+
`${path}.${keyword} must be ${keyword === 'multipleOf' ? 'a positive' : 'a finite'} number.`,
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
if (schema.pattern !== undefined) {
|
|
1718
|
+
assertPattern(schema.pattern, `${path}.pattern`);
|
|
1719
|
+
throw new TypeError(`${path}.pattern cannot be enforced incrementally and is unsupported.`);
|
|
1720
|
+
}
|
|
1721
|
+
if (schema.format !== undefined && typeof schema.format !== 'string') {
|
|
1722
|
+
throw new TypeError(`${path}.format must be a string.`);
|
|
1723
|
+
}
|
|
1724
|
+
if (typeof schema.format === 'string' && ASSERTED_FORMATS.has(schema.format)) {
|
|
1725
|
+
throw new TypeError(
|
|
1726
|
+
`${path}.format ${JSON.stringify(schema.format)} cannot be enforced incrementally and is unsupported.`,
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
if (isRecord(schema.patternProperties)) {
|
|
1730
|
+
for (const pattern of Object.keys(schema.patternProperties))
|
|
1731
|
+
assertPattern(pattern, `${path}.patternProperties`);
|
|
1732
|
+
}
|
|
1733
|
+
if (schema.uniqueItems !== undefined && typeof schema.uniqueItems !== 'boolean') {
|
|
1734
|
+
throw new TypeError(`${path}.uniqueItems must be a boolean.`);
|
|
1735
|
+
}
|
|
1736
|
+
for (const keyword of [
|
|
1737
|
+
'minItems',
|
|
1738
|
+
'maxItems',
|
|
1739
|
+
'minContains',
|
|
1740
|
+
'maxContains',
|
|
1741
|
+
'minProperties',
|
|
1742
|
+
'maxProperties',
|
|
1743
|
+
'minLength',
|
|
1744
|
+
'maxLength',
|
|
1745
|
+
]) {
|
|
1746
|
+
const value = schema[keyword];
|
|
1747
|
+
if (value !== undefined && (!Number.isInteger(value) || (value as number) < 0)) {
|
|
1748
|
+
throw new TypeError(`${path}.${keyword} must be a non-negative integer.`);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
if (schema['x-guidance'] !== undefined && path !== '$') {
|
|
1752
|
+
throw new TypeError('x-guidance is only supported on the root schema.');
|
|
1753
|
+
}
|
|
1754
|
+
schemaChildren(schema).forEach((child, index) => checkSchema(child, `${path}.schema[${index}]`));
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
function allowsKind(schema: Record<string, unknown>, kind: Kind): boolean {
|
|
1758
|
+
if (
|
|
1759
|
+
schema.const !== undefined &&
|
|
1760
|
+
valueKind(schema.const) !== kind &&
|
|
1761
|
+
!(kind === 'number' && valueKind(schema.const) === 'integer')
|
|
1762
|
+
)
|
|
1763
|
+
return false;
|
|
1764
|
+
if (
|
|
1765
|
+
Array.isArray(schema.enum) &&
|
|
1766
|
+
!schema.enum.some((value) => valueKind(value) === kind || (kind === 'number' && valueKind(value) === 'integer'))
|
|
1767
|
+
)
|
|
1768
|
+
return false;
|
|
1769
|
+
if (schema.type === undefined) return true;
|
|
1770
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
1771
|
+
return (
|
|
1772
|
+
types.includes(kind) ||
|
|
1773
|
+
(kind === 'number' && types.includes('integer')) ||
|
|
1774
|
+
(kind === 'integer' && types.includes('number'))
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
function valueKind(value: unknown): Kind | undefined {
|
|
1779
|
+
if (value === null) return 'null';
|
|
1780
|
+
if (Array.isArray(value)) return 'array';
|
|
1781
|
+
if (isRecord(value)) return 'object';
|
|
1782
|
+
if (typeof value === 'number') return Number.isInteger(value) ? 'integer' : 'number';
|
|
1783
|
+
if (['boolean', 'string'].includes(typeof value)) return typeof value as Kind;
|
|
1784
|
+
return undefined;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
function byteKind(byte: number): Kind | undefined {
|
|
1788
|
+
if (byte === 0x22) return 'string';
|
|
1789
|
+
if (byte === 0x7b) return 'object';
|
|
1790
|
+
if (byte === 0x5b) return 'array';
|
|
1791
|
+
if (byte === 0x74 || byte === 0x66) return 'boolean';
|
|
1792
|
+
if (byte === 0x6e) return 'null';
|
|
1793
|
+
if (byte === 0x2d || isDigit(byte)) return 'number';
|
|
1794
|
+
return undefined;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
function numberPrefixValid(value: string): boolean {
|
|
1798
|
+
// The exponent may only start after at least one fraction digit — "1.e"
|
|
1799
|
+
// would be a syntactically extendable prefix that can never complete.
|
|
1800
|
+
return (
|
|
1801
|
+
/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d*)?$/.test(value) ||
|
|
1802
|
+
/^-?(?:0|[1-9]\d*)\.$/.test(value) ||
|
|
1803
|
+
value === '-'
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
function numberComplete(value: string): boolean {
|
|
1808
|
+
return /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(value);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
function isNumberByte(byte: number): boolean {
|
|
1812
|
+
return isDigit(byte) || byte === 0x2e || byte === 0x45 || byte === 0x65 || byte === 0x2b || byte === 0x2d;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
function decodeString(bytes: readonly number[]): string | null {
|
|
1816
|
+
try {
|
|
1817
|
+
return decoder.decode(Uint8Array.from(bytes));
|
|
1818
|
+
} catch {
|
|
1819
|
+
return null;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
function topObject(state: JsonState): ObjectFrame {
|
|
1824
|
+
return state.stack.at(-1) as ObjectFrame;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function replaceTop(stack: readonly Frame[], frame: Frame): Frame[] {
|
|
1828
|
+
return [...stack.slice(0, -1), frame];
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
function allowsWhitespace(mode: Mode): boolean {
|
|
1832
|
+
return ['value', 'array-value', 'object-key', 'colon', 'item-separator', 'after-value', 'done'].includes(mode);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
function isWhitespace(byte: number): boolean {
|
|
1836
|
+
return byte === 0x09 || byte === 0x0a || byte === 0x0d || byte === 0x20;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
function isDigit(byte: number): boolean {
|
|
1840
|
+
return byte >= 0x30 && byte <= 0x39;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function isHex(byte: number): boolean {
|
|
1844
|
+
return isDigit(byte) || (byte >= 0x41 && byte <= 0x46) || (byte >= 0x61 && byte <= 0x66);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
function isSchema(value: unknown): value is Schema {
|
|
1848
|
+
return typeof value === 'boolean' || isRecord(value);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1852
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1853
|
+
}
|