ata-validator 0.7.3 → 0.8.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/CMakeLists.txt +41 -23
- package/README.md +1 -1
- package/binding/ata_napi.cpp +32 -4
- package/index.js +53 -23
- package/lib/js-compiler.js +404 -34
- package/package.json +3 -2
- package/prebuilds/ata-darwin-arm64/node-napi-v10.node +0 -0
- package/prebuilds/darwin-arm64/ata-validator.node +0 -0
- package/src/ata.cpp +536 -147
- package/prebuilds/ata-linux-arm64/node-napi-v10.node +0 -0
- package/prebuilds/ata-linux-x64/node-napi-v10.node +0 -0
package/src/ata.cpp
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
#include <algorithm>
|
|
9
9
|
#include <cmath>
|
|
10
10
|
#include <cstring>
|
|
11
|
+
#ifndef ATA_NO_RE2
|
|
11
12
|
#include <re2/re2.h>
|
|
13
|
+
#endif
|
|
12
14
|
#include <set>
|
|
13
15
|
#include <unordered_map>
|
|
14
16
|
|
|
@@ -53,11 +55,15 @@ static bool fast_check_email(std::string_view s) {
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
static bool fast_check_date(std::string_view s) {
|
|
56
|
-
// YYYY-MM-DD
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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;
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
static bool fast_check_time(std::string_view s) {
|
|
@@ -274,7 +280,9 @@ struct schema_node {
|
|
|
274
280
|
std::optional<uint64_t> min_length;
|
|
275
281
|
std::optional<uint64_t> max_length;
|
|
276
282
|
std::optional<std::string> pattern;
|
|
283
|
+
#ifndef ATA_NO_RE2
|
|
277
284
|
std::shared_ptr<re2::RE2> compiled_pattern; // cached compiled regex (RE2)
|
|
285
|
+
#endif
|
|
278
286
|
|
|
279
287
|
// array
|
|
280
288
|
std::optional<uint64_t> min_items;
|
|
@@ -301,7 +309,9 @@ struct schema_node {
|
|
|
301
309
|
struct pattern_prop {
|
|
302
310
|
std::string pattern;
|
|
303
311
|
schema_node_ptr schema;
|
|
312
|
+
#ifndef ATA_NO_RE2
|
|
304
313
|
std::shared_ptr<re2::RE2> compiled;
|
|
314
|
+
#endif
|
|
305
315
|
};
|
|
306
316
|
std::vector<pattern_prop> pattern_properties;
|
|
307
317
|
|
|
@@ -326,6 +336,8 @@ struct schema_node {
|
|
|
326
336
|
|
|
327
337
|
// $ref
|
|
328
338
|
std::string ref;
|
|
339
|
+
std::string dynamic_ref; // $dynamicRef value (e.g. "#items")
|
|
340
|
+
std::string id; // $id — resource boundary marker
|
|
329
341
|
|
|
330
342
|
// $defs — stored on node for pointer navigation
|
|
331
343
|
std::unordered_map<std::string, schema_node_ptr> defs;
|
|
@@ -351,7 +363,9 @@ struct plan {
|
|
|
351
363
|
std::vector<ins> code;
|
|
352
364
|
std::vector<double> doubles;
|
|
353
365
|
std::vector<std::string> strings;
|
|
366
|
+
#ifndef ATA_NO_RE2
|
|
354
367
|
std::vector<std::shared_ptr<re2::RE2>> regexes;
|
|
368
|
+
#endif
|
|
355
369
|
std::vector<std::vector<std::string>> enum_sets;
|
|
356
370
|
std::vector<uint8_t> type_masks;
|
|
357
371
|
std::vector<uint8_t> format_ids;
|
|
@@ -374,7 +388,9 @@ struct od_plan {
|
|
|
374
388
|
|
|
375
389
|
// String — single value.get(sv) then all checks
|
|
376
390
|
std::optional<uint64_t> min_length, max_length;
|
|
391
|
+
#ifndef ATA_NO_RE2
|
|
377
392
|
re2::RE2* pattern = nullptr; // borrowed pointer from schema_node
|
|
393
|
+
#endif
|
|
378
394
|
uint8_t format_id = 255; // 255 = no format check
|
|
379
395
|
|
|
380
396
|
// Object — single iterate with merged required+property lookup
|
|
@@ -408,10 +424,18 @@ struct compiled_schema {
|
|
|
408
424
|
schema_node_ptr root;
|
|
409
425
|
std::unordered_map<std::string, schema_node_ptr> defs;
|
|
410
426
|
std::string raw_schema;
|
|
427
|
+
std::string compile_error; // non-empty if compilation failed
|
|
411
428
|
dom::parser parser; // used only at compile time
|
|
412
429
|
cg::plan gen_plan; // codegen validation plan
|
|
413
430
|
bool use_ondemand = false; // true if codegen plan supports On Demand
|
|
414
431
|
od_plan_ptr od; // On-Demand execution plan
|
|
432
|
+
|
|
433
|
+
// anchor resolution
|
|
434
|
+
std::unordered_map<std::string, schema_node_ptr> anchors;
|
|
435
|
+
std::unordered_map<std::string,
|
|
436
|
+
std::unordered_map<std::string, schema_node_ptr>> resource_dynamic_anchors;
|
|
437
|
+
bool has_dynamic_refs = false;
|
|
438
|
+
std::string current_resource_id; // compile-time only
|
|
415
439
|
};
|
|
416
440
|
|
|
417
441
|
// Thread-local persistent parsers — reused across all validate calls on the
|
|
@@ -462,6 +486,64 @@ static schema_node_ptr compile_node(dom::element el,
|
|
|
462
486
|
}
|
|
463
487
|
}
|
|
464
488
|
|
|
489
|
+
// $id — must come before $anchor/$dynamicAnchor so current_resource_id is set
|
|
490
|
+
std::string prev_resource = ctx.current_resource_id;
|
|
491
|
+
{
|
|
492
|
+
dom::element id_el;
|
|
493
|
+
if (obj["$id"].get(id_el) == SUCCESS) {
|
|
494
|
+
std::string_view sv;
|
|
495
|
+
if (id_el.get(sv) == SUCCESS) {
|
|
496
|
+
node->id = std::string(sv);
|
|
497
|
+
ctx.current_resource_id = node->id;
|
|
498
|
+
ctx.defs[node->id] = node;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// $anchor — register in flat anchor map
|
|
504
|
+
{
|
|
505
|
+
dom::element anchor_el;
|
|
506
|
+
if (obj["$anchor"].get(anchor_el) == SUCCESS) {
|
|
507
|
+
std::string_view sv;
|
|
508
|
+
if (anchor_el.get(sv) == SUCCESS) {
|
|
509
|
+
ctx.anchors[std::string(sv)] = node;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// $dynamicAnchor — register in both flat anchors and per-resource map
|
|
515
|
+
{
|
|
516
|
+
dom::element da_el;
|
|
517
|
+
if (obj["$dynamicAnchor"].get(da_el) == SUCCESS) {
|
|
518
|
+
std::string_view sv;
|
|
519
|
+
if (da_el.get(sv) == SUCCESS) {
|
|
520
|
+
std::string name(sv);
|
|
521
|
+
ctx.anchors[name] = node;
|
|
522
|
+
ctx.resource_dynamic_anchors[ctx.current_resource_id][name] = node;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// $dynamicRef
|
|
528
|
+
{
|
|
529
|
+
dom::element dr_el;
|
|
530
|
+
if (obj["$dynamicRef"].get(dr_el) == SUCCESS) {
|
|
531
|
+
std::string_view sv;
|
|
532
|
+
if (dr_el.get(sv) == SUCCESS) {
|
|
533
|
+
std::string dr_val(sv);
|
|
534
|
+
// If the $dynamicRef starts with "#" (fragment-only) and we're inside
|
|
535
|
+
// a non-root resource, qualify it with the current resource ID so
|
|
536
|
+
// validation can resolve it correctly.
|
|
537
|
+
if (!dr_val.empty() && dr_val[0] == '#' &&
|
|
538
|
+
!ctx.current_resource_id.empty()) {
|
|
539
|
+
dr_val = ctx.current_resource_id + dr_val;
|
|
540
|
+
}
|
|
541
|
+
node->dynamic_ref = dr_val;
|
|
542
|
+
ctx.has_dynamic_refs = true;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
465
547
|
// type
|
|
466
548
|
dom::element type_el;
|
|
467
549
|
if (obj["type"].get(type_el) == SUCCESS) {
|
|
@@ -516,10 +598,15 @@ static schema_node_ptr compile_node(dom::element el,
|
|
|
516
598
|
std::string_view sv;
|
|
517
599
|
if (str_el.get(sv) == SUCCESS) {
|
|
518
600
|
node->pattern = std::string(sv);
|
|
601
|
+
#ifdef ATA_NO_RE2
|
|
602
|
+
ctx.compile_error = "pattern keyword requires RE2 support (built with ATA_NO_RE2)";
|
|
603
|
+
return node;
|
|
604
|
+
#else
|
|
519
605
|
auto re = std::make_shared<re2::RE2>(node->pattern.value());
|
|
520
606
|
if (re->ok()) {
|
|
521
607
|
node->compiled_pattern = std::move(re);
|
|
522
608
|
}
|
|
609
|
+
#endif
|
|
523
610
|
}
|
|
524
611
|
}
|
|
525
612
|
|
|
@@ -636,6 +723,10 @@ static schema_node_ptr compile_node(dom::element el,
|
|
|
636
723
|
dom::element pp_el;
|
|
637
724
|
if (obj["patternProperties"].get(pp_el) == SUCCESS &&
|
|
638
725
|
pp_el.is<dom::object>()) {
|
|
726
|
+
#ifdef ATA_NO_RE2
|
|
727
|
+
ctx.compile_error = "patternProperties keyword requires RE2 support (built with ATA_NO_RE2)";
|
|
728
|
+
return node;
|
|
729
|
+
#else
|
|
639
730
|
dom::object pp_obj; pp_el.get(pp_obj);
|
|
640
731
|
for (auto [key, val] : pp_obj) {
|
|
641
732
|
schema_node::pattern_prop pp;
|
|
@@ -647,6 +738,7 @@ static schema_node_ptr compile_node(dom::element el,
|
|
|
647
738
|
}
|
|
648
739
|
node->pattern_properties.push_back(std::move(pp));
|
|
649
740
|
}
|
|
741
|
+
#endif
|
|
650
742
|
}
|
|
651
743
|
|
|
652
744
|
// format
|
|
@@ -659,15 +751,6 @@ static schema_node_ptr compile_node(dom::element el,
|
|
|
659
751
|
}
|
|
660
752
|
}
|
|
661
753
|
|
|
662
|
-
// $id (register in defs for potential resolution)
|
|
663
|
-
dom::element id_el;
|
|
664
|
-
if (obj["$id"].get(id_el) == SUCCESS) {
|
|
665
|
-
std::string_view sv;
|
|
666
|
-
if (id_el.get(sv) == SUCCESS) {
|
|
667
|
-
ctx.defs[std::string(sv)] = node;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
754
|
// enum — pre-minify each value at compile time
|
|
672
755
|
dom::element enum_el;
|
|
673
756
|
if (obj["enum"].get(enum_el) == SUCCESS) {
|
|
@@ -743,17 +826,144 @@ static schema_node_ptr compile_node(dom::element el,
|
|
|
743
826
|
}
|
|
744
827
|
}
|
|
745
828
|
|
|
829
|
+
ctx.current_resource_id = prev_resource;
|
|
746
830
|
return node;
|
|
747
831
|
}
|
|
748
832
|
|
|
749
833
|
// --- Validation ---
|
|
750
834
|
|
|
835
|
+
using dynamic_scope_t = std::vector<const std::unordered_map<std::string, schema_node_ptr>*>;
|
|
836
|
+
|
|
837
|
+
// Decode a single JSON Pointer segment (percent-decode, then ~1->/, ~0->~)
|
|
838
|
+
static std::string decode_pointer_segment(const std::string& seg) {
|
|
839
|
+
std::string pct;
|
|
840
|
+
for (size_t i = 0; i < seg.size(); ++i) {
|
|
841
|
+
if (seg[i] == '%' && i + 2 < seg.size()) {
|
|
842
|
+
auto hex = [](char c) -> int {
|
|
843
|
+
if (c >= '0' && c <= '9') return c - '0';
|
|
844
|
+
if (c >= 'a' && c <= 'f') return 10 + c - 'a';
|
|
845
|
+
if (c >= 'A' && c <= 'F') return 10 + c - 'A';
|
|
846
|
+
return -1;
|
|
847
|
+
};
|
|
848
|
+
int hv = hex(seg[i+1]), lv = hex(seg[i+2]);
|
|
849
|
+
if (hv >= 0 && lv >= 0) {
|
|
850
|
+
pct += static_cast<char>(hv * 16 + lv);
|
|
851
|
+
i += 2;
|
|
852
|
+
} else {
|
|
853
|
+
pct += seg[i];
|
|
854
|
+
}
|
|
855
|
+
} else {
|
|
856
|
+
pct += seg[i];
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
std::string out;
|
|
860
|
+
for (size_t i = 0; i < pct.size(); ++i) {
|
|
861
|
+
if (pct[i] == '~' && i + 1 < pct.size()) {
|
|
862
|
+
if (pct[i + 1] == '1') { out += '/'; ++i; }
|
|
863
|
+
else if (pct[i + 1] == '0') { out += '~'; ++i; }
|
|
864
|
+
else out += pct[i];
|
|
865
|
+
} else {
|
|
866
|
+
out += pct[i];
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return out;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Walk a JSON Pointer (without leading #) within a given schema node.
|
|
873
|
+
// Returns the resolved node, or nullptr if not found.
|
|
874
|
+
static schema_node_ptr walk_json_pointer(const schema_node_ptr& root_node,
|
|
875
|
+
const std::string& pointer) {
|
|
876
|
+
if (pointer.empty()) return root_node;
|
|
877
|
+
|
|
878
|
+
std::vector<std::string> segments;
|
|
879
|
+
size_t spos = 0;
|
|
880
|
+
// pointer starts with "/" — skip leading slash
|
|
881
|
+
if (!pointer.empty() && pointer[0] == '/') spos = 1;
|
|
882
|
+
while (spos <= pointer.size()) {
|
|
883
|
+
size_t snext = pointer.find('/', spos);
|
|
884
|
+
segments.push_back(decode_pointer_segment(
|
|
885
|
+
pointer.substr(spos, snext == std::string::npos ? snext : snext - spos)));
|
|
886
|
+
spos = (snext == std::string::npos) ? pointer.size() + 1 : snext + 1;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
schema_node_ptr current = root_node;
|
|
890
|
+
for (size_t si = 0; si < segments.size() && current; ++si) {
|
|
891
|
+
const auto& key = segments[si];
|
|
892
|
+
if (key == "properties" && si + 1 < segments.size()) {
|
|
893
|
+
const auto& prop_name = segments[++si];
|
|
894
|
+
auto pit = current->properties.find(prop_name);
|
|
895
|
+
if (pit != current->properties.end()) { current = pit->second; }
|
|
896
|
+
else { return nullptr; }
|
|
897
|
+
} else if (key == "items" && current->items_schema) {
|
|
898
|
+
current = current->items_schema;
|
|
899
|
+
} else if (key == "$defs" || key == "definitions") {
|
|
900
|
+
if (si + 1 < segments.size()) {
|
|
901
|
+
const auto& def_name = segments[++si];
|
|
902
|
+
auto dit = current->defs.find(def_name);
|
|
903
|
+
if (dit != current->defs.end()) { current = dit->second; }
|
|
904
|
+
else { return nullptr; }
|
|
905
|
+
} else { return nullptr; }
|
|
906
|
+
} else if (key == "allOf" || key == "anyOf" || key == "oneOf") {
|
|
907
|
+
if (si + 1 < segments.size()) {
|
|
908
|
+
size_t idx = std::stoul(segments[++si]);
|
|
909
|
+
auto& vec = (key == "allOf") ? current->all_of
|
|
910
|
+
: (key == "anyOf") ? current->any_of
|
|
911
|
+
: current->one_of;
|
|
912
|
+
if (idx < vec.size()) { current = vec[idx]; }
|
|
913
|
+
else { return nullptr; }
|
|
914
|
+
} else { return nullptr; }
|
|
915
|
+
} else if (key == "not" && current->not_schema) {
|
|
916
|
+
current = current->not_schema;
|
|
917
|
+
} else if (key == "if" && current->if_schema) {
|
|
918
|
+
current = current->if_schema;
|
|
919
|
+
} else if (key == "then" && current->then_schema) {
|
|
920
|
+
current = current->then_schema;
|
|
921
|
+
} else if (key == "else" && current->else_schema) {
|
|
922
|
+
current = current->else_schema;
|
|
923
|
+
} else if (key == "additionalProperties" &&
|
|
924
|
+
current->additional_properties_schema) {
|
|
925
|
+
current = current->additional_properties_schema;
|
|
926
|
+
} else if (key == "prefixItems") {
|
|
927
|
+
if (si + 1 < segments.size()) {
|
|
928
|
+
size_t idx = std::stoul(segments[++si]);
|
|
929
|
+
if (idx < current->prefix_items.size()) { current = current->prefix_items[idx]; }
|
|
930
|
+
else { return nullptr; }
|
|
931
|
+
} else { return nullptr; }
|
|
932
|
+
} else if (key == "contains" && current->contains_schema) {
|
|
933
|
+
current = current->contains_schema;
|
|
934
|
+
} else if (key == "propertyNames" && current->property_names_schema) {
|
|
935
|
+
current = current->property_names_schema;
|
|
936
|
+
} else {
|
|
937
|
+
return nullptr;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return current;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Find an anchor (non-pointer fragment) within a specific resource node by
|
|
944
|
+
// searching its sub-tree. Used for resolving "base#anchor" references.
|
|
945
|
+
static schema_node_ptr find_anchor_in_resource(const compiled_schema& ctx,
|
|
946
|
+
const std::string& resource_id,
|
|
947
|
+
const std::string& anchor_name) {
|
|
948
|
+
// Look up in per-resource dynamic anchors first
|
|
949
|
+
auto rit = ctx.resource_dynamic_anchors.find(resource_id);
|
|
950
|
+
if (rit != ctx.resource_dynamic_anchors.end()) {
|
|
951
|
+
auto ait = rit->second.find(anchor_name);
|
|
952
|
+
if (ait != rit->second.end()) return ait->second;
|
|
953
|
+
}
|
|
954
|
+
// Fallback to flat anchors (which includes $anchor entries)
|
|
955
|
+
auto ait = ctx.anchors.find(anchor_name);
|
|
956
|
+
if (ait != ctx.anchors.end()) return ait->second;
|
|
957
|
+
return nullptr;
|
|
958
|
+
}
|
|
959
|
+
|
|
751
960
|
static void validate_node(const schema_node_ptr& node,
|
|
752
961
|
dom::element value,
|
|
753
962
|
const std::string& path,
|
|
754
963
|
const compiled_schema& ctx,
|
|
755
964
|
std::vector<validation_error>& errors,
|
|
756
|
-
bool all_errors = true
|
|
965
|
+
bool all_errors = true,
|
|
966
|
+
dynamic_scope_t* dynamic_scope = nullptr);
|
|
757
967
|
|
|
758
968
|
// Fast boolean-only tree walker — no error collection, no string allocation.
|
|
759
969
|
// Uses [[likely]]/[[unlikely]] hints. Returns true if valid.
|
|
@@ -808,14 +1018,27 @@ static uint64_t utf8_length(std::string_view s) {
|
|
|
808
1018
|
return count;
|
|
809
1019
|
}
|
|
810
1020
|
|
|
1021
|
+
// Recursion depth guard — prevents stack overflow on self-referencing schemas
|
|
1022
|
+
struct DepthGuard {
|
|
1023
|
+
static thread_local int depth;
|
|
1024
|
+
bool overflow;
|
|
1025
|
+
DepthGuard() : overflow(++depth > 100) {}
|
|
1026
|
+
~DepthGuard() { --depth; }
|
|
1027
|
+
};
|
|
1028
|
+
thread_local int DepthGuard::depth = 0;
|
|
1029
|
+
|
|
811
1030
|
static void validate_node(const schema_node_ptr& node,
|
|
812
1031
|
dom::element value,
|
|
813
1032
|
const std::string& path,
|
|
814
1033
|
const compiled_schema& ctx,
|
|
815
1034
|
std::vector<validation_error>& errors,
|
|
816
|
-
bool all_errors
|
|
1035
|
+
bool all_errors,
|
|
1036
|
+
dynamic_scope_t* dynamic_scope) {
|
|
817
1037
|
if (!node) return;
|
|
818
1038
|
|
|
1039
|
+
DepthGuard guard;
|
|
1040
|
+
if (guard.overflow) return;
|
|
1041
|
+
|
|
819
1042
|
// Boolean schema
|
|
820
1043
|
if (node->boolean_schema.has_value()) {
|
|
821
1044
|
if (!node->boolean_schema.value()) {
|
|
@@ -825,136 +1048,116 @@ static void validate_node(const schema_node_ptr& node,
|
|
|
825
1048
|
return;
|
|
826
1049
|
}
|
|
827
1050
|
|
|
1051
|
+
// Dynamic scope tracking: push this resource's dynamic anchors
|
|
1052
|
+
bool pushed_scope = false;
|
|
1053
|
+
if (dynamic_scope && !node->id.empty()) {
|
|
1054
|
+
auto it = ctx.resource_dynamic_anchors.find(node->id);
|
|
1055
|
+
if (it != ctx.resource_dynamic_anchors.end()) {
|
|
1056
|
+
dynamic_scope->push_back(&it->second);
|
|
1057
|
+
pushed_scope = true;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
828
1061
|
// $ref — Draft 2020-12: $ref is not a short-circuit, sibling keywords still apply
|
|
829
1062
|
bool ref_resolved = false;
|
|
830
1063
|
if (!node->ref.empty()) {
|
|
831
|
-
//
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
validate_node(it->second, value, path, ctx, errors, all_errors);
|
|
1064
|
+
// Self-reference: "#"
|
|
1065
|
+
if (node->ref == "#" && ctx.root) {
|
|
1066
|
+
validate_node(ctx.root, value, path, ctx, errors, all_errors, dynamic_scope);
|
|
835
1067
|
ref_resolved = true;
|
|
836
1068
|
}
|
|
837
|
-
//
|
|
838
|
-
if (
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
pct += seg[i];
|
|
859
|
-
}
|
|
860
|
-
} else {
|
|
861
|
-
pct += seg[i];
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
// Then JSON Pointer unescape: ~1 -> /, ~0 -> ~
|
|
865
|
-
std::string out;
|
|
866
|
-
for (size_t i = 0; i < pct.size(); ++i) {
|
|
867
|
-
if (pct[i] == '~' && i + 1 < pct.size()) {
|
|
868
|
-
if (pct[i + 1] == '1') { out += '/'; ++i; }
|
|
869
|
-
else if (pct[i + 1] == '0') { out += '~'; ++i; }
|
|
870
|
-
else out += pct[i];
|
|
871
|
-
} else {
|
|
872
|
-
out += pct[i];
|
|
1069
|
+
// Check for "base#fragment" pattern (e.g. "first#/$defs/stuff", "tree.json")
|
|
1070
|
+
if (!ref_resolved) {
|
|
1071
|
+
std::string base_uri;
|
|
1072
|
+
std::string fragment;
|
|
1073
|
+
size_t hash_pos = node->ref.find('#');
|
|
1074
|
+
if (hash_pos != std::string::npos) {
|
|
1075
|
+
base_uri = node->ref.substr(0, hash_pos);
|
|
1076
|
+
fragment = node->ref.substr(hash_pos + 1);
|
|
1077
|
+
} else {
|
|
1078
|
+
base_uri = node->ref;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Helper: push base resource's dynamic anchors to scope, validate, pop
|
|
1082
|
+
auto validate_with_resource_scope = [&](const schema_node_ptr& target,
|
|
1083
|
+
const std::string& resource_id) {
|
|
1084
|
+
bool scope_pushed = false;
|
|
1085
|
+
if (dynamic_scope && !resource_id.empty()) {
|
|
1086
|
+
auto rit = ctx.resource_dynamic_anchors.find(resource_id);
|
|
1087
|
+
if (rit != ctx.resource_dynamic_anchors.end()) {
|
|
1088
|
+
dynamic_scope->push_back(&rit->second);
|
|
1089
|
+
scope_pushed = true;
|
|
873
1090
|
}
|
|
874
1091
|
}
|
|
875
|
-
|
|
1092
|
+
validate_node(target, value, path, ctx, errors, all_errors, dynamic_scope);
|
|
1093
|
+
if (scope_pushed) dynamic_scope->pop_back();
|
|
876
1094
|
};
|
|
877
1095
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
bool resolved = true;
|
|
892
|
-
for (size_t si = 0; si < segments.size() && current; ++si) {
|
|
893
|
-
const auto& key = segments[si];
|
|
894
|
-
|
|
895
|
-
if (key == "properties" && si + 1 < segments.size()) {
|
|
896
|
-
const auto& prop_name = segments[++si];
|
|
897
|
-
auto pit = current->properties.find(prop_name);
|
|
898
|
-
if (pit != current->properties.end()) {
|
|
899
|
-
current = pit->second;
|
|
900
|
-
} else { resolved = false; break; }
|
|
901
|
-
} else if (key == "items" && current->items_schema) {
|
|
902
|
-
current = current->items_schema;
|
|
903
|
-
} else if (key == "$defs" || key == "definitions") {
|
|
904
|
-
if (si + 1 < segments.size()) {
|
|
905
|
-
const auto& def_name = segments[++si];
|
|
906
|
-
// Navigate into node's defs map
|
|
907
|
-
auto dit = current->defs.find(def_name);
|
|
908
|
-
if (dit != current->defs.end()) {
|
|
909
|
-
current = dit->second;
|
|
1096
|
+
if (!base_uri.empty()) {
|
|
1097
|
+
// Resolve base URI to a resource via defs
|
|
1098
|
+
auto it = ctx.defs.find(base_uri);
|
|
1099
|
+
if (it != ctx.defs.end()) {
|
|
1100
|
+
schema_node_ptr target = it->second;
|
|
1101
|
+
if (!fragment.empty()) {
|
|
1102
|
+
if (fragment[0] == '/') {
|
|
1103
|
+
// JSON Pointer within the resource
|
|
1104
|
+
auto resolved = walk_json_pointer(target, fragment);
|
|
1105
|
+
if (resolved) {
|
|
1106
|
+
validate_with_resource_scope(resolved, base_uri);
|
|
1107
|
+
ref_resolved = true;
|
|
1108
|
+
}
|
|
910
1109
|
} else {
|
|
911
|
-
//
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
}
|
|
1110
|
+
// Anchor lookup within the resource
|
|
1111
|
+
auto resolved = find_anchor_in_resource(ctx, base_uri, fragment);
|
|
1112
|
+
if (resolved) {
|
|
1113
|
+
validate_with_resource_scope(resolved, base_uri);
|
|
1114
|
+
ref_resolved = true;
|
|
1115
|
+
}
|
|
917
1116
|
}
|
|
918
|
-
} else {
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
current = current->then_schema;
|
|
934
|
-
} else if (key == "else" && current->else_schema) {
|
|
935
|
-
current = current->else_schema;
|
|
936
|
-
} else if (key == "additionalProperties" &&
|
|
937
|
-
current->additional_properties_schema) {
|
|
938
|
-
current = current->additional_properties_schema;
|
|
939
|
-
} else if (key == "prefixItems") {
|
|
940
|
-
if (si + 1 < segments.size()) {
|
|
941
|
-
size_t idx = std::stoul(segments[++si]);
|
|
942
|
-
if (idx < current->prefix_items.size()) { current = current->prefix_items[idx]; }
|
|
943
|
-
else { resolved = false; break; }
|
|
944
|
-
} else { resolved = false; break; }
|
|
1117
|
+
} else {
|
|
1118
|
+
// No fragment, just the base resource (it pushes its own scope)
|
|
1119
|
+
validate_node(target, value, path, ctx, errors, all_errors, dynamic_scope);
|
|
1120
|
+
ref_resolved = true;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
} else if (!fragment.empty()) {
|
|
1124
|
+
// "#fragment" — no base URI
|
|
1125
|
+
if (fragment[0] == '/') {
|
|
1126
|
+
// JSON Pointer from root
|
|
1127
|
+
auto resolved = walk_json_pointer(ctx.root, fragment);
|
|
1128
|
+
if (resolved) {
|
|
1129
|
+
validate_node(resolved, value, path, ctx, errors, all_errors, dynamic_scope);
|
|
1130
|
+
ref_resolved = true;
|
|
1131
|
+
}
|
|
945
1132
|
} else {
|
|
946
|
-
|
|
1133
|
+
// Anchor lookup
|
|
1134
|
+
auto ait = ctx.anchors.find(fragment);
|
|
1135
|
+
if (ait != ctx.anchors.end()) {
|
|
1136
|
+
validate_node(ait->second, value, path, ctx, errors, all_errors, dynamic_scope);
|
|
1137
|
+
ref_resolved = true;
|
|
1138
|
+
}
|
|
947
1139
|
}
|
|
948
1140
|
}
|
|
949
|
-
|
|
950
|
-
|
|
1141
|
+
}
|
|
1142
|
+
// Fallback: try defs map directly (handles bare $id references like "list")
|
|
1143
|
+
if (!ref_resolved) {
|
|
1144
|
+
auto it = ctx.defs.find(node->ref);
|
|
1145
|
+
if (it != ctx.defs.end()) {
|
|
1146
|
+
validate_node(it->second, value, path, ctx, errors, all_errors, dynamic_scope);
|
|
951
1147
|
ref_resolved = true;
|
|
952
1148
|
}
|
|
953
1149
|
}
|
|
954
|
-
//
|
|
955
|
-
if (!ref_resolved && node->ref
|
|
956
|
-
|
|
957
|
-
|
|
1150
|
+
// Fallback: relative URI resolution — match ref against defs keys by suffix
|
|
1151
|
+
if (!ref_resolved && !node->ref.empty() && node->ref[0] != '#') {
|
|
1152
|
+
std::string suffix = "/" + node->ref;
|
|
1153
|
+
for (const auto& [key, def_node] : ctx.defs) {
|
|
1154
|
+
if (key.size() >= suffix.size() &&
|
|
1155
|
+
key.compare(key.size() - suffix.size(), suffix.size(), suffix) == 0) {
|
|
1156
|
+
validate_node(def_node, value, path, ctx, errors, all_errors, dynamic_scope);
|
|
1157
|
+
ref_resolved = true;
|
|
1158
|
+
break;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
958
1161
|
}
|
|
959
1162
|
if (!ref_resolved) {
|
|
960
1163
|
errors.push_back({error_code::ref_not_found, path,
|
|
@@ -962,6 +1165,132 @@ static void validate_node(const schema_node_ptr& node,
|
|
|
962
1165
|
}
|
|
963
1166
|
}
|
|
964
1167
|
|
|
1168
|
+
// $dynamicRef — Draft 2020-12 dynamic scope resolution
|
|
1169
|
+
if (!node->dynamic_ref.empty()) {
|
|
1170
|
+
bool dref_resolved = false;
|
|
1171
|
+
|
|
1172
|
+
// Parse the $dynamicRef value into base URI and fragment
|
|
1173
|
+
std::string dr_base;
|
|
1174
|
+
std::string dr_fragment;
|
|
1175
|
+
{
|
|
1176
|
+
size_t hash_pos = node->dynamic_ref.find('#');
|
|
1177
|
+
if (hash_pos != std::string::npos) {
|
|
1178
|
+
dr_base = node->dynamic_ref.substr(0, hash_pos);
|
|
1179
|
+
dr_fragment = node->dynamic_ref.substr(hash_pos + 1);
|
|
1180
|
+
} else {
|
|
1181
|
+
dr_base = node->dynamic_ref;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Helper: push base resource's dynamic anchors to scope temporarily
|
|
1186
|
+
auto push_resource_scope = [&](const std::string& resource_id) -> bool {
|
|
1187
|
+
if (dynamic_scope && !resource_id.empty()) {
|
|
1188
|
+
auto rit = ctx.resource_dynamic_anchors.find(resource_id);
|
|
1189
|
+
if (rit != ctx.resource_dynamic_anchors.end()) {
|
|
1190
|
+
dynamic_scope->push_back(&rit->second);
|
|
1191
|
+
return true;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return false;
|
|
1195
|
+
};
|
|
1196
|
+
|
|
1197
|
+
// If fragment is a JSON pointer (starts with /), resolve like $ref
|
|
1198
|
+
if (!dr_fragment.empty() && dr_fragment[0] == '/') {
|
|
1199
|
+
schema_node_ptr base_node = dr_base.empty() ? ctx.root : nullptr;
|
|
1200
|
+
if (!dr_base.empty()) {
|
|
1201
|
+
auto it = ctx.defs.find(dr_base);
|
|
1202
|
+
if (it != ctx.defs.end()) base_node = it->second;
|
|
1203
|
+
}
|
|
1204
|
+
if (base_node) {
|
|
1205
|
+
auto resolved = walk_json_pointer(base_node, dr_fragment);
|
|
1206
|
+
if (resolved) {
|
|
1207
|
+
bool dr_scope_pushed = push_resource_scope(dr_base);
|
|
1208
|
+
validate_node(resolved, value, path, ctx, errors, all_errors, dynamic_scope);
|
|
1209
|
+
if (dr_scope_pushed) dynamic_scope->pop_back();
|
|
1210
|
+
dref_resolved = true;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// If fragment is an anchor name (not a JSON pointer)
|
|
1216
|
+
if (!dref_resolved && !dr_fragment.empty() && dr_fragment[0] != '/') {
|
|
1217
|
+
std::string anchor_name = dr_fragment;
|
|
1218
|
+
|
|
1219
|
+
// Initial resolution: find the anchor
|
|
1220
|
+
schema_node_ptr target = nullptr;
|
|
1221
|
+
|
|
1222
|
+
if (!dr_base.empty()) {
|
|
1223
|
+
// Resolve base URI first, then find anchor in that resource
|
|
1224
|
+
auto it = ctx.defs.find(dr_base);
|
|
1225
|
+
if (it != ctx.defs.end()) {
|
|
1226
|
+
target = find_anchor_in_resource(ctx, dr_base, anchor_name);
|
|
1227
|
+
}
|
|
1228
|
+
} else {
|
|
1229
|
+
// No base URI — look up in flat anchors map
|
|
1230
|
+
auto ait = ctx.anchors.find(anchor_name);
|
|
1231
|
+
if (ait != ctx.anchors.end()) {
|
|
1232
|
+
target = ait->second;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
if (target) {
|
|
1237
|
+
// Check if the initially resolved target is itself a $dynamicAnchor
|
|
1238
|
+
// (the "bookend" requirement). Only do dynamic scope walk if the
|
|
1239
|
+
// initial target's resource has a $dynamicAnchor with this name.
|
|
1240
|
+
bool is_dynamic_at_initial = false;
|
|
1241
|
+
if (!dr_base.empty()) {
|
|
1242
|
+
// We resolved via a specific base URI
|
|
1243
|
+
auto rit = ctx.resource_dynamic_anchors.find(dr_base);
|
|
1244
|
+
if (rit != ctx.resource_dynamic_anchors.end() &&
|
|
1245
|
+
rit->second.count(anchor_name)) {
|
|
1246
|
+
is_dynamic_at_initial = true;
|
|
1247
|
+
}
|
|
1248
|
+
} else {
|
|
1249
|
+
// No base URI — check if ANY resource has this as $dynamicAnchor
|
|
1250
|
+
// and the target matches (i.e., the initially resolved node IS a
|
|
1251
|
+
// $dynamicAnchor node)
|
|
1252
|
+
for (const auto& [rid, rmap] : ctx.resource_dynamic_anchors) {
|
|
1253
|
+
auto ait2 = rmap.find(anchor_name);
|
|
1254
|
+
if (ait2 != rmap.end() && ait2->second == target) {
|
|
1255
|
+
is_dynamic_at_initial = true;
|
|
1256
|
+
break;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Dynamic scope walk: find first override in dynamic scope
|
|
1262
|
+
if (is_dynamic_at_initial && dynamic_scope) {
|
|
1263
|
+
for (size_t i = 0; i < dynamic_scope->size(); ++i) {
|
|
1264
|
+
auto dit = (*dynamic_scope)[i]->find(anchor_name);
|
|
1265
|
+
if (dit != (*dynamic_scope)[i]->end()) {
|
|
1266
|
+
target = dit->second;
|
|
1267
|
+
break;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
bool dr_scope_pushed = push_resource_scope(dr_base);
|
|
1273
|
+
validate_node(target, value, path, ctx, errors, all_errors, dynamic_scope);
|
|
1274
|
+
if (dr_scope_pushed) dynamic_scope->pop_back();
|
|
1275
|
+
dref_resolved = true;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// Bare $dynamicRef without fragment (unusual, but handle it)
|
|
1280
|
+
if (!dref_resolved && dr_fragment.empty() && !dr_base.empty()) {
|
|
1281
|
+
auto it = ctx.defs.find(dr_base);
|
|
1282
|
+
if (it != ctx.defs.end()) {
|
|
1283
|
+
validate_node(it->second, value, path, ctx, errors, all_errors, dynamic_scope);
|
|
1284
|
+
dref_resolved = true;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
if (!dref_resolved) {
|
|
1289
|
+
errors.push_back({error_code::ref_not_found, path,
|
|
1290
|
+
"cannot resolve $dynamicRef: " + node->dynamic_ref});
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
965
1294
|
// type
|
|
966
1295
|
if (node->type_mask) {
|
|
967
1296
|
if (!type_matches_mask(value, node->type_mask)) {
|
|
@@ -1061,6 +1390,7 @@ static void validate_node(const schema_node_ptr& node,
|
|
|
1061
1390
|
" > maxLength " +
|
|
1062
1391
|
std::to_string(node->max_length.value())});
|
|
1063
1392
|
}
|
|
1393
|
+
#ifndef ATA_NO_RE2
|
|
1064
1394
|
if (node->compiled_pattern) {
|
|
1065
1395
|
if (!re2::RE2::PartialMatch(re2::StringPiece(sv.data(), sv.size()), *node->compiled_pattern)) {
|
|
1066
1396
|
errors.push_back({error_code::pattern_mismatch, path,
|
|
@@ -1068,6 +1398,7 @@ static void validate_node(const schema_node_ptr& node,
|
|
|
1068
1398
|
node->pattern.value()});
|
|
1069
1399
|
}
|
|
1070
1400
|
}
|
|
1401
|
+
#endif
|
|
1071
1402
|
|
|
1072
1403
|
if (node->format.has_value()) {
|
|
1073
1404
|
if (!check_format_by_id(sv, node->format_id)) {
|
|
@@ -1139,10 +1470,10 @@ static void validate_node(const schema_node_ptr& node,
|
|
|
1139
1470
|
for (auto item : arr) {
|
|
1140
1471
|
if (idx < node->prefix_items.size()) {
|
|
1141
1472
|
validate_node(node->prefix_items[idx], item,
|
|
1142
|
-
path + "/" + std::to_string(idx), ctx, errors, all_errors);
|
|
1473
|
+
path + "/" + std::to_string(idx), ctx, errors, all_errors, dynamic_scope);
|
|
1143
1474
|
} else if (node->items_schema) {
|
|
1144
1475
|
validate_node(node->items_schema, item,
|
|
1145
|
-
path + "/" + std::to_string(idx), ctx, errors, all_errors);
|
|
1476
|
+
path + "/" + std::to_string(idx), ctx, errors, all_errors, dynamic_scope);
|
|
1146
1477
|
}
|
|
1147
1478
|
++idx;
|
|
1148
1479
|
}
|
|
@@ -1209,16 +1540,18 @@ static void validate_node(const schema_node_ptr& node,
|
|
|
1209
1540
|
// Check properties
|
|
1210
1541
|
auto it = node->properties.find(key_str);
|
|
1211
1542
|
if (it != node->properties.end()) {
|
|
1212
|
-
validate_node(it->second, val, path + "/" + key_str, ctx, errors, all_errors);
|
|
1543
|
+
validate_node(it->second, val, path + "/" + key_str, ctx, errors, all_errors, dynamic_scope);
|
|
1213
1544
|
matched = true;
|
|
1214
1545
|
}
|
|
1215
1546
|
|
|
1216
1547
|
// Check patternProperties (use cached compiled regex)
|
|
1217
1548
|
for (const auto& pp : node->pattern_properties) {
|
|
1549
|
+
#ifndef ATA_NO_RE2
|
|
1218
1550
|
if (pp.compiled && re2::RE2::PartialMatch(key_str, *pp.compiled)) {
|
|
1219
|
-
validate_node(pp.schema, val, path + "/" + key_str, ctx, errors, all_errors);
|
|
1551
|
+
validate_node(pp.schema, val, path + "/" + key_str, ctx, errors, all_errors, dynamic_scope);
|
|
1220
1552
|
matched = true;
|
|
1221
1553
|
}
|
|
1554
|
+
#endif
|
|
1222
1555
|
}
|
|
1223
1556
|
|
|
1224
1557
|
// additionalProperties (only if not matched by properties or patternProperties)
|
|
@@ -1230,7 +1563,7 @@ static void validate_node(const schema_node_ptr& node,
|
|
|
1230
1563
|
"additional property not allowed: " + key_str});
|
|
1231
1564
|
} else if (node->additional_properties_schema) {
|
|
1232
1565
|
validate_node(node->additional_properties_schema, val,
|
|
1233
|
-
path + "/" + key_str, ctx, errors);
|
|
1566
|
+
path + "/" + key_str, ctx, errors, all_errors, dynamic_scope);
|
|
1234
1567
|
}
|
|
1235
1568
|
}
|
|
1236
1569
|
}
|
|
@@ -1259,12 +1592,14 @@ static void validate_node(const schema_node_ptr& node,
|
|
|
1259
1592
|
errors.push_back({error_code::max_length_violation, path,
|
|
1260
1593
|
"propertyNames: key too long: " + std::string(key_sv)});
|
|
1261
1594
|
}
|
|
1595
|
+
#ifndef ATA_NO_RE2
|
|
1262
1596
|
if (pn->compiled_pattern) {
|
|
1263
1597
|
if (!re2::RE2::PartialMatch(re2::StringPiece(key_sv.data(), key_sv.size()), *pn->compiled_pattern)) {
|
|
1264
1598
|
errors.push_back({error_code::pattern_mismatch, path,
|
|
1265
1599
|
"propertyNames: key does not match pattern: " + std::string(key_sv)});
|
|
1266
1600
|
}
|
|
1267
1601
|
}
|
|
1602
|
+
#endif
|
|
1268
1603
|
if (pn->format.has_value() && !check_format_by_id(key_sv, pn->format_id)) {
|
|
1269
1604
|
errors.push_back({error_code::format_mismatch, path,
|
|
1270
1605
|
"propertyNames: key does not match format: " + std::string(key_sv)});
|
|
@@ -1276,7 +1611,7 @@ static void validate_node(const schema_node_ptr& node,
|
|
|
1276
1611
|
std::string key_json = "\"" + std::string(key) + "\"";
|
|
1277
1612
|
auto key_result = tl_dom_key_parser().parse(key_json);
|
|
1278
1613
|
if (!key_result.error()) {
|
|
1279
|
-
validate_node(pn, key_result.value(), path, ctx, errors, all_errors);
|
|
1614
|
+
validate_node(pn, key_result.value(), path, ctx, errors, all_errors, dynamic_scope);
|
|
1280
1615
|
}
|
|
1281
1616
|
}
|
|
1282
1617
|
}
|
|
@@ -1301,7 +1636,7 @@ static void validate_node(const schema_node_ptr& node,
|
|
|
1301
1636
|
for (const auto& [prop, schema] : node->dependent_schemas) {
|
|
1302
1637
|
dom::element dummy;
|
|
1303
1638
|
if (obj[prop].get(dummy) == SUCCESS) {
|
|
1304
|
-
validate_node(schema, value, path, ctx, errors, all_errors);
|
|
1639
|
+
validate_node(schema, value, path, ctx, errors, all_errors, dynamic_scope);
|
|
1305
1640
|
}
|
|
1306
1641
|
}
|
|
1307
1642
|
}
|
|
@@ -1310,7 +1645,7 @@ static void validate_node(const schema_node_ptr& node,
|
|
|
1310
1645
|
if (!node->all_of.empty()) {
|
|
1311
1646
|
for (const auto& sub : node->all_of) {
|
|
1312
1647
|
std::vector<validation_error> sub_errors;
|
|
1313
|
-
validate_node(sub, value, path, ctx, sub_errors, all_errors);
|
|
1648
|
+
validate_node(sub, value, path, ctx, sub_errors, all_errors, dynamic_scope);
|
|
1314
1649
|
if (!sub_errors.empty()) {
|
|
1315
1650
|
errors.push_back({error_code::all_of_failed, path,
|
|
1316
1651
|
"allOf subschema failed"});
|
|
@@ -1324,7 +1659,7 @@ static void validate_node(const schema_node_ptr& node,
|
|
|
1324
1659
|
bool any_valid = false;
|
|
1325
1660
|
for (const auto& sub : node->any_of) {
|
|
1326
1661
|
std::vector<validation_error> sub_errors;
|
|
1327
|
-
validate_node(sub, value, path, ctx, sub_errors, all_errors);
|
|
1662
|
+
validate_node(sub, value, path, ctx, sub_errors, all_errors, dynamic_scope);
|
|
1328
1663
|
if (sub_errors.empty()) {
|
|
1329
1664
|
any_valid = true;
|
|
1330
1665
|
break;
|
|
@@ -1341,7 +1676,7 @@ static void validate_node(const schema_node_ptr& node,
|
|
|
1341
1676
|
int match_count = 0;
|
|
1342
1677
|
for (const auto& sub : node->one_of) {
|
|
1343
1678
|
std::vector<validation_error> sub_errors;
|
|
1344
|
-
validate_node(sub, value, path, ctx, sub_errors, all_errors);
|
|
1679
|
+
validate_node(sub, value, path, ctx, sub_errors, all_errors, dynamic_scope);
|
|
1345
1680
|
if (sub_errors.empty()) ++match_count;
|
|
1346
1681
|
}
|
|
1347
1682
|
if (match_count != 1) {
|
|
@@ -1354,7 +1689,7 @@ static void validate_node(const schema_node_ptr& node,
|
|
|
1354
1689
|
// not
|
|
1355
1690
|
if (node->not_schema) {
|
|
1356
1691
|
std::vector<validation_error> sub_errors;
|
|
1357
|
-
validate_node(node->not_schema, value, path, ctx, sub_errors, all_errors);
|
|
1692
|
+
validate_node(node->not_schema, value, path, ctx, sub_errors, all_errors, dynamic_scope);
|
|
1358
1693
|
if (sub_errors.empty()) {
|
|
1359
1694
|
errors.push_back({error_code::not_failed, path,
|
|
1360
1695
|
"value should not match 'not' schema"});
|
|
@@ -1364,19 +1699,21 @@ static void validate_node(const schema_node_ptr& node,
|
|
|
1364
1699
|
// if/then/else
|
|
1365
1700
|
if (node->if_schema) {
|
|
1366
1701
|
std::vector<validation_error> if_errors;
|
|
1367
|
-
validate_node(node->if_schema, value, path, ctx, if_errors, all_errors);
|
|
1702
|
+
validate_node(node->if_schema, value, path, ctx, if_errors, all_errors, dynamic_scope);
|
|
1368
1703
|
if (if_errors.empty()) {
|
|
1369
1704
|
// if passed → validate then
|
|
1370
1705
|
if (node->then_schema) {
|
|
1371
|
-
validate_node(node->then_schema, value, path, ctx, errors, all_errors);
|
|
1706
|
+
validate_node(node->then_schema, value, path, ctx, errors, all_errors, dynamic_scope);
|
|
1372
1707
|
}
|
|
1373
1708
|
} else {
|
|
1374
1709
|
// if failed → validate else
|
|
1375
1710
|
if (node->else_schema) {
|
|
1376
|
-
validate_node(node->else_schema, value, path, ctx, errors, all_errors);
|
|
1711
|
+
validate_node(node->else_schema, value, path, ctx, errors, all_errors, dynamic_scope);
|
|
1377
1712
|
}
|
|
1378
1713
|
}
|
|
1379
1714
|
}
|
|
1715
|
+
|
|
1716
|
+
if (pushed_scope) dynamic_scope->pop_back();
|
|
1380
1717
|
}
|
|
1381
1718
|
|
|
1382
1719
|
// Fast boolean-only tree walker — stripped of all error collection.
|
|
@@ -1387,14 +1724,27 @@ static bool validate_fast(const schema_node_ptr& node,
|
|
|
1387
1724
|
const compiled_schema& ctx) {
|
|
1388
1725
|
if (!node) [[unlikely]] return true;
|
|
1389
1726
|
|
|
1727
|
+
DepthGuard guard;
|
|
1728
|
+
if (guard.overflow) [[unlikely]] return true;
|
|
1729
|
+
|
|
1390
1730
|
if (node->boolean_schema.has_value()) [[unlikely]]
|
|
1391
1731
|
return node->boolean_schema.value();
|
|
1392
1732
|
|
|
1733
|
+
// $dynamicRef — bail to tree walker
|
|
1734
|
+
if (!node->dynamic_ref.empty()) [[unlikely]] return false;
|
|
1735
|
+
|
|
1393
1736
|
// $ref
|
|
1394
1737
|
if (!node->ref.empty()) [[unlikely]] {
|
|
1395
1738
|
auto it = ctx.defs.find(node->ref);
|
|
1396
1739
|
if (it != ctx.defs.end()) {
|
|
1397
1740
|
if (!validate_fast(it->second, value, ctx)) return false;
|
|
1741
|
+
} else if (node->ref.size() > 1 && node->ref[0] == '#' && node->ref[1] != '/') {
|
|
1742
|
+
auto ait = ctx.anchors.find(node->ref.substr(1));
|
|
1743
|
+
if (ait != ctx.anchors.end()) {
|
|
1744
|
+
if (!validate_fast(ait->second, value, ctx)) return false;
|
|
1745
|
+
} else {
|
|
1746
|
+
return false;
|
|
1747
|
+
}
|
|
1398
1748
|
} else if (node->ref == "#" && ctx.root) {
|
|
1399
1749
|
if (!validate_fast(ctx.root, value, ctx)) return false;
|
|
1400
1750
|
} else {
|
|
@@ -1444,10 +1794,12 @@ static bool validate_fast(const schema_node_ptr& node,
|
|
|
1444
1794
|
uint64_t len = utf8_length(sv);
|
|
1445
1795
|
if (node->min_length.has_value() && len < node->min_length.value()) return false;
|
|
1446
1796
|
if (node->max_length.has_value() && len > node->max_length.value()) return false;
|
|
1797
|
+
#ifndef ATA_NO_RE2
|
|
1447
1798
|
if (node->compiled_pattern) {
|
|
1448
1799
|
if (!re2::RE2::PartialMatch(re2::StringPiece(sv.data(), sv.size()), *node->compiled_pattern))
|
|
1449
1800
|
return false;
|
|
1450
1801
|
}
|
|
1802
|
+
#endif
|
|
1451
1803
|
if (node->format.has_value() && !check_format_by_id(sv, node->format_id)) return false;
|
|
1452
1804
|
}
|
|
1453
1805
|
|
|
@@ -1532,11 +1884,13 @@ static bool validate_fast(const schema_node_ptr& node,
|
|
|
1532
1884
|
}
|
|
1533
1885
|
|
|
1534
1886
|
for (const auto& pp : node->pattern_properties) {
|
|
1887
|
+
#ifndef ATA_NO_RE2
|
|
1535
1888
|
if (pp.compiled && re2::RE2::PartialMatch(
|
|
1536
1889
|
re2::StringPiece(key_sv.data(), key_sv.size()), *pp.compiled)) {
|
|
1537
1890
|
if (!validate_fast(pp.schema, val, ctx)) return false;
|
|
1538
1891
|
matched = true;
|
|
1539
1892
|
}
|
|
1893
|
+
#endif
|
|
1540
1894
|
}
|
|
1541
1895
|
|
|
1542
1896
|
if (!matched) {
|
|
@@ -1615,8 +1969,9 @@ static void cg_compile(const schema_node* n, cg::plan& p,
|
|
|
1615
1969
|
return;
|
|
1616
1970
|
}
|
|
1617
1971
|
// Composition fallback
|
|
1618
|
-
if (!n->ref.empty() || !n->
|
|
1619
|
-
!n->
|
|
1972
|
+
if (!n->ref.empty() || !n->dynamic_ref.empty() || !n->all_of.empty() ||
|
|
1973
|
+
!n->any_of.empty() || !n->one_of.empty() || n->not_schema ||
|
|
1974
|
+
n->if_schema) {
|
|
1620
1975
|
uintptr_t ptr = reinterpret_cast<uintptr_t>(n);
|
|
1621
1976
|
out.push_back({cg::op::COMPOSITION, (uint32_t)(ptr & 0xFFFFFFFF),
|
|
1622
1977
|
(uint32_t)((ptr >> 32) & 0xFFFFFFFF)});
|
|
@@ -1670,7 +2025,9 @@ static void cg_compile(const schema_node* n, cg::plan& p,
|
|
|
1670
2025
|
// String
|
|
1671
2026
|
if (n->min_length.has_value()) out.push_back({cg::op::CHECK_MIN_LENGTH,(uint32_t)*n->min_length});
|
|
1672
2027
|
if (n->max_length.has_value()) out.push_back({cg::op::CHECK_MAX_LENGTH,(uint32_t)*n->max_length});
|
|
2028
|
+
#ifndef ATA_NO_RE2
|
|
1673
2029
|
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}); }
|
|
2030
|
+
#endif
|
|
1674
2031
|
if (n->format.has_value()) {
|
|
1675
2032
|
uint32_t i=(uint32_t)p.format_ids.size();
|
|
1676
2033
|
p.format_ids.push_back(n->format_id);
|
|
@@ -1744,7 +2101,11 @@ static bool cg_exec(const cg::plan& p, const std::vector<cg::ins>& code,
|
|
|
1744
2101
|
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;
|
|
1745
2102
|
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;
|
|
1746
2103
|
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;
|
|
2104
|
+
#ifndef ATA_NO_RE2
|
|
1747
2105
|
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;
|
|
2106
|
+
#else
|
|
2107
|
+
case cg::op::CHECK_PATTERN: break;
|
|
2108
|
+
#endif
|
|
1748
2109
|
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;
|
|
1749
2110
|
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;
|
|
1750
2111
|
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;
|
|
@@ -1854,7 +2215,11 @@ static bool od_exec(const cg::plan& p, const std::vector<cg::ins>& code,
|
|
|
1854
2215
|
}
|
|
1855
2216
|
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;
|
|
1856
2217
|
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;
|
|
2218
|
+
#ifndef ATA_NO_RE2
|
|
1857
2219
|
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;
|
|
2220
|
+
#else
|
|
2221
|
+
case cg::op::CHECK_PATTERN: break;
|
|
2222
|
+
#endif
|
|
1858
2223
|
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;
|
|
1859
2224
|
case cg::op::CHECK_MIN_ITEMS: if(t==json_type::array){
|
|
1860
2225
|
simdjson::ondemand::array a; if(value.get(a)!=SUCCESS) return false;
|
|
@@ -2012,7 +2377,9 @@ static od_plan_ptr compile_od_plan(const schema_node_ptr& node) {
|
|
|
2012
2377
|
if (node->multiple_of) { plan->num_flags |= od_plan::HAS_MUL; plan->num_mul = *node->multiple_of; }
|
|
2013
2378
|
plan->min_length = node->min_length;
|
|
2014
2379
|
plan->max_length = node->max_length;
|
|
2380
|
+
#ifndef ATA_NO_RE2
|
|
2015
2381
|
plan->pattern = node->compiled_pattern.get();
|
|
2382
|
+
#endif
|
|
2016
2383
|
plan->format_id = node->format_id;
|
|
2017
2384
|
|
|
2018
2385
|
// Object plan — build hash lookup for O(1) per-field dispatch
|
|
@@ -2151,10 +2518,12 @@ static bool od_exec_plan(const od_plan& plan, simdjson::ondemand::value value) {
|
|
|
2151
2518
|
if (plan.min_length && len < *plan.min_length) return false;
|
|
2152
2519
|
if (plan.max_length && len > *plan.max_length) return false;
|
|
2153
2520
|
}
|
|
2521
|
+
#ifndef ATA_NO_RE2
|
|
2154
2522
|
if (plan.pattern) {
|
|
2155
2523
|
if (!re2::RE2::PartialMatch(re2::StringPiece(sv.data(), sv.size()), *plan.pattern))
|
|
2156
2524
|
return false;
|
|
2157
2525
|
}
|
|
2526
|
+
#endif
|
|
2158
2527
|
if (plan.format_id != 255) {
|
|
2159
2528
|
if (!check_format_by_id(sv, plan.format_id)) return false;
|
|
2160
2529
|
}
|
|
@@ -2235,6 +2604,10 @@ schema_ref compile(std::string_view schema_json) {
|
|
|
2235
2604
|
|
|
2236
2605
|
ctx->root = compile_node(doc, *ctx);
|
|
2237
2606
|
|
|
2607
|
+
if (!ctx->compile_error.empty()) {
|
|
2608
|
+
return schema_ref{nullptr};
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2238
2611
|
// Generate codegen plan
|
|
2239
2612
|
cg_compile(ctx->root.get(), ctx->gen_plan, ctx->gen_plan.code);
|
|
2240
2613
|
ctx->gen_plan.code.push_back({cg::op::END});
|
|
@@ -2290,8 +2663,24 @@ validation_result validate(const schema_ref& schema, std::string_view json,
|
|
|
2290
2663
|
|
|
2291
2664
|
// Slow path: tree walker with error details (reuse already-parsed DOM)
|
|
2292
2665
|
std::vector<validation_error> errors;
|
|
2293
|
-
|
|
2294
|
-
|
|
2666
|
+
if (schema.impl->has_dynamic_refs) {
|
|
2667
|
+
dynamic_scope_t scope;
|
|
2668
|
+
auto rit = schema.impl->resource_dynamic_anchors.find("");
|
|
2669
|
+
if (rit != schema.impl->resource_dynamic_anchors.end()) {
|
|
2670
|
+
scope.push_back(&rit->second);
|
|
2671
|
+
}
|
|
2672
|
+
if (!schema.impl->root->id.empty()) {
|
|
2673
|
+
auto iit = schema.impl->resource_dynamic_anchors.find(schema.impl->root->id);
|
|
2674
|
+
if (iit != schema.impl->resource_dynamic_anchors.end()) {
|
|
2675
|
+
scope.push_back(&iit->second);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
validate_node(schema.impl->root, result.value(), "", *schema.impl, errors,
|
|
2679
|
+
opts.all_errors, &scope);
|
|
2680
|
+
} else {
|
|
2681
|
+
validate_node(schema.impl->root, result.value(), "", *schema.impl, errors,
|
|
2682
|
+
opts.all_errors);
|
|
2683
|
+
}
|
|
2295
2684
|
|
|
2296
2685
|
return {errors.empty(), std::move(errors)};
|
|
2297
2686
|
}
|