ata-validator 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/ata.cpp DELETED
@@ -1,3146 +0,0 @@
1
- #include "ata.h"
2
-
3
- // mimalloc: faster new/delete for small allocations.
4
- #if __has_include(<mimalloc-new-delete.h>)
5
- #include <mimalloc-new-delete.h>
6
- #endif
7
-
8
- #include <algorithm>
9
- #include <cmath>
10
- #include <cstring>
11
- #ifndef ATA_NO_RE2
12
- #include <re2/re2.h>
13
- #endif
14
- #include <set>
15
- #include <unordered_map>
16
-
17
- #ifdef _WIN32
18
- #include <windows.h>
19
- #include <sysinfoapi.h>
20
- #else
21
- #include <unistd.h>
22
- #endif
23
-
24
- // MSVC implementation by Pavel P (https://gist.github.com/pps83/3210a2f980fd02bb2ba2e5a1fc4a2ef0)
25
- #if defined(_MSC_VER) && !defined(__clang__)
26
- #include <intrin.h>
27
- #ifndef __builtin_popcount
28
- #define __builtin_popcount __popcnt
29
- #endif
30
- #endif // defined(_MSC_VER) && !defined(__clang__)
31
-
32
- #include "simdjson.h"
33
-
34
- // --- Fast format validators (no std::regex) ---
35
-
36
- static bool is_digit(char c) { return c >= '0' && c <= '9'; }
37
- static bool is_alpha(char c) {
38
- return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
39
- }
40
- static bool is_alnum(char c) { return is_alpha(c) || is_digit(c); }
41
- static bool is_hex(char c) {
42
- return is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
43
- }
44
-
45
- static bool fast_check_email(std::string_view s) {
46
- auto at = s.find('@');
47
- if (at == std::string_view::npos || at == 0 || at == s.size() - 1)
48
- return false;
49
- auto dot = s.find('.', at + 1);
50
- if (dot == std::string_view::npos || dot == at + 1 ||
51
- dot == s.size() - 1)
52
- return false;
53
- // Check TLD has at least 2 chars
54
- return (s.size() - dot - 1) >= 2;
55
- }
56
-
57
- static bool fast_check_date(std::string_view s) {
58
- // YYYY-MM-DD with range validation
59
- if (s.size() != 10 || !is_digit(s[0]) || !is_digit(s[1]) ||
60
- !is_digit(s[2]) || !is_digit(s[3]) || s[4] != '-' ||
61
- !is_digit(s[5]) || !is_digit(s[6]) || s[7] != '-' ||
62
- !is_digit(s[8]) || !is_digit(s[9]))
63
- return false;
64
- int month = (s[5] - '0') * 10 + (s[6] - '0');
65
- int day = (s[8] - '0') * 10 + (s[9] - '0');
66
- return month >= 1 && month <= 12 && day >= 1 && day <= 31;
67
- }
68
-
69
- static bool fast_check_time(std::string_view s) {
70
- // HH:MM:SS[.frac][Z|+HH:MM]
71
- if (s.size() < 8) return false;
72
- if (!is_digit(s[0]) || !is_digit(s[1]) || s[2] != ':' ||
73
- !is_digit(s[3]) || !is_digit(s[4]) || s[5] != ':' ||
74
- !is_digit(s[6]) || !is_digit(s[7]))
75
- return false;
76
- return true;
77
- }
78
-
79
- static bool fast_check_datetime(std::string_view s) {
80
- if (s.size() < 19) return false;
81
- if (!fast_check_date(s.substr(0, 10))) return false;
82
- if (s[10] != 'T' && s[10] != 't' && s[10] != ' ') return false;
83
- return fast_check_time(s.substr(11));
84
- }
85
-
86
- static bool fast_check_ipv4(std::string_view s) {
87
- int parts = 0, val = 0, digits = 0;
88
- for (size_t i = 0; i <= s.size(); ++i) {
89
- if (i == s.size() || s[i] == '.') {
90
- if (digits == 0 || val > 255) return false;
91
- ++parts;
92
- val = 0;
93
- digits = 0;
94
- } else if (is_digit(s[i])) {
95
- val = val * 10 + (s[i] - '0');
96
- ++digits;
97
- if (digits > 3) return false;
98
- } else {
99
- return false;
100
- }
101
- }
102
- return parts == 4;
103
- }
104
-
105
- static bool fast_check_uri(std::string_view s) {
106
- if (s.size() < 3) return false;
107
- // Must start with alpha, then scheme chars, then ':'
108
- if (!is_alpha(s[0])) return false;
109
- size_t i = 1;
110
- while (i < s.size() && (is_alnum(s[i]) || s[i] == '+' || s[i] == '-' ||
111
- s[i] == '.'))
112
- ++i;
113
- return i < s.size() && s[i] == ':' && i + 1 < s.size();
114
- }
115
-
116
- static bool fast_check_uuid(std::string_view s) {
117
- // 8-4-4-4-12
118
- if (s.size() != 36) return false;
119
- for (size_t i = 0; i < 36; ++i) {
120
- if (i == 8 || i == 13 || i == 18 || i == 23) {
121
- if (s[i] != '-') return false;
122
- } else {
123
- if (!is_hex(s[i])) return false;
124
- }
125
- }
126
- return true;
127
- }
128
-
129
- static bool fast_check_hostname(std::string_view s) {
130
- if (s.empty() || s.size() > 253) return false;
131
- size_t label_len = 0;
132
- for (size_t i = 0; i < s.size(); ++i) {
133
- if (s[i] == '.') {
134
- if (label_len == 0) return false;
135
- label_len = 0;
136
- } else if (is_alnum(s[i]) || s[i] == '-') {
137
- ++label_len;
138
- if (label_len > 63) return false;
139
- } else {
140
- return false;
141
- }
142
- }
143
- return label_len > 0;
144
- }
145
-
146
- // Check format by pre-resolved numeric ID — no string comparisons.
147
- static bool check_format_by_id(std::string_view sv, uint8_t fid) {
148
- switch (fid) {
149
- case 0: return fast_check_email(sv);
150
- case 1: return fast_check_date(sv);
151
- case 2: return fast_check_datetime(sv);
152
- case 3: return fast_check_time(sv);
153
- case 4: return fast_check_ipv4(sv);
154
- case 5: return sv.find(':') != std::string_view::npos;
155
- case 6: return fast_check_uri(sv);
156
- case 7: return fast_check_uuid(sv);
157
- case 8: return fast_check_hostname(sv);
158
- default: return true; // unknown formats pass
159
- }
160
- }
161
-
162
- namespace ata {
163
-
164
- using namespace simdjson;
165
-
166
- // Canonical JSON: sort object keys for semantic equality comparison
167
- static std::string canonical_json(dom::element el) {
168
- switch (el.type()) {
169
- case dom::element_type::OBJECT: {
170
- dom::object obj; el.get(obj);
171
- std::vector<std::pair<std::string_view, dom::element>> entries;
172
- for (auto [k, v] : obj) entries.push_back({k, v});
173
- std::sort(entries.begin(), entries.end(),
174
- [](const auto& a, const auto& b) { return a.first < b.first; });
175
- std::string r = "{";
176
- for (size_t i = 0; i < entries.size(); ++i) {
177
- if (i) r += ',';
178
- r += '"';
179
- r += entries[i].first;
180
- r += "\":";
181
- r += canonical_json(entries[i].second);
182
- }
183
- r += '}';
184
- return r;
185
- }
186
- case dom::element_type::ARRAY: {
187
- dom::array arr; el.get(arr);
188
- std::string r = "[";
189
- bool first = true;
190
- for (auto v : arr) {
191
- if (!first) r += ',';
192
- first = false;
193
- r += canonical_json(v);
194
- }
195
- r += ']';
196
- return r;
197
- }
198
- default:
199
- return std::string(minify(el));
200
- }
201
- }
202
-
203
- // JSON Schema type enum — avoids string comparisons on the hot path.
204
- enum class json_type : uint8_t {
205
- string, number, integer, boolean, null_value, object, array
206
- };
207
-
208
- static json_type json_type_from_sv(std::string_view s) {
209
- if (s == "string") return json_type::string;
210
- if (s == "number") return json_type::number;
211
- if (s == "integer") return json_type::integer;
212
- if (s == "boolean") return json_type::boolean;
213
- if (s == "null") return json_type::null_value;
214
- if (s == "object") return json_type::object;
215
- if (s == "array") return json_type::array;
216
- return json_type::string; // fallback
217
- }
218
-
219
- static const char* json_type_name(json_type t) {
220
- switch (t) {
221
- case json_type::string: return "string";
222
- case json_type::number: return "number";
223
- case json_type::integer: return "integer";
224
- case json_type::boolean: return "boolean";
225
- case json_type::null_value: return "null";
226
- case json_type::object: return "object";
227
- case json_type::array: return "array";
228
- }
229
- return "unknown";
230
- }
231
-
232
- // Bitmask for O(1) type checking: one bit per json_type value.
233
- static uint8_t json_type_bit(json_type t) { return 1u << static_cast<uint8_t>(t); }
234
-
235
- // Map dom::element_type to a json_type bitmask (number matches integer too).
236
- static uint8_t element_type_mask(dom::element_type t) {
237
- switch (t) {
238
- case dom::element_type::STRING: return json_type_bit(json_type::string);
239
- case dom::element_type::INT64:
240
- case dom::element_type::UINT64: return json_type_bit(json_type::integer) | json_type_bit(json_type::number);
241
- case dom::element_type::DOUBLE: return json_type_bit(json_type::number);
242
- case dom::element_type::BOOL: return json_type_bit(json_type::boolean);
243
- case dom::element_type::NULL_VALUE: return json_type_bit(json_type::null_value);
244
- case dom::element_type::ARRAY: return json_type_bit(json_type::array);
245
- case dom::element_type::OBJECT: return json_type_bit(json_type::object);
246
- }
247
- return 0;
248
- }
249
-
250
- // Resolve format string to numeric ID at compile time.
251
- static uint8_t format_id_from_string(const std::string& f) {
252
- if (f == "email") return 0;
253
- if (f == "date") return 1;
254
- if (f == "date-time") return 2;
255
- if (f == "time") return 3;
256
- if (f == "ipv4") return 4;
257
- if (f == "ipv6") return 5;
258
- if (f == "uri" || f == "uri-reference") return 6;
259
- if (f == "uuid") return 7;
260
- if (f == "hostname") return 8;
261
- return 255;
262
- }
263
-
264
- // Forward declarations
265
- struct schema_node;
266
- using schema_node_ptr = std::shared_ptr<schema_node>;
267
-
268
- struct schema_node {
269
- // type constraint — bitmask for O(1) type checking
270
- uint8_t type_mask = 0; // bit per json_type value
271
-
272
- // numeric
273
- std::optional<double> minimum;
274
- std::optional<double> maximum;
275
- std::optional<double> exclusive_minimum;
276
- std::optional<double> exclusive_maximum;
277
- std::optional<double> multiple_of;
278
-
279
- // string
280
- std::optional<uint64_t> min_length;
281
- std::optional<uint64_t> max_length;
282
- std::optional<std::string> pattern;
283
- #ifndef ATA_NO_RE2
284
- std::shared_ptr<re2::RE2> compiled_pattern; // cached compiled regex (RE2)
285
- #endif
286
-
287
- // array
288
- std::optional<uint64_t> min_items;
289
- std::optional<uint64_t> max_items;
290
- bool unique_items = false;
291
- schema_node_ptr items_schema;
292
- std::vector<schema_node_ptr> prefix_items;
293
- schema_node_ptr contains_schema;
294
- std::optional<uint64_t> min_contains;
295
- std::optional<uint64_t> max_contains;
296
-
297
- // object
298
- std::unordered_map<std::string, schema_node_ptr> properties;
299
- std::vector<std::string> required;
300
- std::optional<bool> additional_properties_bool;
301
- schema_node_ptr additional_properties_schema;
302
- std::optional<uint64_t> min_properties;
303
- std::optional<uint64_t> max_properties;
304
- schema_node_ptr property_names_schema;
305
- std::unordered_map<std::string, std::vector<std::string>> dependent_required;
306
- std::unordered_map<std::string, schema_node_ptr> dependent_schemas;
307
-
308
- // patternProperties — each entry: (pattern_string, schema, compiled_regex)
309
- struct pattern_prop {
310
- std::string pattern;
311
- schema_node_ptr schema;
312
- #ifndef ATA_NO_RE2
313
- std::shared_ptr<re2::RE2> compiled;
314
- #endif
315
- };
316
- std::vector<pattern_prop> pattern_properties;
317
-
318
- // enum / const
319
- std::vector<std::string> enum_values_minified; // pre-minified enum values
320
- std::optional<std::string> const_value_raw; // raw JSON value string
321
-
322
- // format
323
- std::optional<std::string> format;
324
- uint8_t format_id = 255; // pre-resolved format ID (255 = unknown/pass)
325
-
326
- // composition
327
- std::vector<schema_node_ptr> all_of;
328
- std::vector<schema_node_ptr> any_of;
329
- std::vector<schema_node_ptr> one_of;
330
- schema_node_ptr not_schema;
331
-
332
- // conditional
333
- schema_node_ptr if_schema;
334
- schema_node_ptr then_schema;
335
- schema_node_ptr else_schema;
336
-
337
- // $ref
338
- std::string ref;
339
- std::string dynamic_ref; // $dynamicRef value (e.g. "#items")
340
- std::string id; // $id — resource boundary marker
341
-
342
- // $defs — stored on node for pointer navigation
343
- std::unordered_map<std::string, schema_node_ptr> defs;
344
-
345
- // boolean schema
346
- std::optional<bool> boolean_schema;
347
- };
348
-
349
- // --- Codegen: flat bytecode plan ---
350
- namespace cg {
351
- enum class op : uint8_t {
352
- END=0, EXPECT_OBJECT, EXPECT_ARRAY, EXPECT_STRING, EXPECT_NUMBER,
353
- EXPECT_INTEGER, EXPECT_BOOLEAN, EXPECT_NULL, EXPECT_TYPE_MULTI,
354
- CHECK_MINIMUM, CHECK_MAXIMUM, CHECK_EX_MINIMUM, CHECK_EX_MAXIMUM,
355
- CHECK_MULTIPLE_OF, CHECK_MIN_LENGTH, CHECK_MAX_LENGTH, CHECK_PATTERN,
356
- CHECK_FORMAT, CHECK_MIN_ITEMS, CHECK_MAX_ITEMS, CHECK_UNIQUE_ITEMS,
357
- ARRAY_ITEMS, CHECK_REQUIRED, CHECK_MIN_PROPS, CHECK_MAX_PROPS,
358
- OBJ_PROPS_START, OBJ_PROP, OBJ_PROPS_END, CHECK_NO_ADDITIONAL,
359
- CHECK_ENUM_STR, CHECK_ENUM, CHECK_CONST, COMPOSITION,
360
- };
361
- struct ins { op o; uint32_t a=0, b=0; };
362
- struct plan {
363
- std::vector<ins> code;
364
- std::vector<double> doubles;
365
- std::vector<std::string> strings;
366
- #ifndef ATA_NO_RE2
367
- std::vector<std::shared_ptr<re2::RE2>> regexes;
368
- #endif
369
- std::vector<std::vector<std::string>> enum_sets;
370
- std::vector<uint8_t> type_masks;
371
- std::vector<uint8_t> format_ids;
372
- std::vector<std::vector<ins>> subs;
373
- };
374
- } // namespace cg
375
-
376
- // --- On-Demand validation plan ---
377
- // Grouped checks per value type. Each value consumed exactly once.
378
- // Built from schema_node at compile time, used by od_exec_plan at runtime.
379
- struct od_plan {
380
- uint8_t type_mask = 0;
381
-
382
- // Numeric — bitmask for which checks to run + flat array of bounds
383
- enum num_flag : uint8_t {
384
- HAS_MIN = 1, HAS_MAX = 2, HAS_EX_MIN = 4, HAS_EX_MAX = 8, HAS_MUL = 16
385
- };
386
- uint8_t num_flags = 0;
387
- double num_min = 0, num_max = 0, num_ex_min = 0, num_ex_max = 0, num_mul = 0;
388
-
389
- // String — single value.get(sv) then all checks
390
- std::optional<uint64_t> min_length, max_length;
391
- #ifndef ATA_NO_RE2
392
- re2::RE2* pattern = nullptr; // borrowed pointer from schema_node
393
- #endif
394
- // Inline digit pattern: ^[0-9]+$ / ^[0-9]{N}$ / ^[0-9]{N,M}$ / ^\d+$ etc.
395
- // When set, supersedes the RE2 pattern call.
396
- struct digit_pattern_t { uint32_t min_len; uint32_t max_len; };
397
- std::optional<digit_pattern_t> digit_pattern;
398
- uint8_t format_id = 255; // 255 = no format check
399
-
400
- // Object — single iterate with merged required+property lookup
401
- // fast_kind allows the obj iterator to skip the recursive od_exec_plan call
402
- // (and its inner type().get() + switch) for primitive sub-plans, dispatching
403
- // straight to inline constraint code.
404
- enum class fast_kind : uint8_t {
405
- OTHER = 0, // recurse via od_exec_plan
406
- INTEGER, // value.get(int64) + numeric range/multipleOf
407
- STRING, // value.get(string) + length/pattern/format/enum
408
- BOOLEAN, // value.get(bool)
409
- OBJECT, // value.get(object) + nested obj iteration (skips type detect)
410
- ARRAY, // value.get(array) + element iteration (skips type detect)
411
- };
412
- struct prop_entry {
413
- std::string key;
414
- int required_idx = -1; // bit index for required tracking, or -1
415
- std::shared_ptr<od_plan> sub; // property sub-plan, or nullptr
416
- fast_kind fk = fast_kind::OTHER; // inline dispatch hint, set at compile time
417
- };
418
- struct obj_plan {
419
- std::vector<prop_entry> entries; // merged required + properties — single scan
420
- size_t required_count = 0;
421
- bool no_additional = false;
422
- std::optional<uint64_t> min_props, max_props;
423
- };
424
- std::shared_ptr<obj_plan> object;
425
-
426
- // Array — single iterate: items + count
427
- struct arr_plan {
428
- std::shared_ptr<od_plan> items;
429
- std::optional<uint64_t> min_items, max_items;
430
- };
431
- std::shared_ptr<arr_plan> array;
432
-
433
- // Enum: primitive-only set membership. Complex (object/array) enums fall back.
434
- struct enum_constraint {
435
- std::vector<std::string> strings; // unescaped string values
436
- std::vector<int64_t> integers;
437
- std::vector<double> doubles;
438
- bool has_null = false;
439
- bool has_true = false;
440
- bool has_false = false;
441
- };
442
- std::shared_ptr<enum_constraint> enum_check;
443
-
444
- // If false, schema uses unsupported features — must fall back to DOM path.
445
- bool supported = true;
446
- };
447
-
448
- using od_plan_ptr = std::shared_ptr<od_plan>;
449
-
450
- struct compiled_schema {
451
- schema_node_ptr root;
452
- std::unordered_map<std::string, schema_node_ptr> defs;
453
- std::string raw_schema;
454
- std::string compile_error; // non-empty if compilation failed
455
- dom::parser parser; // used only at compile time
456
- cg::plan gen_plan; // codegen validation plan
457
- bool use_ondemand = false; // true if codegen plan supports On Demand
458
- od_plan_ptr od; // On-Demand execution plan
459
-
460
- // anchor resolution
461
- std::unordered_map<std::string, schema_node_ptr> anchors;
462
- std::unordered_map<std::string,
463
- std::unordered_map<std::string, schema_node_ptr>> resource_dynamic_anchors;
464
- bool has_dynamic_refs = false;
465
- std::string current_resource_id; // compile-time only
466
-
467
- // compile-time warnings (misplaced keywords, etc.)
468
- std::vector<schema_warning> warnings;
469
- std::string compile_path; // current JSON pointer during compilation
470
- };
471
-
472
- // Thread-local persistent parsers — reused across all validate calls on the
473
- // same thread. Keeps internal buffers hot in cache and avoids re-allocation.
474
- static dom::parser& tl_dom_parser() {
475
- thread_local dom::parser p;
476
- return p;
477
- }
478
- static dom::parser& tl_dom_key_parser() {
479
- thread_local dom::parser p;
480
- return p;
481
- }
482
- static simdjson::ondemand::parser& tl_od_parser() {
483
- thread_local simdjson::ondemand::parser p;
484
- return p;
485
- };
486
-
487
- // --- Schema compilation ---
488
-
489
- static schema_node_ptr compile_node(dom::element el,
490
- compiled_schema& ctx);
491
-
492
- static schema_node_ptr compile_node(dom::element el,
493
- compiled_schema& ctx) {
494
- auto node = std::make_shared<schema_node>();
495
-
496
- // Boolean schema
497
- if (el.is<bool>()) {
498
- bool bval;
499
- el.get(bval);
500
- node->boolean_schema = bval;
501
- return node;
502
- }
503
-
504
- if (!el.is<dom::object>()) {
505
- return node;
506
- }
507
-
508
- dom::object obj;
509
- el.get(obj);
510
-
511
- // $ref
512
- dom::element ref_el;
513
- if (obj["$ref"].get(ref_el) == SUCCESS) {
514
- std::string_view ref_sv;
515
- if (ref_el.get(ref_sv) == SUCCESS) {
516
- node->ref = std::string(ref_sv);
517
- }
518
- }
519
-
520
- // $id — must come before $anchor/$dynamicAnchor so current_resource_id is set
521
- std::string prev_resource = ctx.current_resource_id;
522
- {
523
- dom::element id_el;
524
- if (obj["$id"].get(id_el) == SUCCESS) {
525
- std::string_view sv;
526
- if (id_el.get(sv) == SUCCESS) {
527
- node->id = std::string(sv);
528
- ctx.current_resource_id = node->id;
529
- ctx.defs[node->id] = node;
530
- }
531
- }
532
- }
533
-
534
- // $anchor — register in flat anchor map
535
- {
536
- dom::element anchor_el;
537
- if (obj["$anchor"].get(anchor_el) == SUCCESS) {
538
- std::string_view sv;
539
- if (anchor_el.get(sv) == SUCCESS) {
540
- ctx.anchors[std::string(sv)] = node;
541
- }
542
- }
543
- }
544
-
545
- // $dynamicAnchor — register in both flat anchors and per-resource map
546
- {
547
- dom::element da_el;
548
- if (obj["$dynamicAnchor"].get(da_el) == SUCCESS) {
549
- std::string_view sv;
550
- if (da_el.get(sv) == SUCCESS) {
551
- std::string name(sv);
552
- ctx.anchors[name] = node;
553
- ctx.resource_dynamic_anchors[ctx.current_resource_id][name] = node;
554
- }
555
- }
556
- }
557
-
558
- // $dynamicRef
559
- {
560
- dom::element dr_el;
561
- if (obj["$dynamicRef"].get(dr_el) == SUCCESS) {
562
- std::string_view sv;
563
- if (dr_el.get(sv) == SUCCESS) {
564
- std::string dr_val(sv);
565
- // If the $dynamicRef starts with "#" (fragment-only) and we're inside
566
- // a non-root resource, qualify it with the current resource ID so
567
- // validation can resolve it correctly.
568
- if (!dr_val.empty() && dr_val[0] == '#' &&
569
- !ctx.current_resource_id.empty()) {
570
- dr_val = ctx.current_resource_id + dr_val;
571
- }
572
- node->dynamic_ref = dr_val;
573
- ctx.has_dynamic_refs = true;
574
- }
575
- }
576
- }
577
-
578
- // type
579
- dom::element type_el;
580
- if (obj["type"].get(type_el) == SUCCESS) {
581
- if (type_el.is<std::string_view>()) {
582
- std::string_view sv;
583
- type_el.get(sv);
584
- node->type_mask |= json_type_bit(json_type_from_sv(sv));
585
- } else if (type_el.is<dom::array>()) {
586
- dom::array type_arr; type_el.get(type_arr); for (auto t : type_arr) {
587
- std::string_view sv;
588
- if (t.get(sv) == SUCCESS) {
589
- node->type_mask |= json_type_bit(json_type_from_sv(sv));
590
- }
591
- }
592
- }
593
- }
594
-
595
- // numeric constraints
596
- dom::element num_el;
597
- if (obj["minimum"].get(num_el) == SUCCESS) {
598
- double v;
599
- if (num_el.get(v) == SUCCESS) node->minimum = v;
600
- }
601
- if (obj["maximum"].get(num_el) == SUCCESS) {
602
- double v;
603
- if (num_el.get(v) == SUCCESS) node->maximum = v;
604
- }
605
- if (obj["exclusiveMinimum"].get(num_el) == SUCCESS) {
606
- double v;
607
- if (num_el.get(v) == SUCCESS) node->exclusive_minimum = v;
608
- }
609
- if (obj["exclusiveMaximum"].get(num_el) == SUCCESS) {
610
- double v;
611
- if (num_el.get(v) == SUCCESS) node->exclusive_maximum = v;
612
- }
613
- if (obj["multipleOf"].get(num_el) == SUCCESS) {
614
- double v;
615
- if (num_el.get(v) == SUCCESS) node->multiple_of = v;
616
- }
617
-
618
- // string constraints
619
- dom::element str_el;
620
- if (obj["minLength"].get(str_el) == SUCCESS) {
621
- uint64_t v;
622
- if (str_el.get(v) == SUCCESS) node->min_length = v;
623
- }
624
- if (obj["maxLength"].get(str_el) == SUCCESS) {
625
- uint64_t v;
626
- if (str_el.get(v) == SUCCESS) node->max_length = v;
627
- }
628
- if (obj["pattern"].get(str_el) == SUCCESS) {
629
- std::string_view sv;
630
- if (str_el.get(sv) == SUCCESS) {
631
- node->pattern = std::string(sv);
632
- #ifdef ATA_NO_RE2
633
- ctx.compile_error = "pattern keyword requires RE2 support (built with ATA_NO_RE2)";
634
- return node;
635
- #else
636
- auto re = std::make_shared<re2::RE2>(node->pattern.value());
637
- if (re->ok()) {
638
- node->compiled_pattern = std::move(re);
639
- }
640
- #endif
641
- }
642
- }
643
-
644
- // array constraints
645
- if (obj["minItems"].get(str_el) == SUCCESS) {
646
- uint64_t v;
647
- if (str_el.get(v) == SUCCESS) node->min_items = v;
648
- }
649
- if (obj["maxItems"].get(str_el) == SUCCESS) {
650
- uint64_t v;
651
- if (str_el.get(v) == SUCCESS) node->max_items = v;
652
- }
653
- dom::element ui_el;
654
- if (obj["uniqueItems"].get(ui_el) == SUCCESS) {
655
- bool v;
656
- if (ui_el.get(v) == SUCCESS) node->unique_items = v;
657
- }
658
- // prefixItems (Draft 2020-12)
659
- dom::element pi_el;
660
- if (obj["prefixItems"].get(pi_el) == SUCCESS && pi_el.is<dom::array>()) {
661
- dom::array pi_arr; pi_el.get(pi_arr); for (auto item : pi_arr) {
662
- node->prefix_items.push_back(compile_node(item, ctx));
663
- }
664
- }
665
-
666
- dom::element items_el;
667
- if (obj["items"].get(items_el) == SUCCESS) {
668
- node->items_schema = compile_node(items_el, ctx);
669
- }
670
-
671
- // contains
672
- dom::element contains_el;
673
- if (obj["contains"].get(contains_el) == SUCCESS) {
674
- node->contains_schema = compile_node(contains_el, ctx);
675
- }
676
- dom::element mc_el;
677
- if (obj["minContains"].get(mc_el) == SUCCESS) {
678
- uint64_t v;
679
- if (mc_el.get(v) == SUCCESS) node->min_contains = v;
680
- }
681
- if (obj["maxContains"].get(mc_el) == SUCCESS) {
682
- uint64_t v;
683
- if (mc_el.get(v) == SUCCESS) node->max_contains = v;
684
- }
685
-
686
- // object constraints
687
- dom::element props_el;
688
- if (obj["properties"].get(props_el) == SUCCESS && props_el.is<dom::object>()) {
689
- dom::object props_obj; props_el.get(props_obj); for (auto [key, val] : props_obj) {
690
- node->properties[std::string(key)] = compile_node(val, ctx);
691
- }
692
- }
693
-
694
- dom::element req_el;
695
- if (obj["required"].get(req_el) == SUCCESS && req_el.is<dom::array>()) {
696
- dom::array req_arr; req_el.get(req_arr); for (auto r : req_arr) {
697
- std::string_view sv;
698
- if (r.get(sv) == SUCCESS) {
699
- node->required.emplace_back(sv);
700
- }
701
- }
702
- }
703
-
704
- dom::element ap_el;
705
- if (obj["additionalProperties"].get(ap_el) == SUCCESS) {
706
- if (ap_el.is<bool>()) {
707
- bool ap_bool; ap_el.get(ap_bool); node->additional_properties_bool = ap_bool;
708
- } else {
709
- node->additional_properties_schema = compile_node(ap_el, ctx);
710
- }
711
- }
712
-
713
- if (obj["minProperties"].get(str_el) == SUCCESS) {
714
- uint64_t v;
715
- if (str_el.get(v) == SUCCESS) node->min_properties = v;
716
- }
717
- if (obj["maxProperties"].get(str_el) == SUCCESS) {
718
- uint64_t v;
719
- if (str_el.get(v) == SUCCESS) node->max_properties = v;
720
- }
721
-
722
- // propertyNames
723
- dom::element pn_el;
724
- if (obj["propertyNames"].get(pn_el) == SUCCESS) {
725
- node->property_names_schema = compile_node(pn_el, ctx);
726
- }
727
-
728
- // dependentRequired
729
- dom::element dr_el;
730
- if (obj["dependentRequired"].get(dr_el) == SUCCESS &&
731
- dr_el.is<dom::object>()) {
732
- dom::object dr_obj; dr_el.get(dr_obj); for (auto [key, val] : dr_obj) {
733
- std::vector<std::string> deps;
734
- if (val.is<dom::array>()) {
735
- dom::array val_arr; val.get(val_arr); for (auto d : val_arr) {
736
- std::string_view sv;
737
- if (d.get(sv) == SUCCESS) deps.emplace_back(sv);
738
- }
739
- }
740
- node->dependent_required[std::string(key)] = std::move(deps);
741
- }
742
- }
743
-
744
- // dependentSchemas
745
- dom::element ds_el;
746
- if (obj["dependentSchemas"].get(ds_el) == SUCCESS &&
747
- ds_el.is<dom::object>()) {
748
- dom::object ds_obj; ds_el.get(ds_obj); for (auto [key, val] : ds_obj) {
749
- node->dependent_schemas[std::string(key)] = compile_node(val, ctx);
750
- }
751
- }
752
-
753
- // patternProperties — compile regex at schema compile time
754
- dom::element pp_el;
755
- if (obj["patternProperties"].get(pp_el) == SUCCESS &&
756
- pp_el.is<dom::object>()) {
757
- #ifdef ATA_NO_RE2
758
- ctx.compile_error = "patternProperties keyword requires RE2 support (built with ATA_NO_RE2)";
759
- return node;
760
- #else
761
- dom::object pp_obj; pp_el.get(pp_obj);
762
- for (auto [key, val] : pp_obj) {
763
- schema_node::pattern_prop pp;
764
- pp.pattern = std::string(key);
765
- pp.schema = compile_node(val, ctx);
766
- auto re = std::make_shared<re2::RE2>(pp.pattern);
767
- if (re->ok()) {
768
- pp.compiled = std::move(re);
769
- }
770
- node->pattern_properties.push_back(std::move(pp));
771
- }
772
- #endif
773
- }
774
-
775
- // format
776
- dom::element fmt_el;
777
- if (obj["format"].get(fmt_el) == SUCCESS) {
778
- std::string_view sv;
779
- if (fmt_el.get(sv) == SUCCESS) {
780
- node->format = std::string(sv);
781
- node->format_id = format_id_from_string(node->format.value());
782
- }
783
- }
784
-
785
- // enum — pre-minify each value at compile time
786
- dom::element enum_el;
787
- if (obj["enum"].get(enum_el) == SUCCESS) {
788
- if (enum_el.is<dom::array>()) {
789
- dom::array enum_arr; enum_el.get(enum_arr); for (auto e : enum_arr) {
790
- node->enum_values_minified.push_back(canonical_json(e));
791
- }
792
- }
793
- }
794
-
795
- // const
796
- dom::element const_el;
797
- if (obj["const"].get(const_el) == SUCCESS) {
798
- node->const_value_raw = canonical_json(const_el);
799
- }
800
-
801
- // composition
802
- dom::element comp_el;
803
- if (obj["allOf"].get(comp_el) == SUCCESS && comp_el.is<dom::array>()) {
804
- dom::array comp_arr; comp_el.get(comp_arr);
805
- for (auto s : comp_arr) {
806
- node->all_of.push_back(compile_node(s, ctx));
807
- }
808
- }
809
- if (obj["anyOf"].get(comp_el) == SUCCESS && comp_el.is<dom::array>()) {
810
- dom::array comp_arr2; comp_el.get(comp_arr2);
811
- for (auto s : comp_arr2) {
812
- node->any_of.push_back(compile_node(s, ctx));
813
- }
814
- }
815
- if (obj["oneOf"].get(comp_el) == SUCCESS && comp_el.is<dom::array>()) {
816
- dom::array comp_arr3; comp_el.get(comp_arr3);
817
- for (auto s : comp_arr3) {
818
- node->one_of.push_back(compile_node(s, ctx));
819
- }
820
- }
821
- dom::element not_el;
822
- if (obj["not"].get(not_el) == SUCCESS) {
823
- node->not_schema = compile_node(not_el, ctx);
824
- }
825
-
826
- // conditional
827
- dom::element if_el;
828
- if (obj["if"].get(if_el) == SUCCESS) {
829
- node->if_schema = compile_node(if_el, ctx);
830
- }
831
- dom::element then_el;
832
- if (obj["then"].get(then_el) == SUCCESS) {
833
- node->then_schema = compile_node(then_el, ctx);
834
- }
835
- dom::element else_el;
836
- if (obj["else"].get(else_el) == SUCCESS) {
837
- node->else_schema = compile_node(else_el, ctx);
838
- }
839
-
840
- // $defs / definitions
841
- dom::element defs_el;
842
- if (obj["$defs"].get(defs_el) == SUCCESS && defs_el.is<dom::object>()) {
843
- dom::object defs_obj; defs_el.get(defs_obj); for (auto [key, val] : defs_obj) {
844
- std::string def_path = "#/$defs/" + std::string(key);
845
- auto compiled = compile_node(val, ctx);
846
- ctx.defs[def_path] = compiled;
847
- node->defs[std::string(key)] = compiled;
848
- }
849
- }
850
- if (obj["definitions"].get(defs_el) == SUCCESS &&
851
- defs_el.is<dom::object>()) {
852
- dom::object defs_obj; defs_el.get(defs_obj); for (auto [key, val] : defs_obj) {
853
- std::string def_path = "#/definitions/" + std::string(key);
854
- auto compiled = compile_node(val, ctx);
855
- ctx.defs[def_path] = compiled;
856
- node->defs[std::string(key)] = compiled;
857
- }
858
- }
859
-
860
- // Warn about keywords used at the wrong type level.
861
- // Only check when an explicit "type" is declared (type_mask != 0).
862
- if (node->type_mask != 0) {
863
- const uint8_t array_bit = json_type_bit(json_type::array);
864
- const uint8_t string_bit = json_type_bit(json_type::string);
865
- const uint8_t number_bits = json_type_bit(json_type::number) |
866
- json_type_bit(json_type::integer);
867
- const uint8_t object_bit = json_type_bit(json_type::object);
868
-
869
- auto warn = [&](const char* keyword, const char* expected_type) {
870
- ctx.warnings.push_back({
871
- ctx.compile_path,
872
- std::string(keyword) + " has no effect on type \"" +
873
- (node->type_mask & json_type_bit(json_type::string) ? "string" :
874
- node->type_mask & json_type_bit(json_type::boolean) ? "boolean" :
875
- node->type_mask & json_type_bit(json_type::number) ? "number" :
876
- node->type_mask & object_bit ? "object" :
877
- node->type_mask & array_bit ? "array" : "unknown") +
878
- "\", only applies to " + expected_type
879
- });
880
- };
881
-
882
- // Array keywords on non-array type
883
- if (!(node->type_mask & array_bit)) {
884
- if (node->min_items.has_value()) warn("minItems", "array");
885
- if (node->max_items.has_value()) warn("maxItems", "array");
886
- if (node->unique_items) warn("uniqueItems", "array");
887
- if (!node->prefix_items.empty()) warn("prefixItems", "array");
888
- if (node->items_schema) warn("items", "array");
889
- if (node->contains_schema) warn("contains", "array");
890
- }
891
-
892
- // String keywords on non-string type
893
- if (!(node->type_mask & string_bit)) {
894
- if (node->min_length.has_value()) warn("minLength", "string");
895
- if (node->max_length.has_value()) warn("maxLength", "string");
896
- if (node->pattern.has_value()) warn("pattern", "string");
897
- }
898
-
899
- // Numeric keywords on non-numeric type
900
- if (!(node->type_mask & number_bits)) {
901
- if (node->minimum.has_value()) warn("minimum", "number");
902
- if (node->maximum.has_value()) warn("maximum", "number");
903
- if (node->exclusive_minimum.has_value()) warn("exclusiveMinimum", "number");
904
- if (node->exclusive_maximum.has_value()) warn("exclusiveMaximum", "number");
905
- if (node->multiple_of.has_value()) warn("multipleOf", "number");
906
- }
907
-
908
- // Object keywords on non-object type
909
- if (!(node->type_mask & object_bit)) {
910
- if (!node->properties.empty()) warn("properties", "object");
911
- if (!node->required.empty()) warn("required", "object");
912
- }
913
- }
914
-
915
- ctx.current_resource_id = prev_resource;
916
- return node;
917
- }
918
-
919
- // --- Validation ---
920
-
921
- using dynamic_scope_t = std::vector<const std::unordered_map<std::string, schema_node_ptr>*>;
922
-
923
- // Decode a single JSON Pointer segment (percent-decode, then ~1->/, ~0->~)
924
- static std::string decode_pointer_segment(const std::string& seg) {
925
- std::string pct;
926
- for (size_t i = 0; i < seg.size(); ++i) {
927
- if (seg[i] == '%' && i + 2 < seg.size()) {
928
- auto hex = [](char c) -> int {
929
- if (c >= '0' && c <= '9') return c - '0';
930
- if (c >= 'a' && c <= 'f') return 10 + c - 'a';
931
- if (c >= 'A' && c <= 'F') return 10 + c - 'A';
932
- return -1;
933
- };
934
- int hv = hex(seg[i+1]), lv = hex(seg[i+2]);
935
- if (hv >= 0 && lv >= 0) {
936
- pct += static_cast<char>(hv * 16 + lv);
937
- i += 2;
938
- } else {
939
- pct += seg[i];
940
- }
941
- } else {
942
- pct += seg[i];
943
- }
944
- }
945
- std::string out;
946
- for (size_t i = 0; i < pct.size(); ++i) {
947
- if (pct[i] == '~' && i + 1 < pct.size()) {
948
- if (pct[i + 1] == '1') { out += '/'; ++i; }
949
- else if (pct[i + 1] == '0') { out += '~'; ++i; }
950
- else out += pct[i];
951
- } else {
952
- out += pct[i];
953
- }
954
- }
955
- return out;
956
- }
957
-
958
- // Walk a JSON Pointer (without leading #) within a given schema node.
959
- // Returns the resolved node, or nullptr if not found.
960
- static schema_node_ptr walk_json_pointer(const schema_node_ptr& root_node,
961
- const std::string& pointer) {
962
- if (pointer.empty()) return root_node;
963
-
964
- std::vector<std::string> segments;
965
- size_t spos = 0;
966
- // pointer starts with "/" — skip leading slash
967
- if (!pointer.empty() && pointer[0] == '/') spos = 1;
968
- while (spos <= pointer.size()) {
969
- size_t snext = pointer.find('/', spos);
970
- segments.push_back(decode_pointer_segment(
971
- pointer.substr(spos, snext == std::string::npos ? snext : snext - spos)));
972
- spos = (snext == std::string::npos) ? pointer.size() + 1 : snext + 1;
973
- }
974
-
975
- schema_node_ptr current = root_node;
976
- for (size_t si = 0; si < segments.size() && current; ++si) {
977
- const auto& key = segments[si];
978
- if (key == "properties" && si + 1 < segments.size()) {
979
- const auto& prop_name = segments[++si];
980
- auto pit = current->properties.find(prop_name);
981
- if (pit != current->properties.end()) { current = pit->second; }
982
- else { return nullptr; }
983
- } else if (key == "items" && current->items_schema) {
984
- current = current->items_schema;
985
- } else if (key == "$defs" || key == "definitions") {
986
- if (si + 1 < segments.size()) {
987
- const auto& def_name = segments[++si];
988
- auto dit = current->defs.find(def_name);
989
- if (dit != current->defs.end()) { current = dit->second; }
990
- else { return nullptr; }
991
- } else { return nullptr; }
992
- } else if (key == "allOf" || key == "anyOf" || key == "oneOf") {
993
- if (si + 1 < segments.size()) {
994
- size_t idx = std::stoul(segments[++si]);
995
- auto& vec = (key == "allOf") ? current->all_of
996
- : (key == "anyOf") ? current->any_of
997
- : current->one_of;
998
- if (idx < vec.size()) { current = vec[idx]; }
999
- else { return nullptr; }
1000
- } else { return nullptr; }
1001
- } else if (key == "not" && current->not_schema) {
1002
- current = current->not_schema;
1003
- } else if (key == "if" && current->if_schema) {
1004
- current = current->if_schema;
1005
- } else if (key == "then" && current->then_schema) {
1006
- current = current->then_schema;
1007
- } else if (key == "else" && current->else_schema) {
1008
- current = current->else_schema;
1009
- } else if (key == "additionalProperties" &&
1010
- current->additional_properties_schema) {
1011
- current = current->additional_properties_schema;
1012
- } else if (key == "prefixItems") {
1013
- if (si + 1 < segments.size()) {
1014
- size_t idx = std::stoul(segments[++si]);
1015
- if (idx < current->prefix_items.size()) { current = current->prefix_items[idx]; }
1016
- else { return nullptr; }
1017
- } else { return nullptr; }
1018
- } else if (key == "contains" && current->contains_schema) {
1019
- current = current->contains_schema;
1020
- } else if (key == "propertyNames" && current->property_names_schema) {
1021
- current = current->property_names_schema;
1022
- } else {
1023
- return nullptr;
1024
- }
1025
- }
1026
- return current;
1027
- }
1028
-
1029
- // Find an anchor (non-pointer fragment) within a specific resource node by
1030
- // searching its sub-tree. Used for resolving "base#anchor" references.
1031
- static schema_node_ptr find_anchor_in_resource(const compiled_schema& ctx,
1032
- const std::string& resource_id,
1033
- const std::string& anchor_name) {
1034
- // Look up in per-resource dynamic anchors first
1035
- auto rit = ctx.resource_dynamic_anchors.find(resource_id);
1036
- if (rit != ctx.resource_dynamic_anchors.end()) {
1037
- auto ait = rit->second.find(anchor_name);
1038
- if (ait != rit->second.end()) return ait->second;
1039
- }
1040
- // Fallback to flat anchors (which includes $anchor entries)
1041
- auto ait = ctx.anchors.find(anchor_name);
1042
- if (ait != ctx.anchors.end()) return ait->second;
1043
- return nullptr;
1044
- }
1045
-
1046
- static void validate_node(const schema_node_ptr& node,
1047
- dom::element value,
1048
- const std::string& path,
1049
- const compiled_schema& ctx,
1050
- std::vector<validation_error>& errors,
1051
- bool all_errors = true,
1052
- dynamic_scope_t* dynamic_scope = nullptr);
1053
-
1054
- // Fast boolean-only tree walker — no error collection, no string allocation.
1055
- // Uses [[likely]]/[[unlikely]] hints. Returns true if valid.
1056
- static bool validate_fast(const schema_node_ptr& node,
1057
- dom::element value,
1058
- const compiled_schema& ctx);
1059
-
1060
- // Macro for early termination
1061
- #define ATA_CHECK_EARLY() if (!all_errors && !errors.empty()) return
1062
-
1063
- using et = dom::element_type;
1064
-
1065
-
1066
- // Use string_view to avoid allocations in hot path
1067
- static std::string_view type_of_sv(dom::element el) {
1068
- switch (el.type()) {
1069
- case et::STRING: return "string";
1070
- case et::INT64:
1071
- case et::UINT64: return "integer";
1072
- case et::DOUBLE: return "number";
1073
- case et::BOOL: return "boolean";
1074
- case et::NULL_VALUE:return "null";
1075
- case et::ARRAY: return "array";
1076
- case et::OBJECT: return "object";
1077
- }
1078
- return "unknown";
1079
- }
1080
-
1081
-
1082
- // O(1) type check: test element's type bits against the schema's type_mask.
1083
- static bool type_matches_mask(dom::element el, uint8_t type_mask) {
1084
- return (element_type_mask(el.type()) & type_mask) != 0;
1085
- }
1086
-
1087
- static double to_double(dom::element el) {
1088
- switch (el.type()) {
1089
- case et::DOUBLE: { double v; el.get(v); return v; }
1090
- case et::INT64: { int64_t v; el.get(v); return static_cast<double>(v); }
1091
- case et::UINT64: { uint64_t v; el.get(v); return static_cast<double>(v); }
1092
- default: return 0;
1093
- }
1094
- }
1095
-
1096
- // Count UTF-8 codepoints — branchless: count non-continuation bytes
1097
- static uint64_t utf8_length(std::string_view s) {
1098
- uint64_t count = 0;
1099
- for (size_t i = 0; i < s.size(); ++i) {
1100
- // Continuation bytes are 10xxxxxx (0x80-0xBF)
1101
- // Non-continuation bytes start codepoints
1102
- count += ((static_cast<unsigned char>(s[i]) & 0xC0) != 0x80);
1103
- }
1104
- return count;
1105
- }
1106
-
1107
- // Recursion depth guard — prevents stack overflow on self-referencing schemas
1108
- struct DepthGuard {
1109
- static thread_local int depth;
1110
- bool overflow;
1111
- DepthGuard() : overflow(++depth > 100) {}
1112
- ~DepthGuard() { --depth; }
1113
- };
1114
- thread_local int DepthGuard::depth = 0;
1115
-
1116
- static void validate_node(const schema_node_ptr& node,
1117
- dom::element value,
1118
- const std::string& path,
1119
- const compiled_schema& ctx,
1120
- std::vector<validation_error>& errors,
1121
- bool all_errors,
1122
- dynamic_scope_t* dynamic_scope) {
1123
- if (!node) return;
1124
-
1125
- DepthGuard guard;
1126
- if (guard.overflow) return;
1127
-
1128
- // Boolean schema
1129
- if (node->boolean_schema.has_value()) {
1130
- if (!node->boolean_schema.value()) {
1131
- errors.push_back({error_code::type_mismatch, path,
1132
- "schema is false, no value is valid"});
1133
- }
1134
- return;
1135
- }
1136
-
1137
- // Dynamic scope tracking: push this resource's dynamic anchors
1138
- bool pushed_scope = false;
1139
- if (dynamic_scope && !node->id.empty()) {
1140
- auto it = ctx.resource_dynamic_anchors.find(node->id);
1141
- if (it != ctx.resource_dynamic_anchors.end()) {
1142
- dynamic_scope->push_back(&it->second);
1143
- pushed_scope = true;
1144
- }
1145
- }
1146
-
1147
- // $ref — Draft 2020-12: $ref is not a short-circuit, sibling keywords still apply
1148
- bool ref_resolved = false;
1149
- if (!node->ref.empty()) {
1150
- // Self-reference: "#"
1151
- if (node->ref == "#" && ctx.root) {
1152
- validate_node(ctx.root, value, path, ctx, errors, all_errors, dynamic_scope);
1153
- ref_resolved = true;
1154
- }
1155
- // Check for "base#fragment" pattern (e.g. "first#/$defs/stuff", "tree.json")
1156
- if (!ref_resolved) {
1157
- std::string base_uri;
1158
- std::string fragment;
1159
- size_t hash_pos = node->ref.find('#');
1160
- if (hash_pos != std::string::npos) {
1161
- base_uri = node->ref.substr(0, hash_pos);
1162
- fragment = node->ref.substr(hash_pos + 1);
1163
- } else {
1164
- base_uri = node->ref;
1165
- }
1166
-
1167
- // Helper: push base resource's dynamic anchors to scope, validate, pop
1168
- auto validate_with_resource_scope = [&](const schema_node_ptr& target,
1169
- const std::string& resource_id) {
1170
- bool scope_pushed = false;
1171
- if (dynamic_scope && !resource_id.empty()) {
1172
- auto rit = ctx.resource_dynamic_anchors.find(resource_id);
1173
- if (rit != ctx.resource_dynamic_anchors.end()) {
1174
- dynamic_scope->push_back(&rit->second);
1175
- scope_pushed = true;
1176
- }
1177
- }
1178
- validate_node(target, value, path, ctx, errors, all_errors, dynamic_scope);
1179
- if (scope_pushed) dynamic_scope->pop_back();
1180
- };
1181
-
1182
- if (!base_uri.empty()) {
1183
- // Resolve base URI to a resource via defs
1184
- auto it = ctx.defs.find(base_uri);
1185
- if (it != ctx.defs.end()) {
1186
- schema_node_ptr target = it->second;
1187
- if (!fragment.empty()) {
1188
- if (fragment[0] == '/') {
1189
- // JSON Pointer within the resource
1190
- auto resolved = walk_json_pointer(target, fragment);
1191
- if (resolved) {
1192
- validate_with_resource_scope(resolved, base_uri);
1193
- ref_resolved = true;
1194
- }
1195
- } else {
1196
- // Anchor lookup within the resource
1197
- auto resolved = find_anchor_in_resource(ctx, base_uri, fragment);
1198
- if (resolved) {
1199
- validate_with_resource_scope(resolved, base_uri);
1200
- ref_resolved = true;
1201
- }
1202
- }
1203
- } else {
1204
- // No fragment, just the base resource (it pushes its own scope)
1205
- validate_node(target, value, path, ctx, errors, all_errors, dynamic_scope);
1206
- ref_resolved = true;
1207
- }
1208
- }
1209
- } else if (!fragment.empty()) {
1210
- // "#fragment" — no base URI
1211
- if (fragment[0] == '/') {
1212
- // JSON Pointer from root
1213
- auto resolved = walk_json_pointer(ctx.root, fragment);
1214
- if (resolved) {
1215
- validate_node(resolved, value, path, ctx, errors, all_errors, dynamic_scope);
1216
- ref_resolved = true;
1217
- }
1218
- } else {
1219
- // Anchor lookup
1220
- auto ait = ctx.anchors.find(fragment);
1221
- if (ait != ctx.anchors.end()) {
1222
- validate_node(ait->second, value, path, ctx, errors, all_errors, dynamic_scope);
1223
- ref_resolved = true;
1224
- }
1225
- }
1226
- }
1227
- }
1228
- // Fallback: try defs map directly (handles bare $id references like "list")
1229
- if (!ref_resolved) {
1230
- auto it = ctx.defs.find(node->ref);
1231
- if (it != ctx.defs.end()) {
1232
- validate_node(it->second, value, path, ctx, errors, all_errors, dynamic_scope);
1233
- ref_resolved = true;
1234
- }
1235
- }
1236
- // Fallback: relative URI resolution — match ref against defs keys by suffix
1237
- if (!ref_resolved && !node->ref.empty() && node->ref[0] != '#') {
1238
- std::string suffix = "/" + node->ref;
1239
- for (const auto& [key, def_node] : ctx.defs) {
1240
- if (key.size() >= suffix.size() &&
1241
- key.compare(key.size() - suffix.size(), suffix.size(), suffix) == 0) {
1242
- validate_node(def_node, value, path, ctx, errors, all_errors, dynamic_scope);
1243
- ref_resolved = true;
1244
- break;
1245
- }
1246
- }
1247
- }
1248
- if (!ref_resolved) {
1249
- errors.push_back({error_code::ref_not_found, path,
1250
- "cannot resolve $ref: " + node->ref});
1251
- }
1252
- }
1253
-
1254
- // $dynamicRef — Draft 2020-12 dynamic scope resolution
1255
- if (!node->dynamic_ref.empty()) {
1256
- bool dref_resolved = false;
1257
-
1258
- // Parse the $dynamicRef value into base URI and fragment
1259
- std::string dr_base;
1260
- std::string dr_fragment;
1261
- {
1262
- size_t hash_pos = node->dynamic_ref.find('#');
1263
- if (hash_pos != std::string::npos) {
1264
- dr_base = node->dynamic_ref.substr(0, hash_pos);
1265
- dr_fragment = node->dynamic_ref.substr(hash_pos + 1);
1266
- } else {
1267
- dr_base = node->dynamic_ref;
1268
- }
1269
- }
1270
-
1271
- // Helper: push base resource's dynamic anchors to scope temporarily
1272
- auto push_resource_scope = [&](const std::string& resource_id) -> bool {
1273
- if (dynamic_scope && !resource_id.empty()) {
1274
- auto rit = ctx.resource_dynamic_anchors.find(resource_id);
1275
- if (rit != ctx.resource_dynamic_anchors.end()) {
1276
- dynamic_scope->push_back(&rit->second);
1277
- return true;
1278
- }
1279
- }
1280
- return false;
1281
- };
1282
-
1283
- // If fragment is a JSON pointer (starts with /), resolve like $ref
1284
- if (!dr_fragment.empty() && dr_fragment[0] == '/') {
1285
- schema_node_ptr base_node = dr_base.empty() ? ctx.root : nullptr;
1286
- if (!dr_base.empty()) {
1287
- auto it = ctx.defs.find(dr_base);
1288
- if (it != ctx.defs.end()) base_node = it->second;
1289
- }
1290
- if (base_node) {
1291
- auto resolved = walk_json_pointer(base_node, dr_fragment);
1292
- if (resolved) {
1293
- bool dr_scope_pushed = push_resource_scope(dr_base);
1294
- validate_node(resolved, value, path, ctx, errors, all_errors, dynamic_scope);
1295
- if (dr_scope_pushed) dynamic_scope->pop_back();
1296
- dref_resolved = true;
1297
- }
1298
- }
1299
- }
1300
-
1301
- // If fragment is an anchor name (not a JSON pointer)
1302
- if (!dref_resolved && !dr_fragment.empty() && dr_fragment[0] != '/') {
1303
- std::string anchor_name = dr_fragment;
1304
-
1305
- // Initial resolution: find the anchor
1306
- schema_node_ptr target = nullptr;
1307
-
1308
- if (!dr_base.empty()) {
1309
- // Resolve base URI first, then find anchor in that resource
1310
- auto it = ctx.defs.find(dr_base);
1311
- if (it != ctx.defs.end()) {
1312
- target = find_anchor_in_resource(ctx, dr_base, anchor_name);
1313
- }
1314
- } else {
1315
- // No base URI — look up in flat anchors map
1316
- auto ait = ctx.anchors.find(anchor_name);
1317
- if (ait != ctx.anchors.end()) {
1318
- target = ait->second;
1319
- }
1320
- }
1321
-
1322
- if (target) {
1323
- // Check if the initially resolved target is itself a $dynamicAnchor
1324
- // (the "bookend" requirement). Only do dynamic scope walk if the
1325
- // initial target's resource has a $dynamicAnchor with this name.
1326
- bool is_dynamic_at_initial = false;
1327
- if (!dr_base.empty()) {
1328
- // We resolved via a specific base URI
1329
- auto rit = ctx.resource_dynamic_anchors.find(dr_base);
1330
- if (rit != ctx.resource_dynamic_anchors.end() &&
1331
- rit->second.count(anchor_name)) {
1332
- is_dynamic_at_initial = true;
1333
- }
1334
- } else {
1335
- // No base URI — check if ANY resource has this as $dynamicAnchor
1336
- // and the target matches (i.e., the initially resolved node IS a
1337
- // $dynamicAnchor node)
1338
- for (const auto& [rid, rmap] : ctx.resource_dynamic_anchors) {
1339
- auto ait2 = rmap.find(anchor_name);
1340
- if (ait2 != rmap.end() && ait2->second == target) {
1341
- is_dynamic_at_initial = true;
1342
- break;
1343
- }
1344
- }
1345
- }
1346
-
1347
- // Dynamic scope walk: find first override in dynamic scope
1348
- if (is_dynamic_at_initial && dynamic_scope) {
1349
- for (size_t i = 0; i < dynamic_scope->size(); ++i) {
1350
- auto dit = (*dynamic_scope)[i]->find(anchor_name);
1351
- if (dit != (*dynamic_scope)[i]->end()) {
1352
- target = dit->second;
1353
- break;
1354
- }
1355
- }
1356
- }
1357
-
1358
- bool dr_scope_pushed = push_resource_scope(dr_base);
1359
- validate_node(target, value, path, ctx, errors, all_errors, dynamic_scope);
1360
- if (dr_scope_pushed) dynamic_scope->pop_back();
1361
- dref_resolved = true;
1362
- }
1363
- }
1364
-
1365
- // Bare $dynamicRef without fragment (unusual, but handle it)
1366
- if (!dref_resolved && dr_fragment.empty() && !dr_base.empty()) {
1367
- auto it = ctx.defs.find(dr_base);
1368
- if (it != ctx.defs.end()) {
1369
- validate_node(it->second, value, path, ctx, errors, all_errors, dynamic_scope);
1370
- dref_resolved = true;
1371
- }
1372
- }
1373
-
1374
- if (!dref_resolved) {
1375
- errors.push_back({error_code::ref_not_found, path,
1376
- "cannot resolve $dynamicRef: " + node->dynamic_ref});
1377
- }
1378
- }
1379
-
1380
- // type
1381
- if (node->type_mask) {
1382
- if (!type_matches_mask(value, node->type_mask)) {
1383
- std::string expected;
1384
- for (int b = 0; b < 7; ++b) {
1385
- if (node->type_mask & (1u << b)) {
1386
- if (!expected.empty()) expected += ", ";
1387
- expected += json_type_name(static_cast<json_type>(b));
1388
- }
1389
- }
1390
- std::string actual = std::string(type_of_sv(value));
1391
- errors.push_back({error_code::type_mismatch, path,
1392
- "expected type " + expected + ", got " + actual,
1393
- expected, actual});
1394
- ATA_CHECK_EARLY();
1395
- }
1396
- }
1397
-
1398
- // enum — use pre-minified values (no re-parsing)
1399
- if (!node->enum_values_minified.empty()) {
1400
- std::string val_str = canonical_json(value);
1401
- bool found = false;
1402
- for (const auto& ev : node->enum_values_minified) {
1403
- if (ev == val_str) {
1404
- found = true;
1405
- break;
1406
- }
1407
- }
1408
- if (!found) {
1409
- errors.push_back({error_code::enum_mismatch, path,
1410
- "value not in enum"});
1411
- }
1412
- }
1413
-
1414
- // const
1415
- if (node->const_value_raw.has_value()) {
1416
- std::string val_str = canonical_json(value);
1417
- if (val_str != node->const_value_raw.value()) {
1418
- errors.push_back({error_code::const_mismatch, path,
1419
- "value does not match const"});
1420
- ATA_CHECK_EARLY();
1421
- }
1422
- }
1423
-
1424
- ATA_CHECK_EARLY();
1425
- // Numeric validations
1426
- auto vtype = value.type();
1427
- if (vtype == et::INT64 || vtype == et::UINT64 || vtype == et::DOUBLE) {
1428
- double v = to_double(value);
1429
- if (node->minimum.has_value() && v < node->minimum.value()) {
1430
- errors.push_back({error_code::minimum_violation, path,
1431
- "value " + std::to_string(v) + " < minimum " +
1432
- std::to_string(node->minimum.value())});
1433
- }
1434
- if (node->maximum.has_value() && v > node->maximum.value()) {
1435
- errors.push_back({error_code::maximum_violation, path,
1436
- "value " + std::to_string(v) + " > maximum " +
1437
- std::to_string(node->maximum.value())});
1438
- }
1439
- if (node->exclusive_minimum.has_value() &&
1440
- v <= node->exclusive_minimum.value()) {
1441
- errors.push_back({error_code::exclusive_minimum_violation, path,
1442
- "value must be > " +
1443
- std::to_string(node->exclusive_minimum.value())});
1444
- }
1445
- if (node->exclusive_maximum.has_value() &&
1446
- v >= node->exclusive_maximum.value()) {
1447
- errors.push_back({error_code::exclusive_maximum_violation, path,
1448
- "value must be < " +
1449
- std::to_string(node->exclusive_maximum.value())});
1450
- }
1451
- if (node->multiple_of.has_value()) {
1452
- double divisor = node->multiple_of.value();
1453
- double rem = std::fmod(v, divisor);
1454
- // Use relative tolerance for floating point comparison
1455
- if (std::abs(rem) > 1e-8 && std::abs(rem - divisor) > 1e-8) {
1456
- errors.push_back({error_code::multiple_of_violation, path,
1457
- "value not a multiple of " +
1458
- std::to_string(node->multiple_of.value())});
1459
- }
1460
- }
1461
- }
1462
-
1463
- // String validations
1464
- if (vtype == et::STRING) {
1465
- std::string_view sv;
1466
- value.get(sv);
1467
- uint64_t len = utf8_length(sv);
1468
-
1469
- if (node->min_length.has_value() && len < node->min_length.value()) {
1470
- errors.push_back({error_code::min_length_violation, path,
1471
- "string length " + std::to_string(len) +
1472
- " < minLength " +
1473
- std::to_string(node->min_length.value())});
1474
- }
1475
- if (node->max_length.has_value() && len > node->max_length.value()) {
1476
- errors.push_back({error_code::max_length_violation, path,
1477
- "string length " + std::to_string(len) +
1478
- " > maxLength " +
1479
- std::to_string(node->max_length.value())});
1480
- }
1481
- #ifndef ATA_NO_RE2
1482
- if (node->compiled_pattern) {
1483
- if (!re2::RE2::PartialMatch(re2::StringPiece(sv.data(), sv.size()), *node->compiled_pattern)) {
1484
- errors.push_back({error_code::pattern_mismatch, path,
1485
- "string does not match pattern: " +
1486
- node->pattern.value()});
1487
- }
1488
- }
1489
- #endif
1490
-
1491
- if (node->format.has_value()) {
1492
- if (!check_format_by_id(sv, node->format_id)) {
1493
- errors.push_back({error_code::format_mismatch, path,
1494
- "string does not match format: " +
1495
- node->format.value()});
1496
- }
1497
- }
1498
- }
1499
-
1500
- // Array validations
1501
- if (vtype == et::ARRAY) {
1502
- dom::array arr; value.get(arr);
1503
- uint64_t arr_size = arr.size();
1504
- if(arr_size == 0xFFFFFF) [[unlikely]] {
1505
- // Fallback for large arrays where size() saturates — count manually to avoid overflow
1506
- arr_size = 0;
1507
- for ([[maybe_unused]] auto _ : arr) ++arr_size;
1508
- }
1509
-
1510
- if (node->min_items.has_value() && arr_size < node->min_items.value()) {
1511
- errors.push_back({error_code::min_items_violation, path,
1512
- "array has " + std::to_string(arr_size) +
1513
- " items, minimum " +
1514
- std::to_string(node->min_items.value())});
1515
- }
1516
- if (node->max_items.has_value() && arr_size > node->max_items.value()) {
1517
- errors.push_back({error_code::max_items_violation, path,
1518
- "array has " + std::to_string(arr_size) +
1519
- " items, maximum " +
1520
- std::to_string(node->max_items.value())});
1521
- }
1522
-
1523
- if (node->unique_items) {
1524
- bool has_dup = false;
1525
- // Fast path: check if all items are the same simple type
1526
- auto first_it = arr.begin();
1527
- if (first_it != arr.end()) {
1528
- auto first_type = (*first_it).type();
1529
- bool all_same = true;
1530
- for (auto item : arr) { if (item.type() != first_type) { all_same = false; break; } }
1531
- if (all_same && first_type == et::STRING) {
1532
- std::set<std::string_view> seen;
1533
- for (auto item : arr) {
1534
- std::string_view sv; item.get(sv);
1535
- if (!seen.insert(sv).second) { has_dup = true; break; }
1536
- }
1537
- } else if (all_same && (first_type == et::INT64 || first_type == et::UINT64 || first_type == et::DOUBLE)) {
1538
- std::set<double> seen;
1539
- for (auto item : arr) {
1540
- if (!seen.insert(to_double(item)).second) { has_dup = true; break; }
1541
- }
1542
- } else {
1543
- std::set<std::string> seen;
1544
- for (auto item : arr) {
1545
- if (!seen.insert(canonical_json(item)).second) { has_dup = true; break; }
1546
- }
1547
- }
1548
- }
1549
- if (has_dup) {
1550
- errors.push_back({error_code::unique_items_violation, path,
1551
- "array contains duplicate items"});
1552
- }
1553
- }
1554
-
1555
- // prefixItems + items (Draft 2020-12 semantics)
1556
- {
1557
- uint64_t idx = 0;
1558
- for (auto item : arr) {
1559
- if (idx < node->prefix_items.size()) {
1560
- validate_node(node->prefix_items[idx], item,
1561
- path + "/" + std::to_string(idx), ctx, errors, all_errors, dynamic_scope);
1562
- } else if (node->items_schema) {
1563
- validate_node(node->items_schema, item,
1564
- path + "/" + std::to_string(idx), ctx, errors, all_errors, dynamic_scope);
1565
- }
1566
- ++idx;
1567
- }
1568
- }
1569
-
1570
- // contains / minContains / maxContains
1571
- if (node->contains_schema) {
1572
- uint64_t match_count = 0;
1573
- for (auto item : arr) {
1574
- if (validate_fast(node->contains_schema, item, ctx)) ++match_count;
1575
- }
1576
- uint64_t min_c = node->min_contains.value_or(1);
1577
- uint64_t max_c = node->max_contains.value_or(arr_size);
1578
- if (match_count < min_c) {
1579
- errors.push_back({error_code::min_items_violation, path,
1580
- "contains: " + std::to_string(match_count) +
1581
- " matches, minimum " + std::to_string(min_c)});
1582
- }
1583
- if (match_count > max_c) {
1584
- errors.push_back({error_code::max_items_violation, path,
1585
- "contains: " + std::to_string(match_count) +
1586
- " matches, maximum " + std::to_string(max_c)});
1587
- }
1588
- }
1589
- }
1590
-
1591
- // Object validations
1592
- if (vtype == et::OBJECT) {
1593
- dom::object obj; value.get(obj);
1594
-
1595
- if (node->min_properties.has_value() || node->max_properties.has_value()) {
1596
- uint64_t prop_count = 0;
1597
- for ([[maybe_unused]] auto _ : obj) ++prop_count;
1598
- if (node->min_properties.has_value() &&
1599
- prop_count < node->min_properties.value()) {
1600
- errors.push_back({error_code::min_properties_violation, path,
1601
- "object has " + std::to_string(prop_count) +
1602
- " properties, minimum " +
1603
- std::to_string(node->min_properties.value())});
1604
- }
1605
- if (node->max_properties.has_value() &&
1606
- prop_count > node->max_properties.value()) {
1607
- errors.push_back({error_code::max_properties_violation, path,
1608
- "object has " + std::to_string(prop_count) +
1609
- " properties, maximum " +
1610
- std::to_string(node->max_properties.value())});
1611
- }
1612
- }
1613
-
1614
- // required
1615
- for (const auto& req : node->required) {
1616
- dom::element dummy;
1617
- if (obj[req].get(dummy) != SUCCESS) {
1618
- errors.push_back({error_code::required_property_missing, path,
1619
- "missing required property: " + req});
1620
- }
1621
- }
1622
-
1623
- // properties + patternProperties + additionalProperties
1624
- for (auto [key, val] : obj) {
1625
- std::string key_str(key);
1626
- bool matched = false;
1627
-
1628
- // Check properties
1629
- auto it = node->properties.find(key_str);
1630
- if (it != node->properties.end()) {
1631
- validate_node(it->second, val, path + "/" + key_str, ctx, errors, all_errors, dynamic_scope);
1632
- matched = true;
1633
- }
1634
-
1635
- // Check patternProperties (use cached compiled regex)
1636
- for (const auto& pp : node->pattern_properties) {
1637
- #ifndef ATA_NO_RE2
1638
- if (pp.compiled && re2::RE2::PartialMatch(key_str, *pp.compiled)) {
1639
- validate_node(pp.schema, val, path + "/" + key_str, ctx, errors, all_errors, dynamic_scope);
1640
- matched = true;
1641
- }
1642
- #endif
1643
- }
1644
-
1645
- // additionalProperties (only if not matched by properties or patternProperties)
1646
- if (!matched) {
1647
- if (node->additional_properties_bool.has_value() &&
1648
- !node->additional_properties_bool.value()) {
1649
- errors.push_back(
1650
- {error_code::additional_property_not_allowed, path,
1651
- "additional property not allowed: " + key_str});
1652
- } else if (node->additional_properties_schema) {
1653
- validate_node(node->additional_properties_schema, val,
1654
- path + "/" + key_str, ctx, errors, all_errors, dynamic_scope);
1655
- }
1656
- }
1657
- }
1658
- // propertyNames — validate key as string directly when possible
1659
- if (node->property_names_schema) {
1660
- auto pn = node->property_names_schema;
1661
- bool string_only = pn->ref.empty() && pn->all_of.empty() &&
1662
- pn->any_of.empty() && pn->one_of.empty() && !pn->not_schema &&
1663
- !pn->if_schema && pn->enum_values_minified.empty() &&
1664
- !pn->const_value_raw.has_value();
1665
- if (string_only) {
1666
- // Fast path: validate string constraints on key directly
1667
- for (auto [key, val] : obj) {
1668
- std::string_view key_sv(key);
1669
- if (pn->type_mask && !(pn->type_mask & json_type_bit(json_type::string))) {
1670
- errors.push_back({error_code::type_mismatch, path,
1671
- "propertyNames: key is string but schema requires different type"});
1672
- continue;
1673
- }
1674
- uint64_t len = utf8_length(key_sv);
1675
- if (pn->min_length.has_value() && len < pn->min_length.value()) {
1676
- errors.push_back({error_code::min_length_violation, path,
1677
- "propertyNames: key too short: " + std::string(key_sv)});
1678
- }
1679
- if (pn->max_length.has_value() && len > pn->max_length.value()) {
1680
- errors.push_back({error_code::max_length_violation, path,
1681
- "propertyNames: key too long: " + std::string(key_sv)});
1682
- }
1683
- #ifndef ATA_NO_RE2
1684
- if (pn->compiled_pattern) {
1685
- if (!re2::RE2::PartialMatch(re2::StringPiece(key_sv.data(), key_sv.size()), *pn->compiled_pattern)) {
1686
- errors.push_back({error_code::pattern_mismatch, path,
1687
- "propertyNames: key does not match pattern: " + std::string(key_sv)});
1688
- }
1689
- }
1690
- #endif
1691
- if (pn->format.has_value() && !check_format_by_id(key_sv, pn->format_id)) {
1692
- errors.push_back({error_code::format_mismatch, path,
1693
- "propertyNames: key does not match format: " + std::string(key_sv)});
1694
- }
1695
- }
1696
- } else {
1697
- // Fallback: parse key as JSON string element
1698
- for (auto [key, val] : obj) {
1699
- std::string key_json = "\"" + std::string(key) + "\"";
1700
- auto key_result = tl_dom_key_parser().parse(key_json);
1701
- if (!key_result.error()) {
1702
- validate_node(pn, key_result.value_unsafe(), path, ctx, errors, all_errors, dynamic_scope);
1703
- }
1704
- }
1705
- }
1706
- }
1707
-
1708
- // dependentRequired
1709
- for (const auto& [prop, deps] : node->dependent_required) {
1710
- dom::element dummy;
1711
- if (obj[prop].get(dummy) == SUCCESS) {
1712
- for (const auto& dep : deps) {
1713
- dom::element dep_dummy;
1714
- if (obj[dep].get(dep_dummy) != SUCCESS) {
1715
- errors.push_back({error_code::required_property_missing, path,
1716
- "property '" + prop + "' requires '" + dep +
1717
- "' to be present"});
1718
- }
1719
- }
1720
- }
1721
- }
1722
-
1723
- // dependentSchemas
1724
- for (const auto& [prop, schema] : node->dependent_schemas) {
1725
- dom::element dummy;
1726
- if (obj[prop].get(dummy) == SUCCESS) {
1727
- validate_node(schema, value, path, ctx, errors, all_errors, dynamic_scope);
1728
- }
1729
- }
1730
- }
1731
-
1732
- // allOf
1733
- if (!node->all_of.empty()) {
1734
- for (const auto& sub : node->all_of) {
1735
- std::vector<validation_error> sub_errors;
1736
- validate_node(sub, value, path, ctx, sub_errors, all_errors, dynamic_scope);
1737
- if (!sub_errors.empty()) {
1738
- errors.push_back({error_code::all_of_failed, path,
1739
- "allOf subschema failed"});
1740
- errors.insert(errors.end(), sub_errors.begin(), sub_errors.end());
1741
- }
1742
- }
1743
- }
1744
-
1745
- // anyOf
1746
- if (!node->any_of.empty()) {
1747
- bool any_valid = false;
1748
- for (const auto& sub : node->any_of) {
1749
- std::vector<validation_error> sub_errors;
1750
- validate_node(sub, value, path, ctx, sub_errors, all_errors, dynamic_scope);
1751
- if (sub_errors.empty()) {
1752
- any_valid = true;
1753
- break;
1754
- }
1755
- }
1756
- if (!any_valid) {
1757
- errors.push_back({error_code::any_of_failed, path,
1758
- "no anyOf subschema matched"});
1759
- }
1760
- }
1761
-
1762
- // oneOf
1763
- if (!node->one_of.empty()) {
1764
- int match_count = 0;
1765
- for (const auto& sub : node->one_of) {
1766
- std::vector<validation_error> sub_errors;
1767
- validate_node(sub, value, path, ctx, sub_errors, all_errors, dynamic_scope);
1768
- if (sub_errors.empty()) ++match_count;
1769
- }
1770
- if (match_count != 1) {
1771
- errors.push_back({error_code::one_of_failed, path,
1772
- "expected exactly one oneOf match, got " +
1773
- std::to_string(match_count)});
1774
- }
1775
- }
1776
-
1777
- // not
1778
- if (node->not_schema) {
1779
- std::vector<validation_error> sub_errors;
1780
- validate_node(node->not_schema, value, path, ctx, sub_errors, all_errors, dynamic_scope);
1781
- if (sub_errors.empty()) {
1782
- errors.push_back({error_code::not_failed, path,
1783
- "value should not match 'not' schema"});
1784
- }
1785
- }
1786
-
1787
- // if/then/else
1788
- if (node->if_schema) {
1789
- std::vector<validation_error> if_errors;
1790
- validate_node(node->if_schema, value, path, ctx, if_errors, all_errors, dynamic_scope);
1791
- if (if_errors.empty()) {
1792
- // if passed → validate then
1793
- if (node->then_schema) {
1794
- validate_node(node->then_schema, value, path, ctx, errors, all_errors, dynamic_scope);
1795
- }
1796
- } else {
1797
- // if failed → validate else
1798
- if (node->else_schema) {
1799
- validate_node(node->else_schema, value, path, ctx, errors, all_errors, dynamic_scope);
1800
- }
1801
- }
1802
- }
1803
-
1804
- if (pushed_scope) dynamic_scope->pop_back();
1805
- }
1806
-
1807
- // Fast boolean-only tree walker — stripped of all error collection.
1808
- // No std::string allocation, no path tracking, no error messages.
1809
- // Returns true if valid. Uses [[likely]]/[[unlikely]] branch hints.
1810
- static bool validate_fast(const schema_node_ptr& node,
1811
- dom::element value,
1812
- const compiled_schema& ctx) {
1813
- if (!node) [[unlikely]] return true;
1814
-
1815
- DepthGuard guard;
1816
- if (guard.overflow) [[unlikely]] return true;
1817
-
1818
- if (node->boolean_schema.has_value()) [[unlikely]]
1819
- return node->boolean_schema.value();
1820
-
1821
- // $dynamicRef — bail to tree walker
1822
- if (!node->dynamic_ref.empty()) [[unlikely]] return false;
1823
-
1824
- // $ref
1825
- if (!node->ref.empty()) [[unlikely]] {
1826
- auto it = ctx.defs.find(node->ref);
1827
- if (it != ctx.defs.end()) {
1828
- if (!validate_fast(it->second, value, ctx)) return false;
1829
- } else if (node->ref.size() > 1 && node->ref[0] == '#' && node->ref[1] != '/') {
1830
- auto ait = ctx.anchors.find(node->ref.substr(1));
1831
- if (ait != ctx.anchors.end()) {
1832
- if (!validate_fast(ait->second, value, ctx)) return false;
1833
- } else {
1834
- return false;
1835
- }
1836
- } else if (node->ref == "#" && ctx.root) {
1837
- if (!validate_fast(ctx.root, value, ctx)) return false;
1838
- } else {
1839
- return false;
1840
- }
1841
- }
1842
-
1843
- // type
1844
- if (node->type_mask) {
1845
- if (!type_matches_mask(value, node->type_mask)) [[unlikely]] return false;
1846
- }
1847
-
1848
- // enum
1849
- if (!node->enum_values_minified.empty()) {
1850
- auto val_str = canonical_json(value);
1851
- bool found = false;
1852
- for (const auto& ev : node->enum_values_minified) {
1853
- if (ev == val_str) { found = true; break; }
1854
- }
1855
- if (!found) [[unlikely]] return false;
1856
- }
1857
-
1858
- // const
1859
- if (node->const_value_raw.has_value()) {
1860
- if (canonical_json(value) != node->const_value_raw.value()) [[unlikely]] return false;
1861
- }
1862
-
1863
- auto vtype = value.type();
1864
-
1865
- // Numeric
1866
- if (vtype == et::INT64 || vtype == et::UINT64 || vtype == et::DOUBLE) {
1867
- double v = to_double(value);
1868
- if (node->minimum.has_value() && v < node->minimum.value()) return false;
1869
- if (node->maximum.has_value() && v > node->maximum.value()) return false;
1870
- if (node->exclusive_minimum.has_value() && v <= node->exclusive_minimum.value()) return false;
1871
- if (node->exclusive_maximum.has_value() && v >= node->exclusive_maximum.value()) return false;
1872
- if (node->multiple_of.has_value()) {
1873
- double rem = std::fmod(v, node->multiple_of.value());
1874
- if (std::abs(rem) > 1e-8 && std::abs(rem - node->multiple_of.value()) > 1e-8) return false;
1875
- }
1876
- }
1877
-
1878
- // String
1879
- if (vtype == et::STRING) {
1880
- std::string_view sv;
1881
- value.get(sv);
1882
- uint64_t len = utf8_length(sv);
1883
- if (node->min_length.has_value() && len < node->min_length.value()) return false;
1884
- if (node->max_length.has_value() && len > node->max_length.value()) return false;
1885
- #ifndef ATA_NO_RE2
1886
- if (node->compiled_pattern) {
1887
- if (!re2::RE2::PartialMatch(re2::StringPiece(sv.data(), sv.size()), *node->compiled_pattern))
1888
- return false;
1889
- }
1890
- #endif
1891
- if (node->format.has_value() && !check_format_by_id(sv, node->format_id)) return false;
1892
- }
1893
-
1894
- // Array
1895
- if (vtype == et::ARRAY) {
1896
- dom::array arr; value.get(arr);
1897
- uint64_t arr_size = arr.size();
1898
- if(arr_size == 0xFFFFFF) [[unlikely]] {
1899
- // Fallback for large arrays where size() saturates — count manually to avoid overflow
1900
- arr_size = 0;
1901
- for ([[maybe_unused]] auto _ : arr) ++arr_size;
1902
- }
1903
-
1904
- if (node->min_items.has_value() && arr_size < node->min_items.value()) return false;
1905
- if (node->max_items.has_value() && arr_size > node->max_items.value()) return false;
1906
-
1907
- if (node->unique_items) {
1908
- auto first_it = arr.begin();
1909
- if (first_it != arr.end()) {
1910
- auto first_type = (*first_it).type();
1911
- bool all_same = true;
1912
- for (auto item : arr) { if (item.type() != first_type) { all_same = false; break; } }
1913
- if (all_same && first_type == et::STRING) {
1914
- std::set<std::string_view> seen;
1915
- for (auto item : arr) { std::string_view sv; item.get(sv); if (!seen.insert(sv).second) return false; }
1916
- } else if (all_same && (first_type == et::INT64 || first_type == et::UINT64 || first_type == et::DOUBLE)) {
1917
- std::set<double> seen;
1918
- for (auto item : arr) { if (!seen.insert(to_double(item)).second) return false; }
1919
- } else {
1920
- std::set<std::string> seen;
1921
- for (auto item : arr) { if (!seen.insert(canonical_json(item)).second) return false; }
1922
- }
1923
- }
1924
- }
1925
-
1926
- { uint64_t idx = 0;
1927
- for (auto item : arr) {
1928
- if (idx < node->prefix_items.size()) {
1929
- if (!validate_fast(node->prefix_items[idx], item, ctx)) return false;
1930
- } else if (node->items_schema) {
1931
- if (!validate_fast(node->items_schema, item, ctx)) return false;
1932
- }
1933
- ++idx;
1934
- }
1935
- }
1936
-
1937
- if (node->contains_schema) {
1938
- uint64_t match_count = 0;
1939
- for (auto item : arr) {
1940
- if (validate_fast(node->contains_schema, item, ctx)) ++match_count;
1941
- }
1942
- uint64_t min_c = node->min_contains.value_or(1);
1943
- uint64_t max_c = node->max_contains.value_or(arr_size);
1944
- if (match_count < min_c || match_count > max_c) return false;
1945
- }
1946
- }
1947
-
1948
- // Object
1949
- if (vtype == et::OBJECT) {
1950
- dom::object obj; value.get(obj);
1951
-
1952
- if (node->min_properties.has_value() || node->max_properties.has_value()) {
1953
- uint64_t n = 0;
1954
- for ([[maybe_unused]] auto _ : obj) ++n;
1955
- if (node->min_properties.has_value() && n < node->min_properties.value()) return false;
1956
- if (node->max_properties.has_value() && n > node->max_properties.value()) return false;
1957
- }
1958
-
1959
- for (const auto& req : node->required) {
1960
- dom::element d;
1961
- if (obj[req].get(d) != SUCCESS) [[unlikely]] return false;
1962
- }
1963
-
1964
- for (auto [key, val] : obj) {
1965
- std::string_view key_sv(key);
1966
- bool matched = false;
1967
-
1968
- auto it = node->properties.find(std::string(key_sv));
1969
- if (it != node->properties.end()) {
1970
- if (!validate_fast(it->second, val, ctx)) return false;
1971
- matched = true;
1972
- }
1973
-
1974
- for (const auto& pp : node->pattern_properties) {
1975
- #ifndef ATA_NO_RE2
1976
- if (pp.compiled && re2::RE2::PartialMatch(
1977
- re2::StringPiece(key_sv.data(), key_sv.size()), *pp.compiled)) {
1978
- if (!validate_fast(pp.schema, val, ctx)) return false;
1979
- matched = true;
1980
- }
1981
- #endif
1982
- }
1983
-
1984
- if (!matched) {
1985
- if (node->additional_properties_bool.has_value() &&
1986
- !node->additional_properties_bool.value()) return false;
1987
- if (node->additional_properties_schema &&
1988
- !validate_fast(node->additional_properties_schema, val, ctx)) return false;
1989
- }
1990
- }
1991
-
1992
- for (const auto& [prop, deps] : node->dependent_required) {
1993
- dom::element d;
1994
- if (obj[prop].get(d) == SUCCESS) {
1995
- for (const auto& dep : deps) {
1996
- dom::element dd;
1997
- if (obj[dep].get(dd) != SUCCESS) return false;
1998
- }
1999
- }
2000
- }
2001
-
2002
- for (const auto& [prop, schema] : node->dependent_schemas) {
2003
- dom::element d;
2004
- if (obj[prop].get(d) == SUCCESS) {
2005
- if (!validate_fast(schema, value, ctx)) return false;
2006
- }
2007
- }
2008
- }
2009
-
2010
- // allOf
2011
- for (const auto& sub : node->all_of) {
2012
- if (!validate_fast(sub, value, ctx)) return false;
2013
- }
2014
-
2015
- // anyOf
2016
- if (!node->any_of.empty()) {
2017
- bool any = false;
2018
- for (const auto& sub : node->any_of) {
2019
- if (validate_fast(sub, value, ctx)) { any = true; break; }
2020
- }
2021
- if (!any) return false;
2022
- }
2023
-
2024
- // oneOf
2025
- if (!node->one_of.empty()) {
2026
- int n = 0;
2027
- for (const auto& sub : node->one_of) {
2028
- if (validate_fast(sub, value, ctx)) ++n;
2029
- if (n > 1) return false;
2030
- }
2031
- if (n != 1) return false;
2032
- }
2033
-
2034
- // not
2035
- if (node->not_schema) {
2036
- if (validate_fast(node->not_schema, value, ctx)) return false;
2037
- }
2038
-
2039
- // if/then/else
2040
- if (node->if_schema) {
2041
- if (validate_fast(node->if_schema, value, ctx)) {
2042
- if (node->then_schema && !validate_fast(node->then_schema, value, ctx)) return false;
2043
- } else {
2044
- if (node->else_schema && !validate_fast(node->else_schema, value, ctx)) return false;
2045
- }
2046
- }
2047
-
2048
- return true;
2049
- }
2050
-
2051
- // --- Codegen compiler ---
2052
- static void cg_compile(const schema_node* n, cg::plan& p,
2053
- std::vector<cg::ins>& out) {
2054
- if (!n) return;
2055
- if (n->boolean_schema.has_value()) {
2056
- if (!*n->boolean_schema) out.push_back({cg::op::EXPECT_NULL});
2057
- return;
2058
- }
2059
- // Composition fallback
2060
- if (!n->ref.empty() || !n->dynamic_ref.empty() || !n->all_of.empty() ||
2061
- !n->any_of.empty() || !n->one_of.empty() || n->not_schema ||
2062
- n->if_schema) {
2063
- uintptr_t ptr = reinterpret_cast<uintptr_t>(n);
2064
- out.push_back({cg::op::COMPOSITION, (uint32_t)(ptr & 0xFFFFFFFF),
2065
- (uint32_t)((ptr >> 32) & 0xFFFFFFFF)});
2066
- return;
2067
- }
2068
- // Type
2069
- if (n->type_mask) {
2070
- int popcount = __builtin_popcount(n->type_mask);
2071
- if (popcount == 1) {
2072
- // Single type — emit specific opcode
2073
- for (int b = 0; b < 7; ++b) {
2074
- if (n->type_mask & (1u << b)) {
2075
- switch (static_cast<json_type>(b)) {
2076
- case json_type::object: out.push_back({cg::op::EXPECT_OBJECT}); break;
2077
- case json_type::array: out.push_back({cg::op::EXPECT_ARRAY}); break;
2078
- case json_type::string: out.push_back({cg::op::EXPECT_STRING}); break;
2079
- case json_type::number: out.push_back({cg::op::EXPECT_NUMBER}); break;
2080
- case json_type::integer: out.push_back({cg::op::EXPECT_INTEGER}); break;
2081
- case json_type::boolean: out.push_back({cg::op::EXPECT_BOOLEAN}); break;
2082
- case json_type::null_value: out.push_back({cg::op::EXPECT_NULL}); break;
2083
- }
2084
- break;
2085
- }
2086
- }
2087
- } else {
2088
- uint32_t i = (uint32_t)p.type_masks.size();
2089
- p.type_masks.push_back(n->type_mask);
2090
- out.push_back({cg::op::EXPECT_TYPE_MULTI, i});
2091
- }
2092
- }
2093
- // Enum
2094
- if (!n->enum_values_minified.empty()) {
2095
- bool all_str = true;
2096
- for (auto& e : n->enum_values_minified)
2097
- if (e.empty() || e[0]!='"') { all_str=false; break; }
2098
- uint32_t i = (uint32_t)p.enum_sets.size();
2099
- p.enum_sets.push_back(n->enum_values_minified);
2100
- out.push_back({all_str ? cg::op::CHECK_ENUM_STR : cg::op::CHECK_ENUM, i});
2101
- }
2102
- if (n->const_value_raw.has_value()) {
2103
- uint32_t i=(uint32_t)p.strings.size();
2104
- p.strings.push_back(*n->const_value_raw);
2105
- out.push_back({cg::op::CHECK_CONST, i});
2106
- }
2107
- // Numeric
2108
- if (n->minimum.has_value()) { uint32_t i=(uint32_t)p.doubles.size(); p.doubles.push_back(*n->minimum); out.push_back({cg::op::CHECK_MINIMUM,i}); }
2109
- if (n->maximum.has_value()) { uint32_t i=(uint32_t)p.doubles.size(); p.doubles.push_back(*n->maximum); out.push_back({cg::op::CHECK_MAXIMUM,i}); }
2110
- if (n->exclusive_minimum.has_value()) { uint32_t i=(uint32_t)p.doubles.size(); p.doubles.push_back(*n->exclusive_minimum); out.push_back({cg::op::CHECK_EX_MINIMUM,i}); }
2111
- if (n->exclusive_maximum.has_value()) { uint32_t i=(uint32_t)p.doubles.size(); p.doubles.push_back(*n->exclusive_maximum); out.push_back({cg::op::CHECK_EX_MAXIMUM,i}); }
2112
- if (n->multiple_of.has_value()) { uint32_t i=(uint32_t)p.doubles.size(); p.doubles.push_back(*n->multiple_of); out.push_back({cg::op::CHECK_MULTIPLE_OF,i}); }
2113
- // String
2114
- if (n->min_length.has_value()) out.push_back({cg::op::CHECK_MIN_LENGTH,(uint32_t)*n->min_length});
2115
- if (n->max_length.has_value()) out.push_back({cg::op::CHECK_MAX_LENGTH,(uint32_t)*n->max_length});
2116
- #ifndef ATA_NO_RE2
2117
- if (n->compiled_pattern) { uint32_t i=(uint32_t)p.regexes.size(); p.regexes.push_back(n->compiled_pattern); out.push_back({cg::op::CHECK_PATTERN,i}); }
2118
- #endif
2119
- if (n->format.has_value()) {
2120
- uint32_t i=(uint32_t)p.format_ids.size();
2121
- p.format_ids.push_back(n->format_id);
2122
- out.push_back({cg::op::CHECK_FORMAT,i});
2123
- }
2124
- // Array
2125
- if (n->min_items.has_value()) out.push_back({cg::op::CHECK_MIN_ITEMS,(uint32_t)*n->min_items});
2126
- if (n->max_items.has_value()) out.push_back({cg::op::CHECK_MAX_ITEMS,(uint32_t)*n->max_items});
2127
- if (n->unique_items) out.push_back({cg::op::CHECK_UNIQUE_ITEMS});
2128
- if (n->items_schema) {
2129
- uint32_t si=(uint32_t)p.subs.size();
2130
- p.subs.emplace_back();
2131
- std::vector<cg::ins> sub_code;
2132
- cg_compile(n->items_schema.get(), p, sub_code);
2133
- sub_code.push_back({cg::op::END});
2134
- p.subs[si] = std::move(sub_code);
2135
- out.push_back({cg::op::ARRAY_ITEMS, si});
2136
- }
2137
- // Object
2138
- for (auto& r : n->required) { uint32_t i=(uint32_t)p.strings.size(); p.strings.push_back(r); out.push_back({cg::op::CHECK_REQUIRED,i}); }
2139
- if (n->min_properties.has_value()) out.push_back({cg::op::CHECK_MIN_PROPS,(uint32_t)*n->min_properties});
2140
- if (n->max_properties.has_value()) out.push_back({cg::op::CHECK_MAX_PROPS,(uint32_t)*n->max_properties});
2141
- // additional_properties_schema requires tree walker — bail out to COMPOSITION
2142
- if (n->additional_properties_schema) {
2143
- out.push_back({cg::op::COMPOSITION, 0, 0});
2144
- return;
2145
- }
2146
- if (!n->properties.empty() || (n->additional_properties_bool.has_value() && !*n->additional_properties_bool)) {
2147
- out.push_back({cg::op::OBJ_PROPS_START});
2148
- if (n->additional_properties_bool.has_value() && !*n->additional_properties_bool)
2149
- out.push_back({cg::op::CHECK_NO_ADDITIONAL});
2150
- for (auto& [name, schema] : n->properties) {
2151
- uint32_t ni=(uint32_t)p.strings.size(); p.strings.push_back(name);
2152
- uint32_t si=(uint32_t)p.subs.size();
2153
- p.subs.emplace_back();
2154
- std::vector<cg::ins> sub_code;
2155
- cg_compile(schema.get(), p, sub_code);
2156
- sub_code.push_back({cg::op::END});
2157
- p.subs[si] = std::move(sub_code);
2158
- out.push_back({cg::op::OBJ_PROP, ni, si});
2159
- }
2160
- out.push_back({cg::op::OBJ_PROPS_END});
2161
- }
2162
- }
2163
-
2164
- // --- Codegen executor ---
2165
-
2166
- static bool cg_exec(const cg::plan& p, const std::vector<cg::ins>& code,
2167
- dom::element value) {
2168
- auto t = value.type();
2169
- bool t_numeric = (t == et::INT64 || t == et::UINT64 || t == et::DOUBLE);
2170
- double t_dval = t_numeric ? to_double(value) : 0.0;
2171
- for (size_t i=0; i<code.size(); ++i) {
2172
- auto& c = code[i];
2173
- switch(c.o) {
2174
- case cg::op::END: return true;
2175
- case cg::op::EXPECT_OBJECT: if(t!=et::OBJECT) return false; break;
2176
- case cg::op::EXPECT_ARRAY: if(t!=et::ARRAY) return false; break;
2177
- case cg::op::EXPECT_STRING: if(t!=et::STRING) return false; break;
2178
- case cg::op::EXPECT_NUMBER: if(!t_numeric) return false; break;
2179
- case cg::op::EXPECT_INTEGER: if(t!=et::INT64&&t!=et::UINT64) return false; break;
2180
- case cg::op::EXPECT_BOOLEAN: if(t!=et::BOOL) return false; break;
2181
- case cg::op::EXPECT_NULL: if(t!=et::NULL_VALUE) return false; break;
2182
- case cg::op::EXPECT_TYPE_MULTI: {
2183
- if(!(element_type_mask(t) & p.type_masks[c.a])) return false; break;
2184
- }
2185
- case cg::op::CHECK_MINIMUM: if(t_numeric&&t_dval<p.doubles[c.a])return false; break;
2186
- case cg::op::CHECK_MAXIMUM: if(t_numeric&&t_dval>p.doubles[c.a])return false; break;
2187
- case cg::op::CHECK_EX_MINIMUM: if(t_numeric&&t_dval<=p.doubles[c.a])return false; break;
2188
- case cg::op::CHECK_EX_MAXIMUM: if(t_numeric&&t_dval>=p.doubles[c.a])return false; break;
2189
- case cg::op::CHECK_MULTIPLE_OF: if(t_numeric){double d=p.doubles[c.a],r=std::fmod(t_dval,d);if(std::abs(r)>1e-8&&std::abs(r-d)>1e-8)return false;} break;
2190
- case cg::op::CHECK_MIN_LENGTH: if(t==et::STRING){std::string_view sv;value.get(sv);if(utf8_length(sv)<c.a)return false;} break;
2191
- case cg::op::CHECK_MAX_LENGTH: if(t==et::STRING){std::string_view sv;value.get(sv);if(utf8_length(sv)>c.a)return false;} break;
2192
- #ifndef ATA_NO_RE2
2193
- case cg::op::CHECK_PATTERN: if(t==et::STRING){std::string_view sv;value.get(sv);if(!re2::RE2::PartialMatch(re2::StringPiece(sv.data(),sv.size()),*p.regexes[c.a]))return false;} break;
2194
- #else
2195
- case cg::op::CHECK_PATTERN: break;
2196
- #endif
2197
- case cg::op::CHECK_FORMAT: if(t==et::STRING){std::string_view sv;value.get(sv);if(!check_format_by_id(sv,p.format_ids[c.a]))return false;} break;
2198
- case cg::op::CHECK_MIN_ITEMS: if(t==et::ARRAY){dom::array a;value.get(a);uint64_t s=0;for([[maybe_unused]]auto _:a)++s;if(s<c.a)return false;} break;
2199
- case cg::op::CHECK_MAX_ITEMS: if(t==et::ARRAY){dom::array a;value.get(a);uint64_t s=0;for([[maybe_unused]]auto _:a)++s;if(s>c.a)return false;} break;
2200
- case cg::op::CHECK_UNIQUE_ITEMS: if(t==et::ARRAY){dom::array a;value.get(a);std::set<std::string> seen;for(auto x:a)if(!seen.insert(canonical_json(x)).second)return false;} break;
2201
- case cg::op::ARRAY_ITEMS: if(t==et::ARRAY){dom::array a;value.get(a);for(auto x:a)if(!cg_exec(p,p.subs[c.a],x))return false;} break;
2202
- case cg::op::CHECK_REQUIRED: if(t==et::OBJECT){dom::object o;value.get(o);dom::element d;if(o[p.strings[c.a]].get(d)!=SUCCESS)return false;} break;
2203
- case cg::op::CHECK_MIN_PROPS: if(t==et::OBJECT){dom::object o;value.get(o);uint64_t n=0;for([[maybe_unused]]auto _:o)++n;if(n<c.a)return false;} break;
2204
- case cg::op::CHECK_MAX_PROPS: if(t==et::OBJECT){dom::object o;value.get(o);uint64_t n=0;for([[maybe_unused]]auto _:o)++n;if(n>c.a)return false;} break;
2205
- case cg::op::OBJ_PROPS_START: if(t==et::OBJECT){
2206
- dom::object o; value.get(o);
2207
- // collect prop defs
2208
- struct pd{std::string_view nm;uint32_t si;};
2209
- std::vector<pd> props; bool no_add=false;
2210
- size_t j=i+1;
2211
- for(;j<code.size()&&code[j].o!=cg::op::OBJ_PROPS_END;++j){
2212
- if(code[j].o==cg::op::OBJ_PROP) props.push_back({p.strings[code[j].a],code[j].b});
2213
- else if(code[j].o==cg::op::CHECK_NO_ADDITIONAL) no_add=true;
2214
- }
2215
- for(auto [key,val]:o){
2216
- bool matched=false;
2217
- for(auto& pp:props){if(key==pp.nm){if(!cg_exec(p,p.subs[pp.si],val))return false;matched=true;break;}}
2218
- if(!matched&&no_add)return false;
2219
- }
2220
- i=j; break;
2221
- } else { /* skip to OBJ_PROPS_END */ size_t j=i+1; for(;j<code.size()&&code[j].o!=cg::op::OBJ_PROPS_END;++j); i=j; } break;
2222
- case cg::op::OBJ_PROP: case cg::op::OBJ_PROPS_END: case cg::op::CHECK_NO_ADDITIONAL: break;
2223
- case cg::op::CHECK_ENUM_STR: {
2224
- auto& es=p.enum_sets[c.a]; bool f=false;
2225
- if(t==et::STRING){std::string_view sv;value.get(sv);for(auto& e:es)if(e.size()==sv.size()+2&&e[0]=='"'&&e.back()=='"'&&e.compare(1,sv.size(),sv)==0){f=true;break;}}
2226
- if(!f){std::string v=canonical_json(value);for(auto& e:es)if(e==v){f=true;break;}}
2227
- if(!f)return false; break;
2228
- }
2229
- case cg::op::CHECK_ENUM: {
2230
- auto& es=p.enum_sets[c.a]; bool f=false;
2231
- if(t==et::STRING){std::string_view sv;value.get(sv);for(auto& e:es)if(e.size()==sv.size()+2&&e[0]=='"'&&e.back()=='"'&&e.compare(1,sv.size(),sv)==0){f=true;break;}}
2232
- if(!f&&value.is<int64_t>()){int64_t v;value.get(v);auto s=std::to_string(v);for(auto& e:es)if(e==s){f=true;break;}}
2233
- if(!f){std::string v=canonical_json(value);for(auto& e:es)if(e==v){f=true;break;}}
2234
- if(!f)return false; break;
2235
- }
2236
- case cg::op::CHECK_CONST: if(canonical_json(value)!=p.strings[c.a])return false; break;
2237
- case cg::op::COMPOSITION: return false; // fallback to tree walker
2238
- }
2239
- }
2240
- return true;
2241
- }
2242
-
2243
- // --- On Demand fast path executor ---
2244
- // Uses simdjson On Demand API to avoid materializing the full DOM tree.
2245
- // Returns: true = valid, false = invalid OR unsupported (fallback to DOM).
2246
-
2247
- static json_type od_type(simdjson::ondemand::value& v) {
2248
- simdjson::ondemand::json_type jt;
2249
- if (v.type().get(jt)) return json_type::null_value;
2250
- switch (jt) {
2251
- case simdjson::ondemand::json_type::object: return json_type::object;
2252
- case simdjson::ondemand::json_type::array: return json_type::array;
2253
- case simdjson::ondemand::json_type::string: return json_type::string;
2254
- case simdjson::ondemand::json_type::boolean: return json_type::boolean;
2255
- case simdjson::ondemand::json_type::null: return json_type::null_value;
2256
- case simdjson::ondemand::json_type::number: {
2257
- simdjson::ondemand::number_type nt;
2258
- if (v.get_number_type().get(nt) == SUCCESS &&
2259
- nt == simdjson::ondemand::number_type::floating_point_number)
2260
- return json_type::number;
2261
- return json_type::integer;
2262
- }
2263
- }
2264
- return json_type::string;
2265
- }
2266
-
2267
- static bool od_exec(const cg::plan& p, const std::vector<cg::ins>& code,
2268
- simdjson::ondemand::value value) {
2269
- auto t = od_type(value);
2270
- bool t_numeric = (t == json_type::integer || t == json_type::number);
2271
- for (size_t i = 0; i < code.size(); ++i) {
2272
- auto& c = code[i];
2273
- switch (c.o) {
2274
- case cg::op::END: return true;
2275
- case cg::op::EXPECT_OBJECT: if(t!=json_type::object) return false; break;
2276
- case cg::op::EXPECT_ARRAY: if(t!=json_type::array) return false; break;
2277
- case cg::op::EXPECT_STRING: if(t!=json_type::string) return false; break;
2278
- case cg::op::EXPECT_NUMBER: if(!t_numeric) return false; break;
2279
- case cg::op::EXPECT_INTEGER: if(t!=json_type::integer) return false; break;
2280
- case cg::op::EXPECT_BOOLEAN: if(t!=json_type::boolean) return false; break;
2281
- case cg::op::EXPECT_NULL: if(t!=json_type::null_value) return false; break;
2282
- case cg::op::EXPECT_TYPE_MULTI: {
2283
- // integer matches both "integer" and "number" type constraints
2284
- uint8_t tbits = json_type_bit(t);
2285
- if (t == json_type::integer) tbits |= json_type_bit(json_type::number);
2286
- if(!(tbits & p.type_masks[c.a])) return false; break;
2287
- }
2288
- case cg::op::CHECK_MINIMUM:
2289
- case cg::op::CHECK_MAXIMUM:
2290
- case cg::op::CHECK_EX_MINIMUM:
2291
- case cg::op::CHECK_EX_MAXIMUM:
2292
- case cg::op::CHECK_MULTIPLE_OF: {
2293
- if (t_numeric) {
2294
- double v;
2295
- if (t==json_type::integer) { int64_t iv; if(value.get(iv)!=SUCCESS) return false; v=(double)iv; }
2296
- else { if(value.get(v)!=SUCCESS) return false; }
2297
- double d=p.doubles[c.a];
2298
- if(c.o==cg::op::CHECK_MINIMUM && v<d) return false;
2299
- if(c.o==cg::op::CHECK_MAXIMUM && v>d) return false;
2300
- if(c.o==cg::op::CHECK_EX_MINIMUM && v<=d) return false;
2301
- if(c.o==cg::op::CHECK_EX_MAXIMUM && v>=d) return false;
2302
- if(c.o==cg::op::CHECK_MULTIPLE_OF){double r=std::fmod(v,d);if(std::abs(r)>1e-8&&std::abs(r-d)>1e-8)return false;}
2303
- }
2304
- break;
2305
- }
2306
- case cg::op::CHECK_MIN_LENGTH: if(t==json_type::string){std::string_view sv; if(value.get(sv)!=SUCCESS) return false; if(utf8_length(sv)<c.a) return false;} break;
2307
- case cg::op::CHECK_MAX_LENGTH: if(t==json_type::string){std::string_view sv; if(value.get(sv)!=SUCCESS) return false; if(utf8_length(sv)>c.a) return false;} break;
2308
- #ifndef ATA_NO_RE2
2309
- case cg::op::CHECK_PATTERN: if(t==json_type::string){std::string_view sv; if(value.get(sv)!=SUCCESS) return false; if(!re2::RE2::PartialMatch(re2::StringPiece(sv.data(),sv.size()),*p.regexes[c.a]))return false;} break;
2310
- #else
2311
- case cg::op::CHECK_PATTERN: break;
2312
- #endif
2313
- case cg::op::CHECK_FORMAT: if(t==json_type::string){std::string_view sv; if(value.get(sv)!=SUCCESS) return false; if(!check_format_by_id(sv,p.format_ids[c.a]))return false;} break;
2314
- case cg::op::CHECK_MIN_ITEMS: if(t==json_type::array){
2315
- simdjson::ondemand::array a; if(value.get(a)!=SUCCESS) return false;
2316
- uint64_t s=0; for(auto x:a){(void)x;++s;} if(s<c.a) return false;
2317
- } break;
2318
- case cg::op::CHECK_MAX_ITEMS: if(t==json_type::array){
2319
- simdjson::ondemand::array a; if(value.get(a)!=SUCCESS) return false;
2320
- uint64_t s=0; for(auto x:a){(void)x;++s;} if(s>c.a) return false;
2321
- } break;
2322
- case cg::op::ARRAY_ITEMS: if(t==json_type::array){
2323
- simdjson::ondemand::array a; if(value.get(a)!=SUCCESS) return false;
2324
- for(auto elem:a){
2325
- simdjson::ondemand::value v; if(elem.get(v)!=SUCCESS) return false;
2326
- if(!od_exec(p,p.subs[c.a],v)) return false;
2327
- }
2328
- } break;
2329
- case cg::op::CHECK_REQUIRED: if(t==json_type::object){
2330
- simdjson::ondemand::object o; if(value.get(o)!=SUCCESS) return false;
2331
- auto f = o.find_field_unordered(p.strings[c.a]);
2332
- if(f.error()) return false;
2333
- } break;
2334
- case cg::op::CHECK_MIN_PROPS: if(t==json_type::object){
2335
- simdjson::ondemand::object o; if(value.get(o)!=SUCCESS) return false;
2336
- uint64_t n=0; for(auto f:o){(void)f;++n;} if(n<c.a) return false;
2337
- } break;
2338
- case cg::op::CHECK_MAX_PROPS: if(t==json_type::object){
2339
- simdjson::ondemand::object o; if(value.get(o)!=SUCCESS) return false;
2340
- uint64_t n=0; for(auto f:o){(void)f;++n;} if(n>c.a) return false;
2341
- } break;
2342
- case cg::op::OBJ_PROPS_START: if(t==json_type::object){
2343
- simdjson::ondemand::object o; if(value.get(o)!=SUCCESS) return false;
2344
- struct pd{std::string_view nm;uint32_t si;};
2345
- std::vector<pd> props; bool no_add=false;
2346
- size_t j=i+1;
2347
- for(;j<code.size()&&code[j].o!=cg::op::OBJ_PROPS_END;++j){
2348
- if(code[j].o==cg::op::OBJ_PROP) props.push_back({p.strings[code[j].a],code[j].b});
2349
- else if(code[j].o==cg::op::CHECK_NO_ADDITIONAL) no_add=true;
2350
- }
2351
- for(auto field:o){
2352
- simdjson::ondemand::raw_json_string rk; if(field.key().get(rk)!=SUCCESS) return false;
2353
- std::string_view key;
2354
- if (field.unescaped_key().get(key)) continue;
2355
- bool matched=false;
2356
- for(auto& pp:props){
2357
- if(key==pp.nm){
2358
- simdjson::ondemand::value fv; if(field.value().get(fv)!=SUCCESS) return false;
2359
- if(!od_exec(p,p.subs[pp.si],fv)) return false;
2360
- matched=true; break;
2361
- }
2362
- }
2363
- if(!matched&&no_add) return false;
2364
- }
2365
- i=j; break;
2366
- } else { size_t j=i+1; for(;j<code.size()&&code[j].o!=cg::op::OBJ_PROPS_END;++j); i=j; } break;
2367
- case cg::op::OBJ_PROP: case cg::op::OBJ_PROPS_END: case cg::op::CHECK_NO_ADDITIONAL: break;
2368
-
2369
- // These require full materialization — bail to DOM path
2370
- case cg::op::CHECK_UNIQUE_ITEMS:
2371
- case cg::op::CHECK_ENUM_STR:
2372
- case cg::op::CHECK_ENUM:
2373
- case cg::op::CHECK_CONST:
2374
- case cg::op::COMPOSITION:
2375
- return false;
2376
- }
2377
- }
2378
- return true;
2379
- }
2380
-
2381
- // Determine if a codegen plan can use On Demand (no enum/const/uniqueItems)
2382
- static bool plan_supports_ondemand(const cg::plan& p) {
2383
- for (auto& c : p.code) {
2384
- if (c.o == cg::op::CHECK_UNIQUE_ITEMS || c.o == cg::op::CHECK_ENUM_STR ||
2385
- c.o == cg::op::CHECK_ENUM || c.o == cg::op::CHECK_CONST ||
2386
- c.o == cg::op::COMPOSITION)
2387
- return false;
2388
- }
2389
- // Also check sub-plans
2390
- for (auto& sub : p.subs) {
2391
- for (auto& c : sub) {
2392
- if (c.o == cg::op::CHECK_UNIQUE_ITEMS || c.o == cg::op::CHECK_ENUM_STR ||
2393
- c.o == cg::op::CHECK_ENUM || c.o == cg::op::CHECK_CONST ||
2394
- c.o == cg::op::COMPOSITION)
2395
- return false;
2396
- }
2397
- }
2398
- return true;
2399
- }
2400
-
2401
- // Free padding: check if buffer is near a page boundary
2402
- // On modern systems, pages are at least 4096 bytes. If we're far enough
2403
- // from the end of a page, we can read 64 bytes beyond without a fault.
2404
- static long get_page_size() {
2405
- #ifdef _WIN32
2406
- SYSTEM_INFO si; GetSystemInfo(&si); return si.dwPageSize;
2407
- #else
2408
- static long ps = sysconf(_SC_PAGESIZE);
2409
- return ps;
2410
- #endif
2411
- }
2412
-
2413
- static bool near_page_boundary(const char* buf, size_t len) {
2414
- return ((reinterpret_cast<uintptr_t>(buf + len - 1) % get_page_size())
2415
- + REQUIRED_PADDING >= static_cast<uintptr_t>(get_page_size()));
2416
- }
2417
-
2418
- // Zero-copy validate with free padding (Lemire's trick).
2419
- // Almost never allocates — only if buffer is near a page boundary.
2420
- static simdjson::padded_string_view get_free_padded_view(
2421
- const char* data, size_t length, simdjson::padded_string& fallback) {
2422
- if (near_page_boundary(data, length)) {
2423
- // Rare: near page boundary, must copy
2424
- fallback = simdjson::padded_string(data, length);
2425
- return fallback;
2426
- }
2427
- // Common: free padding available, zero-copy
2428
- return simdjson::padded_string_view(data, length, length + REQUIRED_PADDING);
2429
- }
2430
-
2431
- // Recognize common digit-only regex patterns: ^[0-9]+$, ^[0-9]{N}$, ^[0-9]{N,M}$
2432
- // (and \d variants). Returns true if recognized, fills min_n/max_n.
2433
- static bool parse_digit_pattern(const std::string& p,
2434
- uint32_t& min_n, uint32_t& max_n) {
2435
- if (p.size() < 6) return false;
2436
- if (p.front() != '^' || p.back() != '$') return false;
2437
- std::string_view body(p.data() + 1, p.size() - 2);
2438
- size_t pos = 0;
2439
- if (body.size() >= 5 && body.compare(0, 5, "[0-9]") == 0) {
2440
- pos = 5;
2441
- } else if (body.size() >= 2 && body[0] == '\\' && body[1] == 'd') {
2442
- pos = 2;
2443
- } else {
2444
- return false;
2445
- }
2446
- if (pos >= body.size()) return false;
2447
- char q = body[pos];
2448
- if (q == '+' && pos + 1 == body.size()) {
2449
- min_n = 1; max_n = UINT32_MAX; return true;
2450
- }
2451
- if (q == '*' && pos + 1 == body.size()) {
2452
- min_n = 0; max_n = UINT32_MAX; return true;
2453
- }
2454
- if (q != '{') return false;
2455
- size_t close = body.find('}', pos);
2456
- if (close == std::string_view::npos || close + 1 != body.size()) return false;
2457
- auto inner = body.substr(pos + 1, close - pos - 1);
2458
- if (inner.empty()) return false;
2459
- size_t comma = inner.find(',');
2460
- auto parse_num = [](std::string_view s, uint32_t& out) -> bool {
2461
- if (s.empty()) return false;
2462
- uint64_t v = 0;
2463
- for (char c : s) {
2464
- if (c < '0' || c > '9') return false;
2465
- v = v * 10 + (c - '0');
2466
- if (v > UINT32_MAX) return false;
2467
- }
2468
- out = static_cast<uint32_t>(v);
2469
- return true;
2470
- };
2471
- if (comma == std::string_view::npos) {
2472
- if (!parse_num(inner, min_n)) return false;
2473
- max_n = min_n;
2474
- } else {
2475
- auto a = inner.substr(0, comma);
2476
- auto b = inner.substr(comma + 1);
2477
- if (!parse_num(a, min_n)) return false;
2478
- if (b.empty()) max_n = UINT32_MAX;
2479
- else if (!parse_num(b, max_n)) return false;
2480
- }
2481
- return true;
2482
- }
2483
-
2484
- // Build an od_plan from a schema_node tree.
2485
- static od_plan_ptr compile_od_plan(const schema_node_ptr& node) {
2486
- if (!node) return nullptr;
2487
-
2488
- auto plan = std::make_shared<od_plan>();
2489
-
2490
- if (node->boolean_schema.has_value()) {
2491
- if (!node->boolean_schema.value()) plan->supported = false;
2492
- return plan;
2493
- }
2494
-
2495
- // Unsupported features → fall back to DOM
2496
- if (!node->ref.empty() ||
2497
- node->const_value_raw.has_value() ||
2498
- node->unique_items ||
2499
- !node->all_of.empty() ||
2500
- !node->any_of.empty() ||
2501
- !node->one_of.empty() ||
2502
- node->not_schema ||
2503
- node->if_schema ||
2504
- node->contains_schema ||
2505
- !node->prefix_items.empty() ||
2506
- !node->pattern_properties.empty() ||
2507
- !node->dependent_required.empty() ||
2508
- !node->dependent_schemas.empty() ||
2509
- node->property_names_schema ||
2510
- node->additional_properties_schema) {
2511
- plan->supported = false;
2512
- return plan;
2513
- }
2514
-
2515
- // Enum: handle primitive enums on the on-demand path. Complex enum values
2516
- // (objects, arrays, escaped strings) fall back to DOM.
2517
- if (!node->enum_values_minified.empty()) {
2518
- auto ec = std::make_shared<od_plan::enum_constraint>();
2519
- for (const auto& ev : node->enum_values_minified) {
2520
- if (ev.empty()) { plan->supported = false; return plan; }
2521
- char c = ev[0];
2522
- if (c == '"') {
2523
- if (ev.size() < 2 || ev.back() != '"') { plan->supported = false; return plan; }
2524
- bool has_escape = false;
2525
- for (size_t i = 1; i + 1 < ev.size(); i++) {
2526
- if (ev[i] == '\\') { has_escape = true; break; }
2527
- }
2528
- if (has_escape) { plan->supported = false; return plan; }
2529
- ec->strings.push_back(ev.substr(1, ev.size() - 2));
2530
- } else if (c == '-' || (c >= '0' && c <= '9')) {
2531
- bool is_int = true;
2532
- for (size_t i = (c == '-' ? 1 : 0); i < ev.size(); i++) {
2533
- if (ev[i] < '0' || ev[i] > '9') { is_int = false; break; }
2534
- }
2535
- if (is_int) {
2536
- try { ec->integers.push_back(std::stoll(ev)); }
2537
- catch (...) { plan->supported = false; return plan; }
2538
- } else {
2539
- try { ec->doubles.push_back(std::stod(ev)); }
2540
- catch (...) { plan->supported = false; return plan; }
2541
- }
2542
- } else if (ev == "true") {
2543
- ec->has_true = true;
2544
- } else if (ev == "false") {
2545
- ec->has_false = true;
2546
- } else if (ev == "null") {
2547
- ec->has_null = true;
2548
- } else {
2549
- plan->supported = false;
2550
- return plan;
2551
- }
2552
- }
2553
- plan->enum_check = std::move(ec);
2554
- }
2555
-
2556
- plan->type_mask = node->type_mask;
2557
- if (node->minimum) { plan->num_flags |= od_plan::HAS_MIN; plan->num_min = *node->minimum; }
2558
- if (node->maximum) { plan->num_flags |= od_plan::HAS_MAX; plan->num_max = *node->maximum; }
2559
- if (node->exclusive_minimum) { plan->num_flags |= od_plan::HAS_EX_MIN; plan->num_ex_min = *node->exclusive_minimum; }
2560
- if (node->exclusive_maximum) { plan->num_flags |= od_plan::HAS_EX_MAX; plan->num_ex_max = *node->exclusive_maximum; }
2561
- if (node->multiple_of) { plan->num_flags |= od_plan::HAS_MUL; plan->num_mul = *node->multiple_of; }
2562
- plan->min_length = node->min_length;
2563
- plan->max_length = node->max_length;
2564
- #ifndef ATA_NO_RE2
2565
- plan->pattern = node->compiled_pattern.get();
2566
- #endif
2567
- // Try to recognize the pattern as an inlined digit check; if so, skip RE2.
2568
- if (node->pattern.has_value()) {
2569
- uint32_t min_n, max_n;
2570
- if (parse_digit_pattern(*node->pattern, min_n, max_n)) {
2571
- plan->digit_pattern = od_plan::digit_pattern_t{min_n, max_n};
2572
- #ifndef ATA_NO_RE2
2573
- plan->pattern = nullptr;
2574
- #endif
2575
- }
2576
- }
2577
- plan->format_id = node->format_id;
2578
-
2579
- // Object plan — build hash lookup for O(1) per-field dispatch
2580
- if (!node->properties.empty() || !node->required.empty() ||
2581
- node->additional_properties_bool.has_value() ||
2582
- node->min_properties.has_value() || node->max_properties.has_value()) {
2583
- auto op = std::make_shared<od_plan::obj_plan>();
2584
- op->required_count = node->required.size();
2585
- op->min_props = node->min_properties;
2586
- op->max_props = node->max_properties;
2587
- if (node->additional_properties_bool.has_value() &&
2588
- !node->additional_properties_bool.value()) {
2589
- op->no_additional = true;
2590
- }
2591
- // Build merged entries: each key appears once with required_idx + sub_plan
2592
- std::unordered_map<std::string, size_t> key_to_idx;
2593
- // Register required keys
2594
- for (size_t i = 0; i < node->required.size() && i < 64; i++) {
2595
- auto& rk = node->required[i];
2596
- if (key_to_idx.find(rk) == key_to_idx.end()) {
2597
- key_to_idx[rk] = op->entries.size();
2598
- op->entries.push_back({rk, static_cast<int>(i), nullptr});
2599
- } else {
2600
- op->entries[key_to_idx[rk]].required_idx = static_cast<int>(i);
2601
- }
2602
- }
2603
- // Register properties + compile sub-plans
2604
- for (auto& [key, sub_node] : node->properties) {
2605
- auto sub = compile_od_plan(sub_node);
2606
- if (!sub || !sub->supported) { plan->supported = false; return plan; }
2607
- auto it = key_to_idx.find(key);
2608
- if (it != key_to_idx.end()) {
2609
- op->entries[it->second].sub = std::move(sub);
2610
- } else {
2611
- key_to_idx[key] = op->entries.size();
2612
- op->entries.push_back({key, -1, std::move(sub), od_plan::fast_kind::OTHER});
2613
- }
2614
- }
2615
- // Compute fast_kind for each entry post-hoc — lets the obj iterator
2616
- // skip the recursive od_exec_plan call (and its type().get + switch)
2617
- // for primitive properties, and also skip type detection for nested
2618
- // objects / primitive arrays.
2619
- for (auto& e : op->entries) {
2620
- if (!e.sub) continue;
2621
- // Nested object: dispatch directly to value.get(obj) + iteration helper.
2622
- if (e.sub->object && !e.sub->array) {
2623
- e.fk = od_plan::fast_kind::OBJECT;
2624
- continue;
2625
- }
2626
- // Array: dispatch directly to value.get(arr) + element iteration helper.
2627
- if (e.sub->array && !e.sub->object) {
2628
- e.fk = od_plan::fast_kind::ARRAY;
2629
- continue;
2630
- }
2631
- if (e.sub->object || e.sub->array) continue; // unusual: both, skip
2632
- uint8_t m = e.sub->type_mask;
2633
- uint8_t s_bit = json_type_bit(json_type::string);
2634
- uint8_t i_bit = json_type_bit(json_type::integer);
2635
- uint8_t n_bit = json_type_bit(json_type::number);
2636
- uint8_t b_bit = json_type_bit(json_type::boolean);
2637
- if (m == s_bit) e.fk = od_plan::fast_kind::STRING;
2638
- else if (m == i_bit || m == (i_bit | n_bit)) e.fk = od_plan::fast_kind::INTEGER;
2639
- else if (m == b_bit) e.fk = od_plan::fast_kind::BOOLEAN;
2640
- else if (m == 0 && e.sub->enum_check) {
2641
- // Untyped enum: infer from enum value shapes
2642
- auto& ec = *e.sub->enum_check;
2643
- bool only_str = !ec.strings.empty() && ec.integers.empty() && ec.doubles.empty()
2644
- && !ec.has_null && !ec.has_true && !ec.has_false;
2645
- bool only_int = ec.strings.empty() && !ec.integers.empty() && ec.doubles.empty()
2646
- && !ec.has_null && !ec.has_true && !ec.has_false;
2647
- bool only_bool = ec.strings.empty() && ec.integers.empty() && ec.doubles.empty()
2648
- && !ec.has_null && (ec.has_true || ec.has_false);
2649
- if (only_str) e.fk = od_plan::fast_kind::STRING;
2650
- else if (only_int) e.fk = od_plan::fast_kind::INTEGER;
2651
- else if (only_bool) e.fk = od_plan::fast_kind::BOOLEAN;
2652
- }
2653
- }
2654
- plan->object = std::move(op);
2655
- }
2656
-
2657
- // Array plan
2658
- if (node->items_schema || node->min_items.has_value() || node->max_items.has_value()) {
2659
- auto ap = std::make_shared<od_plan::arr_plan>();
2660
- ap->min_items = node->min_items;
2661
- ap->max_items = node->max_items;
2662
- if (node->items_schema) {
2663
- ap->items = compile_od_plan(node->items_schema);
2664
- if (!ap->items || !ap->items->supported) { plan->supported = false; return plan; }
2665
- }
2666
- plan->array = std::move(ap);
2667
- }
2668
-
2669
- return plan;
2670
- }
2671
-
2672
- // Fast ASCII check: if all bytes < 0x80, byte length == codepoint length
2673
- static inline uint64_t utf8_length_fast(std::string_view s) {
2674
- // Check 8 bytes at a time for non-ASCII
2675
- const uint8_t* p = reinterpret_cast<const uint8_t*>(s.data());
2676
- size_t n = s.size();
2677
- size_t i = 0;
2678
- uint64_t has_high = 0;
2679
- for (; i + 8 <= n; i += 8) {
2680
- uint64_t block;
2681
- std::memcpy(&block, p + i, 8);
2682
- has_high |= block & 0x8080808080808080ULL;
2683
- }
2684
- for (; i < n; i++) has_high |= p[i] & 0x80;
2685
- if (has_high == 0) return n; // Pure ASCII — byte count == codepoint count
2686
- return utf8_length(s); // Fallback to full counting
2687
- }
2688
-
2689
- // Execute an od_plan against a simdjson On-Demand value.
2690
- // Each value consumed exactly once. Uses simdjson types directly — no od_type() overhead.
2691
- // `pre_type` is an optional caller-supplied type hint; when set, the value.type().get
2692
- // simdjson call is skipped (saves ~3-5 ns per recursion). Used by fast_kind::OBJECT
2693
- // / ARRAY dispatch where the type is already established at compile time.
2694
- static bool od_exec_plan(const od_plan& plan, simdjson::ondemand::value value,
2695
- std::optional<simdjson::ondemand::json_type> pre_type = std::nullopt) {
2696
- using sjt = simdjson::ondemand::json_type;
2697
- sjt st;
2698
- if (pre_type) {
2699
- st = *pre_type;
2700
- } else {
2701
- if (value.type().get(st) != SUCCESS) return false;
2702
- }
2703
-
2704
- // Type check using simdjson type directly
2705
- if (plan.type_mask) {
2706
- uint8_t tbits;
2707
- switch (st) {
2708
- case sjt::string: tbits = json_type_bit(json_type::string); break;
2709
- case sjt::boolean: tbits = json_type_bit(json_type::boolean); break;
2710
- case sjt::null: tbits = json_type_bit(json_type::null_value); break;
2711
- case sjt::object: tbits = json_type_bit(json_type::object); break;
2712
- case sjt::array: tbits = json_type_bit(json_type::array); break;
2713
- case sjt::number:
2714
- // Only call get_number_type when schema has type constraint that distinguishes int/number
2715
- tbits = json_type_bit(json_type::number) | json_type_bit(json_type::integer);
2716
- if ((plan.type_mask & tbits) != tbits) {
2717
- // Schema distinguishes — need to check actual number type
2718
- simdjson::ondemand::number_type nt;
2719
- if (value.get_number_type().get(nt) == SUCCESS &&
2720
- nt != simdjson::ondemand::number_type::floating_point_number)
2721
- tbits = json_type_bit(json_type::integer) | json_type_bit(json_type::number);
2722
- else
2723
- tbits = json_type_bit(json_type::number);
2724
- }
2725
- break;
2726
- default: tbits = 0;
2727
- }
2728
- if (!(tbits & plan.type_mask)) return false;
2729
- }
2730
-
2731
- switch (st) {
2732
- case sjt::number: {
2733
- bool need_value = plan.num_flags || plan.enum_check;
2734
- if (!need_value) break;
2735
- double v;
2736
- int64_t iv;
2737
- bool got_int = (value.get(iv) == SUCCESS);
2738
- if (got_int) {
2739
- v = static_cast<double>(iv);
2740
- } else if (value.get(v) != SUCCESS) {
2741
- return false;
2742
- }
2743
- uint8_t f = plan.num_flags;
2744
- if ((f & od_plan::HAS_MIN) && v < plan.num_min) return false;
2745
- if ((f & od_plan::HAS_MAX) && v > plan.num_max) return false;
2746
- if ((f & od_plan::HAS_EX_MIN) && v <= plan.num_ex_min) return false;
2747
- if ((f & od_plan::HAS_EX_MAX) && v >= plan.num_ex_max) return false;
2748
- if (f & od_plan::HAS_MUL) {
2749
- double r = std::fmod(v, plan.num_mul);
2750
- if (std::abs(r) > 1e-8 && std::abs(r - plan.num_mul) > 1e-8) return false;
2751
- }
2752
- if (plan.enum_check) {
2753
- auto& ec = *plan.enum_check;
2754
- bool match = false;
2755
- if (got_int) {
2756
- for (auto i : ec.integers) if (i == iv) { match = true; break; }
2757
- }
2758
- if (!match) {
2759
- for (auto d : ec.doubles) if (d == v) { match = true; break; }
2760
- }
2761
- if (!match && got_int) {
2762
- for (auto d : ec.doubles) if (d == v) { match = true; break; }
2763
- }
2764
- if (!match) return false;
2765
- }
2766
- break;
2767
- }
2768
- case sjt::string: {
2769
- std::string_view sv;
2770
- if (value.get(sv) != SUCCESS) return false;
2771
- if (plan.min_length || plan.max_length) {
2772
- uint64_t len = utf8_length_fast(sv);
2773
- if (plan.min_length && len < *plan.min_length) return false;
2774
- if (plan.max_length && len > *plan.max_length) return false;
2775
- }
2776
- if (plan.digit_pattern) {
2777
- auto& dp = *plan.digit_pattern;
2778
- if (sv.size() < dp.min_len || sv.size() > dp.max_len) return false;
2779
- const uint8_t* p = reinterpret_cast<const uint8_t*>(sv.data());
2780
- for (size_t i = 0, n = sv.size(); i < n; i++) {
2781
- if (p[i] < '0' || p[i] > '9') return false;
2782
- }
2783
- }
2784
- #ifndef ATA_NO_RE2
2785
- else if (plan.pattern) {
2786
- if (!re2::RE2::PartialMatch(re2::StringPiece(sv.data(), sv.size()), *plan.pattern))
2787
- return false;
2788
- }
2789
- #endif
2790
- if (plan.format_id != 255) {
2791
- if (!check_format_by_id(sv, plan.format_id)) return false;
2792
- }
2793
- if (plan.enum_check) {
2794
- auto& ec = *plan.enum_check;
2795
- bool match = false;
2796
- for (auto& s : ec.strings) {
2797
- if (sv.size() == s.size() && std::memcmp(sv.data(), s.data(), s.size()) == 0) {
2798
- match = true;
2799
- break;
2800
- }
2801
- }
2802
- if (!match) return false;
2803
- }
2804
- break;
2805
- }
2806
- case sjt::object: {
2807
- if (!plan.object) break;
2808
- auto& op = *plan.object;
2809
- simdjson::ondemand::object obj;
2810
- if (value.get(obj) != SUCCESS) return false;
2811
-
2812
- uint64_t required_found = 0;
2813
- uint64_t prop_count = 0;
2814
-
2815
- for (auto field : obj) {
2816
- // Fast key compare: use raw_json_string against schema keys directly,
2817
- // skipping the unescape pass. Schema keys are assumed to have no
2818
- // unescaped quotes (true for any well-formed JSON Schema).
2819
- simdjson::ondemand::raw_json_string raw_key;
2820
- if (field.key().get(raw_key) != SUCCESS) continue;
2821
- prop_count++;
2822
-
2823
- bool matched = false;
2824
- for (auto& e : op.entries) {
2825
- if (raw_key.unsafe_is_equal(e.key)) {
2826
- if (e.required_idx >= 0)
2827
- required_found |= (1ULL << e.required_idx);
2828
- if (e.sub) {
2829
- // Fast dispatch: skip recursive od_exec_plan + type().get + switch
2830
- // for primitive sub-plans. Mirrors the constraint code in od_exec_plan.
2831
- switch (e.fk) {
2832
- case od_plan::fast_kind::INTEGER: {
2833
- int64_t iv;
2834
- if (field.value().get(iv) != SUCCESS) return false;
2835
- auto& sub = *e.sub;
2836
- uint8_t f = sub.num_flags;
2837
- double v = static_cast<double>(iv);
2838
- if ((f & od_plan::HAS_MIN) && v < sub.num_min) return false;
2839
- if ((f & od_plan::HAS_MAX) && v > sub.num_max) return false;
2840
- if ((f & od_plan::HAS_EX_MIN) && v <= sub.num_ex_min) return false;
2841
- if ((f & od_plan::HAS_EX_MAX) && v >= sub.num_ex_max) return false;
2842
- if (f & od_plan::HAS_MUL) {
2843
- double r = std::fmod(v, sub.num_mul);
2844
- if (std::abs(r) > 1e-8 && std::abs(r - sub.num_mul) > 1e-8) return false;
2845
- }
2846
- if (sub.enum_check) {
2847
- bool em = false;
2848
- for (auto i2 : sub.enum_check->integers) if (i2 == iv) { em = true; break; }
2849
- if (!em) for (auto d : sub.enum_check->doubles) if (d == v) { em = true; break; }
2850
- if (!em) return false;
2851
- }
2852
- break;
2853
- }
2854
- case od_plan::fast_kind::STRING: {
2855
- std::string_view sv;
2856
- if (field.value().get(sv) != SUCCESS) return false;
2857
- auto& sub = *e.sub;
2858
- if (sub.min_length || sub.max_length) {
2859
- uint64_t len = utf8_length_fast(sv);
2860
- if (sub.min_length && len < *sub.min_length) return false;
2861
- if (sub.max_length && len > *sub.max_length) return false;
2862
- }
2863
- if (sub.digit_pattern) {
2864
- auto& dp = *sub.digit_pattern;
2865
- if (sv.size() < dp.min_len || sv.size() > dp.max_len) return false;
2866
- const uint8_t* sp = reinterpret_cast<const uint8_t*>(sv.data());
2867
- for (size_t i = 0, n = sv.size(); i < n; i++) {
2868
- if (sp[i] < '0' || sp[i] > '9') return false;
2869
- }
2870
- }
2871
- #ifndef ATA_NO_RE2
2872
- else if (sub.pattern) {
2873
- if (!re2::RE2::PartialMatch(re2::StringPiece(sv.data(), sv.size()), *sub.pattern))
2874
- return false;
2875
- }
2876
- #endif
2877
- if (sub.format_id != 255) {
2878
- if (!check_format_by_id(sv, sub.format_id)) return false;
2879
- }
2880
- if (sub.enum_check) {
2881
- bool em = false;
2882
- for (auto& s : sub.enum_check->strings) {
2883
- if (sv.size() == s.size() && std::memcmp(sv.data(), s.data(), s.size()) == 0) {
2884
- em = true;
2885
- break;
2886
- }
2887
- }
2888
- if (!em) return false;
2889
- }
2890
- break;
2891
- }
2892
- case od_plan::fast_kind::BOOLEAN: {
2893
- bool bv;
2894
- if (field.value().get(bv) != SUCCESS) return false;
2895
- if (e.sub->enum_check) {
2896
- if (bv ? !e.sub->enum_check->has_true : !e.sub->enum_check->has_false) return false;
2897
- }
2898
- break;
2899
- }
2900
- case od_plan::fast_kind::OBJECT: {
2901
- simdjson::ondemand::value fv;
2902
- if (field.value().get(fv) != SUCCESS) return false;
2903
- if (!od_exec_plan(*e.sub, fv,
2904
- simdjson::ondemand::json_type::object)) return false;
2905
- break;
2906
- }
2907
- case od_plan::fast_kind::ARRAY: {
2908
- simdjson::ondemand::value fv;
2909
- if (field.value().get(fv) != SUCCESS) return false;
2910
- if (!od_exec_plan(*e.sub, fv,
2911
- simdjson::ondemand::json_type::array)) return false;
2912
- break;
2913
- }
2914
- case od_plan::fast_kind::OTHER:
2915
- default: {
2916
- simdjson::ondemand::value fv;
2917
- if (field.value().get(fv) != SUCCESS) return false;
2918
- if (!od_exec_plan(*e.sub, fv)) return false;
2919
- }
2920
- }
2921
- }
2922
- matched = true;
2923
- break;
2924
- }
2925
- }
2926
- // Safety net: if no match via raw byte compare, the JSON key may be
2927
- // escaped (e.g., "aname"). Fall back to a properly-unescaped
2928
- // compare so escaped keys still match the unescaped schema keys.
2929
- if (!matched) {
2930
- std::string_view raw_view = field.escaped_key();
2931
- if (raw_view.find('\\') != std::string_view::npos) {
2932
- std::string_view ukey;
2933
- if (field.unescaped_key().get(ukey) == SUCCESS) {
2934
- for (auto& e : op.entries) {
2935
- if (ukey == e.key) {
2936
- if (e.required_idx >= 0)
2937
- required_found |= (1ULL << e.required_idx);
2938
- if (e.sub) {
2939
- simdjson::ondemand::value fv;
2940
- if (field.value().get(fv) != SUCCESS) return false;
2941
- if (!od_exec_plan(*e.sub, fv)) return false;
2942
- }
2943
- matched = true;
2944
- break;
2945
- }
2946
- }
2947
- }
2948
- }
2949
- }
2950
- if (!matched && op.no_additional) return false;
2951
- }
2952
-
2953
- uint64_t required_mask = (op.required_count >= 64)
2954
- ? ~0ULL : ((1ULL << op.required_count) - 1);
2955
- if ((required_found & required_mask) != required_mask) return false;
2956
- if (op.min_props && prop_count < *op.min_props) return false;
2957
- if (op.max_props && prop_count > *op.max_props) return false;
2958
- break;
2959
- }
2960
- case sjt::array: {
2961
- if (!plan.array) break;
2962
- auto& ap = *plan.array;
2963
- simdjson::ondemand::array arr;
2964
- if (value.get(arr) != SUCCESS) return false;
2965
-
2966
- uint64_t count = 0;
2967
- for (auto elem : arr) {
2968
- simdjson::ondemand::value v;
2969
- if (elem.get(v) != SUCCESS) return false;
2970
- if (ap.items && !od_exec_plan(*ap.items, v)) return false;
2971
- count++;
2972
- }
2973
- if (ap.min_items && count < *ap.min_items) return false;
2974
- if (ap.max_items && count > *ap.max_items) return false;
2975
- break;
2976
- }
2977
- case sjt::boolean: {
2978
- if (plan.enum_check) {
2979
- bool b;
2980
- if (value.get(b) != SUCCESS) return false;
2981
- if (b ? !plan.enum_check->has_true : !plan.enum_check->has_false) return false;
2982
- }
2983
- break;
2984
- }
2985
- case sjt::null: {
2986
- if (plan.enum_check && !plan.enum_check->has_null) return false;
2987
- break;
2988
- }
2989
- default:
2990
- break;
2991
- }
2992
-
2993
- return true;
2994
- }
2995
-
2996
- schema_ref compile(std::string_view schema_json) {
2997
- auto ctx = std::make_shared<compiled_schema>();
2998
- ctx->raw_schema = std::string(schema_json);
2999
-
3000
- dom::element doc;
3001
- auto result = ctx->parser.parse(ctx->raw_schema);
3002
- if (result.error()) {
3003
- return schema_ref{nullptr};
3004
- }
3005
- doc = result.value_unsafe();
3006
-
3007
- ctx->root = compile_node(doc, *ctx);
3008
-
3009
- if (!ctx->compile_error.empty()) {
3010
- return schema_ref{nullptr};
3011
- }
3012
-
3013
- // Generate codegen plan
3014
- cg_compile(ctx->root.get(), ctx->gen_plan, ctx->gen_plan.code);
3015
- ctx->gen_plan.code.push_back({cg::op::END});
3016
- ctx->use_ondemand = plan_supports_ondemand(ctx->gen_plan);
3017
- ctx->od = compile_od_plan(ctx->root);
3018
-
3019
- schema_ref ref;
3020
- ref.impl = ctx;
3021
- ref.warnings = std::move(ctx->warnings);
3022
- return ref;
3023
- }
3024
-
3025
- validation_result validate(const schema_ref& schema, std::string_view json,
3026
- const validate_options& opts) {
3027
- if (!schema.impl || !schema.impl->root) {
3028
- return {false, {{error_code::invalid_schema, "", "schema not compiled"}}};
3029
- }
3030
-
3031
- // Free padding trick: avoid padded_string copy when possible
3032
- simdjson::padded_string fallback;
3033
- auto psv = get_free_padded_view(json.data(), json.size(), fallback);
3034
-
3035
- // Ultra-fast path: On Demand (no DOM materialization)
3036
- static constexpr size_t OD_THRESHOLD = 32;
3037
- if (schema.impl->use_ondemand && !schema.impl->gen_plan.code.empty() &&
3038
- json.size() >= OD_THRESHOLD) {
3039
- auto od_result = tl_od_parser().iterate(psv);
3040
- if (!od_result.error()) {
3041
- simdjson::ondemand::value root_val;
3042
- if (od_result.get_value().get(root_val) == SUCCESS) {
3043
- if (od_exec(schema.impl->gen_plan, schema.impl->gen_plan.code, root_val)) {
3044
- return {true, {}};
3045
- }
3046
- }
3047
- }
3048
- // Need fresh view for DOM parse (On Demand consumed it)
3049
- psv = get_free_padded_view(json.data(), json.size(), fallback);
3050
- }
3051
-
3052
- auto& dom_p = tl_dom_parser();
3053
- auto result = dom_p.parse(psv);
3054
- if (result.error()) {
3055
- return {false, {{error_code::invalid_json, "", "invalid JSON document"}}};
3056
- }
3057
-
3058
- // Fast path: codegen bytecode execution (DOM)
3059
- if (!schema.impl->use_ondemand && !schema.impl->gen_plan.code.empty()) {
3060
- if (cg_exec(schema.impl->gen_plan, schema.impl->gen_plan.code,
3061
- result.value_unsafe())) {
3062
- return {true, {}};
3063
- }
3064
- // Codegen said invalid OR hit COMPOSITION — fall through to tree walker
3065
- }
3066
-
3067
- // Slow path: tree walker with error details (reuse already-parsed DOM)
3068
- std::vector<validation_error> errors;
3069
- if (schema.impl->has_dynamic_refs) {
3070
- dynamic_scope_t scope;
3071
- auto rit = schema.impl->resource_dynamic_anchors.find("");
3072
- if (rit != schema.impl->resource_dynamic_anchors.end()) {
3073
- scope.push_back(&rit->second);
3074
- }
3075
- if (!schema.impl->root->id.empty()) {
3076
- auto iit = schema.impl->resource_dynamic_anchors.find(schema.impl->root->id);
3077
- if (iit != schema.impl->resource_dynamic_anchors.end()) {
3078
- scope.push_back(&iit->second);
3079
- }
3080
- }
3081
- validate_node(schema.impl->root, result.value_unsafe(), "", *schema.impl, errors,
3082
- opts.all_errors, &scope);
3083
- } else {
3084
- validate_node(schema.impl->root, result.value_unsafe(), "", *schema.impl, errors,
3085
- opts.all_errors);
3086
- }
3087
-
3088
- return {errors.empty(), std::move(errors)};
3089
- }
3090
-
3091
- validation_result validate(std::string_view schema_json,
3092
- std::string_view json,
3093
- const validate_options& opts) {
3094
- auto s = compile(schema_json);
3095
- if (!s) {
3096
- return {false, {{error_code::invalid_schema, "", "failed to compile schema"}}};
3097
- }
3098
- return validate(s, json, opts);
3099
- }
3100
-
3101
-
3102
- bool is_valid_prepadded(const schema_ref& schema, const char* data, size_t length) {
3103
- if (!schema.impl || !schema.impl->root) return false;
3104
-
3105
- simdjson::padded_string fallback;
3106
- auto psv = get_free_padded_view(data, length, fallback);
3107
-
3108
- // On-Demand fast path: skip DOM parse entirely
3109
- // Minimum 32 bytes — On-Demand doesn't fully validate small malformed docs
3110
- if (schema.impl->od && schema.impl->od->supported && length >= 32) {
3111
- auto od_result = tl_od_parser().iterate(psv);
3112
- if (!od_result.error()) {
3113
- simdjson::ondemand::value root_val;
3114
- if (od_result.get_value().get(root_val) == SUCCESS) {
3115
- if (od_exec_plan(*schema.impl->od, root_val)) {
3116
- return true;
3117
- }
3118
- }
3119
- }
3120
- psv = get_free_padded_view(data, length, fallback);
3121
- }
3122
-
3123
- auto result = tl_dom_parser().parse(psv);
3124
- if (result.error()) return false;
3125
-
3126
- if (!schema.impl->gen_plan.code.empty()) {
3127
- return cg_exec(schema.impl->gen_plan, schema.impl->gen_plan.code, result.value_unsafe());
3128
- }
3129
-
3130
- return validate_fast(schema.impl->root, result.value_unsafe(), *schema.impl);
3131
- }
3132
-
3133
- bool is_valid_buf(const schema_ref& schema, const uint8_t* data, size_t length) {
3134
- if (!schema.impl || !schema.impl->root || !data || length == 0) return false;
3135
-
3136
- // Thread-local buffer with simdjson padding — reused across calls
3137
- thread_local std::string tl_buf;
3138
- const size_t needed = length + REQUIRED_PADDING;
3139
- if (tl_buf.size() < needed) tl_buf.resize(needed);
3140
- std::memcpy(tl_buf.data(), data, length);
3141
- std::memset(tl_buf.data() + length, 0, REQUIRED_PADDING);
3142
-
3143
- return is_valid_prepadded(schema, tl_buf.data(), length);
3144
- }
3145
-
3146
- } // namespace ata