teptris 0.3.22.0 → 0.3.23.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.
@@ -6,6 +6,8 @@
6
6
  #include <stdlib.h>
7
7
  #include <string.h>
8
8
 
9
+ #include "refs.h"
10
+ #include "metaschemas.h"
9
11
  #include "schema_internal.h"
10
12
  #include "teptris/teptris.h"
11
13
 
@@ -26,17 +28,75 @@ static sch_node *sch_node_new(sctx *c) {
26
28
 
27
29
  /* ------------------------------------------------------------- memo -- */
28
30
 
31
+ /* pointer-keyed open addressing (FNV-1a over the pointer bytes); the
32
+ * slot holds the entry index + 1, 0 = empty. Compile is O(n), not
33
+ * O(n^2): memo and node-base lookups are per-node on large schemas. */
34
+ static uint32_t ptr_hash(const void *p) {
35
+ uint64_t x = (uint64_t)(uintptr_t)p;
36
+ uint32_t h = 2166136261u;
37
+ for (size_t i = 0; i < sizeof x; i++) {
38
+ h ^= (uint32_t)(x & 0xff);
39
+ h *= 16777619u;
40
+ x >>= 8;
41
+ }
42
+ return h;
43
+ }
44
+
45
+ static uint32_t *hash_build(sctx *c, const void **keys, size_t n,
46
+ size_t *cap_out) {
47
+ size_t ncap = 64;
48
+ while (ncap * 7 / 10 < n + 1) ncap *= 2;
49
+ uint32_t *nt = teptris_arena_alloc(c->a, ncap * sizeof(uint32_t));
50
+ memset(nt, 0, ncap * sizeof(uint32_t));
51
+ uint32_t mask = (uint32_t)ncap - 1;
52
+ for (size_t i = 0; i < n; i++) {
53
+ size_t slot = ptr_hash(keys[i]) & mask;
54
+ while (nt[slot] != 0) slot = (slot + 1) & mask;
55
+ nt[slot] = (uint32_t)(i + 1);
56
+ }
57
+ *cap_out = ncap;
58
+ return nt;
59
+ }
60
+
61
+ /* entries: base of an array whose stride is `stride`; the key sits in
62
+ * the first field (offset 0) of each entry */
63
+ static size_t hash_find(const uint32_t *tbl, size_t cap, const void *key,
64
+ const char *entries, size_t stride) {
65
+ if (tbl == NULL || cap == 0) return SIZE_MAX;
66
+ uint32_t mask = (uint32_t)cap - 1;
67
+ size_t slot = ptr_hash(key) & mask;
68
+ while (tbl[slot] != 0) {
69
+ size_t idx = tbl[slot] - 1;
70
+ const void *k = *(const void *const *)(entries + idx * stride);
71
+ if (k == key) return idx;
72
+ slot = (slot + 1) & mask;
73
+ }
74
+ return SIZE_MAX;
75
+ }
76
+
77
+ static void memo_hash_put(sctx *c, const jv *v) {
78
+ if (c->n_memo + 1 > c->memo_hash_cap * 7 / 10) {
79
+ /* rebuild from the entry list */
80
+ const void **keys = teptris_arena_alloc(
81
+ c->a, (c->n_memo + 1) * sizeof(*keys));
82
+ for (size_t i = 0; i < c->n_memo; i++) keys[i] = c->memo[i].src;
83
+ c->memo_hash = hash_build(c, keys, c->n_memo, &c->memo_hash_cap);
84
+ }
85
+ uint32_t mask = (uint32_t)c->memo_hash_cap - 1;
86
+ size_t slot = ptr_hash(v) & mask;
87
+ while (c->memo_hash[slot] != 0) slot = (slot + 1) & mask;
88
+ c->memo_hash[slot] = (uint32_t)c->n_memo + 1;
89
+ }
90
+
29
91
  static sch_node *memo_get(sctx *c, const jv *v, bool *found, bool *busy) {
30
92
  *found = false;
31
93
  *busy = false;
32
- for (size_t i = 0; i < c->n_memo; i++) {
33
- if (c->memo[i].src == v) {
34
- *found = true;
35
- *busy = c->memo[i].in_progress;
36
- return c->memo[i].node;
37
- }
38
- }
39
- return NULL;
94
+ size_t idx = hash_find(c->memo_hash, c->memo_hash_cap, v,
95
+ (const char *)c->memo, sizeof(*c->memo));
96
+ if (idx == SIZE_MAX) return NULL;
97
+ *found = true;
98
+ *busy = c->memo[idx].in_progress;
99
+ return c->memo[idx].node;
40
100
  }
41
101
 
42
102
  static void memo_put(sctx *c, const jv *v, sch_node *n) {
@@ -45,7 +105,9 @@ static void memo_put(sctx *c, const jv *v, sch_node *n) {
45
105
  void *nm = teptris_arena_alloc(c->a, c->cap_memo * sizeof(*c->memo));
46
106
  if (c->n_memo > 0) memcpy(nm, c->memo, c->n_memo * sizeof(*c->memo));
47
107
  c->memo = nm;
108
+ c->memo_hash_cap = 0; /* keys moved: rebuild on the next put */
48
109
  }
110
+ memo_hash_put(c, v);
49
111
  c->memo[c->n_memo].src = v;
50
112
  c->memo[c->n_memo].node = n;
51
113
  c->memo[c->n_memo].in_progress = true;
@@ -53,23 +115,23 @@ static void memo_put(sctx *c, const jv *v, sch_node *n) {
53
115
  }
54
116
 
55
117
  static void memo_done(sctx *c, const jv *v) {
56
- for (size_t i = 0; i < c->n_memo; i++) {
57
- if (c->memo[i].src == v) {
58
- c->memo[i].in_progress = false;
59
- return;
60
- }
61
- }
118
+ size_t idx = hash_find(c->memo_hash, c->memo_hash_cap, v,
119
+ (const char *)c->memo, sizeof(*c->memo));
120
+ if (idx != SIZE_MAX) c->memo[idx].in_progress = false;
62
121
  }
63
122
 
64
123
  /* --------------------------------------------------- JSON Pointer -- */
65
124
 
66
- /* resolve "#", "#/a/b", or "#anchor" to a jv subtree */
125
+ /* resolve "#", "#/a/b", or "#anchor" to a jv subtree of the CURRENT
126
+ * resource (a plain "#" names the resource the walk is inside, which
127
+ * is the document root for single-resource schemas) */
67
128
  static jv *resolve_local(sctx *c, const char *ref) {
68
129
  if (ref[0] != '#') return NULL;
69
- if (ref[1] == '\0') return c->root;
130
+ jv *root = c->cur_root != NULL ? c->cur_root : c->root;
131
+ if (ref[1] == '\0') return root;
70
132
  if (ref[1] == '/') {
71
133
  /* JSON Pointer with ~0/~1 unescaping */
72
- jv *cur = c->root;
134
+ jv *cur = root;
73
135
  const char *p = ref + 2;
74
136
  while (*p != '\0') {
75
137
  if (cur == NULL || jv_kind_of(cur) != JV_OBJECT) return NULL;
@@ -83,6 +145,18 @@ static jv *resolve_local(sctx *c, const char *ref) {
83
145
  if (p[i] == '~' && i + 1 < tlen) {
84
146
  tok[o++] = p[i + 1] == '1' ? '/' : '~';
85
147
  i++;
148
+ } else if (p[i] == '%' && i + 2 < tlen) {
149
+ /* RFC 3986: refs percent-decode before the
150
+ * pointer walk (~1 decodes to '%', %25 to '%') */
151
+ static const char hex[] = "0123456789abcdef";
152
+ const char *hi = strchr(hex, (char)((p[i + 1] | 0x20)));
153
+ const char *lo = strchr(hex, (char)((p[i + 2] | 0x20)));
154
+ if (hi != NULL && lo != NULL) {
155
+ tok[o++] = (char)((hi - hex) * 16 + (lo - hex));
156
+ i += 2;
157
+ } else {
158
+ tok[o++] = p[i];
159
+ }
86
160
  } else {
87
161
  tok[o++] = p[i];
88
162
  }
@@ -118,6 +192,162 @@ sch_node *sch_resolve_pointer(sctx *c, const char *pointer) {
118
192
 
119
193
  /* -------------------------------------------------- anchor collection */
120
194
 
195
+ /* pre-register every $id in the document against its enclosing base:
196
+ * references may point at resources the compile has not reached yet */
197
+ static void node_base_hash_put(sctx *c, const jv *v) {
198
+ if (c->n_node_bases + 1 > c->node_base_hash_cap * 7 / 10) {
199
+ const void **keys = teptris_arena_alloc(
200
+ c->a, (c->n_node_bases + 1) * sizeof(*keys));
201
+ for (size_t i = 0; i < c->n_node_bases; i++) {
202
+ keys[i] = c->node_bases[i].node;
203
+ }
204
+ c->node_base_hash = hash_build(c, keys, c->n_node_bases,
205
+ &c->node_base_hash_cap);
206
+ }
207
+ uint32_t mask = (uint32_t)c->node_base_hash_cap - 1;
208
+ size_t slot = ptr_hash(v) & mask;
209
+ while (c->node_base_hash[slot] != 0) slot = (slot + 1) & mask;
210
+ c->node_base_hash[slot] = (uint32_t)c->n_node_bases + 1;
211
+ }
212
+
213
+ static void node_base_put(sctx *c, const jv *v, const char *abs,
214
+ const char *dir, jv *root, bool recursive) {
215
+ if (c->n_node_bases == c->cap_node_bases) {
216
+ c->cap_node_bases =
217
+ c->cap_node_bases == 0 ? 64 : c->cap_node_bases * 2;
218
+ void *nb = teptris_arena_alloc(
219
+ c->a, c->cap_node_bases * sizeof(*c->node_bases));
220
+ if (c->n_node_bases > 0) {
221
+ memcpy(nb, c->node_bases, c->n_node_bases * sizeof(*c->node_bases));
222
+ }
223
+ c->node_bases = nb;
224
+ c->node_base_hash_cap = 0; /* keys moved: rebuild on the next put */
225
+ }
226
+ node_base_hash_put(c, v);
227
+ struct node_base *nb = &c->node_bases[c->n_node_bases++];
228
+ nb->node = v;
229
+ nb->abs = abs;
230
+ nb->dir = dir;
231
+ nb->root = root;
232
+ nb->recursive = recursive;
233
+ }
234
+
235
+ static void collect_ids(sctx *c, jv *v, const char *base, const char *dir,
236
+ jv *res_root, bool res_recursive) {
237
+ if (v == NULL) return;
238
+ if (jv_kind_of(v) == JV_OBJECT) {
239
+ /* compute this node's own base first: a $id applies to the
240
+ * resource's own subtree, including its own $refs */
241
+ bool recursive_root = res_recursive;
242
+ jv *idv = jv_member_get(v, "$id");
243
+ if (idv != NULL && jv_kind_of(idv) == JV_STRING) {
244
+ const char *idtext = jv_str(idv);
245
+ if (idtext[0] == '#') {
246
+ /* a fragment-only $id aliases an anchor of the current
247
+ * resource (2019-09+: same as $anchor) */
248
+ if (c->n_anchors == c->cap_anchors) {
249
+ c->cap_anchors =
250
+ c->cap_anchors == 0 ? 8 : c->cap_anchors * 2;
251
+ void *na = teptris_arena_alloc(
252
+ c->a, c->cap_anchors * sizeof(*c->anchors));
253
+ if (c->n_anchors > 0) {
254
+ memcpy(na, c->anchors,
255
+ c->n_anchors * sizeof(*c->anchors));
256
+ }
257
+ c->anchors = na;
258
+ }
259
+ c->anchors[c->n_anchors].name = idtext + 1;
260
+ c->anchors[c->n_anchors].target = v;
261
+ c->n_anchors++;
262
+ /* the subtree still records with the current base:
263
+ * nested $ids and refs beneath it keep resolving */
264
+ }
265
+ char abs[TEPTRIS_URI_MAX];
266
+ if (teptris_uri_join(base, idtext, abs, sizeof abs)) {
267
+ char *held = teptris_arena_alloc(c->a, strlen(abs) + 1);
268
+ strcpy(held, abs);
269
+ base = held;
270
+ res_root = v; /* a new resource boundary */
271
+ /* does this resource root declare $recursiveAnchor
272
+ * true? (2019-09: it enables the dynamic $recursiveRef
273
+ * within the resource) */
274
+ jv *ra = jv_member_get(v, "$recursiveAnchor");
275
+ recursive_root = ra != NULL && jv_kind_of(ra) == JV_BOOL &&
276
+ jv_bool(ra);
277
+ if (strncmp(abs, "file://", 7) == 0) {
278
+ char path[TEPTRIS_URI_MAX];
279
+ teptris_file_uri_to_path(abs, path, sizeof path);
280
+ char *slash = strrchr(path, '/');
281
+ char *held_dir = teptris_arena_alloc(
282
+ c->a, (slash != NULL ? (size_t)(slash - path) : 0) + 2);
283
+ if (slash != NULL) {
284
+ size_t n = (size_t)(slash - path) + 1;
285
+ memcpy(held_dir, path, n);
286
+ held_dir[n] = '\0';
287
+ } else {
288
+ held_dir[0] = '\0';
289
+ }
290
+ dir = held_dir;
291
+ }
292
+ } else if ((strncmp(idtext, "http://", 7) == 0 ||
293
+ strncmp(idtext, "https://", 8) == 0)) {
294
+ /* an http-space $id names an in-memory resource (the
295
+ * document itself or one of its subtrees): it registers
296
+ * under its own text and nested relative references
297
+ * merge against that space; nothing ever loads from the
298
+ * network */
299
+ char *held_id = teptris_arena_alloc(c->a, strlen(idtext) + 1);
300
+ strcpy(held_id, idtext);
301
+ base = held_id;
302
+ res_root = v;
303
+ jv *ra = jv_member_get(v, "$recursiveAnchor");
304
+ recursive_root = ra != NULL && jv_kind_of(ra) == JV_BOOL &&
305
+ jv_bool(ra);
306
+ }
307
+ }
308
+ node_base_put(c, v, base, dir, res_root, recursive_root);
309
+ const char *next_base = base;
310
+ const char *next_dir = dir;
311
+ /* the resource this $id names is registered under the
312
+ * already-joined base */
313
+ if (idv != NULL && base[0] != '\0') {
314
+ bool known = false;
315
+ for (size_t i = 0; i < c->n_resources; i++) {
316
+ if (strcmp(c->resources[i].abs, base) == 0) {
317
+ known = true;
318
+ break;
319
+ }
320
+ }
321
+ if (!known && c->n_resources < 128) {
322
+ if (c->n_resources == c->cap_resources) {
323
+ c->cap_resources =
324
+ c->cap_resources == 0 ? 8 : c->cap_resources * 2;
325
+ void *nr = teptris_arena_alloc(
326
+ c->a, c->cap_resources * sizeof(*c->resources));
327
+ if (c->n_resources > 0) {
328
+ memcpy(nr, c->resources,
329
+ c->n_resources * sizeof(*c->resources));
330
+ }
331
+ c->resources = nr;
332
+ }
333
+ teptris_resource *res = &c->resources[c->n_resources++];
334
+ res->abs = base;
335
+ res->root = v;
336
+ res->node = NULL;
337
+ res->recursive_anchor = recursive_root;
338
+ }
339
+ }
340
+ for (size_t i = 0; i < jv_member_count(v); i++) {
341
+ collect_ids(c, jv_member_val(v, i), next_base, next_dir, res_root,
342
+ recursive_root);
343
+ }
344
+ } else if (jv_kind_of(v) == JV_ARRAY) {
345
+ for (size_t i = 0; i < jv_item_count(v); i++) {
346
+ collect_ids(c, jv_item(v, i), base, dir, res_root, res_recursive);
347
+ }
348
+ }
349
+ }
350
+
121
351
  static void collect_anchors(sctx *c, jv *v) {
122
352
  if (v == NULL) return;
123
353
  if (jv_kind_of(v) == JV_OBJECT) {
@@ -167,6 +397,88 @@ static void collect_anchors(sctx *c, jv *v) {
167
397
  }
168
398
  }
169
399
 
400
+ /* ------------------------------------------------ self scope frames -- */
401
+
402
+ /* the anchors (dynamic or recursive) inside a schema's own subtree,
403
+ * self included, in document order: applying the schema puts all of
404
+ * them in dynamic scope, whether or not the walk reaches each one.
405
+ * $defs bodies compile lazily, so the walk compiles what it needs. */
406
+ static void build_self_scope(sctx *c, const jv *v) {
407
+ if (v == NULL) return;
408
+ if (jv_kind_of(v) == JV_OBJECT) {
409
+ bool found = false, busy = false;
410
+ sch_node *cn = memo_get(c, v, &found, &busy);
411
+ if (!found) cn = sch_compile_node(c, v);
412
+ if (cn != NULL &&
413
+ (cn->dynamic_anchor != NULL || cn->recursive_anchor != NULL)) {
414
+ if (c->n_entry_acc == c->cap_entry_acc) {
415
+ c->cap_entry_acc =
416
+ c->cap_entry_acc == 0 ? 8 : c->cap_entry_acc * 2;
417
+ void *ne = teptris_arena_alloc(
418
+ c->a, c->cap_entry_acc * sizeof(*c->entry_acc));
419
+ if (c->n_entry_acc > 0) {
420
+ memcpy(ne, c->entry_acc,
421
+ c->n_entry_acc * sizeof(*c->entry_acc));
422
+ }
423
+ c->entry_acc = ne;
424
+ }
425
+ c->entry_acc[c->n_entry_acc].name =
426
+ cn->dynamic_anchor != NULL ? cn->dynamic_anchor : "";
427
+ c->entry_acc[c->n_entry_acc].node = cn;
428
+ c->entry_acc[c->n_entry_acc].res = cn->res_id;
429
+ c->n_entry_acc++;
430
+ }
431
+ for (size_t i = 0; i < jv_member_count(v); i++) {
432
+ build_self_scope(c, jv_member_val(v, i));
433
+ }
434
+ } else if (jv_kind_of(v) == JV_ARRAY) {
435
+ for (size_t i = 0; i < jv_item_count(v); i++) {
436
+ build_self_scope(c, jv_item(v, i));
437
+ }
438
+ }
439
+ }
440
+
441
+ static void attach_self_scope(sctx *c, sch_node *owner, const jv *self) {
442
+ /* the walk may compile a lazily-seen $defs body whose own attach
443
+ * re-enters this function: my entries start at the base and the
444
+ * accumulator is handed back to my caller untouched */
445
+ size_t base = c->n_entry_acc;
446
+ build_self_scope(c, self);
447
+ size_t mine = c->n_entry_acc - base;
448
+ if (mine == 0) return;
449
+ owner->self_scope =
450
+ teptris_arena_alloc(c->a, mine * sizeof(*owner->self_scope));
451
+ memcpy(owner->self_scope, &c->entry_acc[base],
452
+ mine * sizeof(*owner->self_scope));
453
+ owner->n_self_scope = mine;
454
+ c->n_entry_acc = base;
455
+ }
456
+
457
+ /* the first $dynamicAnchor with this name inside the CURRENT resource
458
+ * (the bookend: a fragment-only $dynamicRef initially resolves here) */
459
+ static jv *resource_local_anchor(sctx *c, const char *name) {
460
+ jv *root = c->cur_root != NULL ? c->cur_root : c->root;
461
+ if (root == NULL || jv_kind_of(root) != JV_OBJECT) return NULL;
462
+ size_t base = c->n_entry_acc;
463
+ build_self_scope(c, root);
464
+ size_t mine = c->n_entry_acc - base;
465
+ for (size_t i = 0; i < mine; i++) {
466
+ if (strcmp(c->entry_acc[base + i].name, name) == 0) {
467
+ /* recover the anchor's jv through the compiled node: the
468
+ * walk was document order, so the first match is the
469
+ * resource-local initial resolution */
470
+ for (size_t m = 0; m < c->n_memo; m++) {
471
+ if (c->memo[m].node == c->entry_acc[base + i].node) {
472
+ return (jv *)c->memo[m].src;
473
+ }
474
+ }
475
+ return NULL;
476
+ }
477
+ }
478
+ c->n_entry_acc = base;
479
+ return NULL;
480
+ }
481
+
170
482
  /* ----------------------------------------------------------- compile -- */
171
483
 
172
484
  static int type_bit(const char *name) {
@@ -176,6 +488,7 @@ static int type_bit(const char *name) {
176
488
  if (strcmp(name, "boolean") == 0) return ST_BOOLEAN;
177
489
  if (strcmp(name, "object") == 0) return ST_OBJECT;
178
490
  if (strcmp(name, "array") == 0) return ST_ARRAY;
491
+ if (strcmp(name, "null") == 0) return ST_NULL;
179
492
  return 0;
180
493
  }
181
494
 
@@ -327,6 +640,275 @@ static bool sch_compile_dep_schema(sctx *c, sch_node *n, const char *key,
327
640
  return true;
328
641
  }
329
642
 
643
+ /* resolve a non-fragment $ref against the current resource's base:
644
+ * the document part names a resource (file-backed), the fragment
645
+ * names a subtree within it. NULL with *remote set when the target
646
+ * is a network URI the library must not touch. */
647
+ /* a builtin resource (the vendored metaschemas): parse once, register
648
+ * in the resource table, collect its anchors so its own refs resolve */
649
+ static jv *load_builtin_resource(sctx *c, const char *uri,
650
+ const char *json) {
651
+ size_t len = strlen(json);
652
+ char jerr[160] = {0};
653
+ jv *root = json_read(c->a, json, len, jerr, sizeof jerr);
654
+ if (root == NULL) return NULL;
655
+
656
+ if (c->n_resources == c->cap_resources) {
657
+ c->cap_resources = c->cap_resources == 0 ? 8 : c->cap_resources * 2;
658
+ void *nr = teptris_arena_alloc(
659
+ c->a, c->cap_resources * sizeof(*c->resources));
660
+ if (c->n_resources > 0) {
661
+ memcpy(nr, c->resources, c->n_resources * sizeof(*c->resources));
662
+ }
663
+ c->resources = nr;
664
+ }
665
+ teptris_resource *res = &c->resources[c->n_resources++];
666
+ res->abs = teptris_arena_alloc(c->a, strlen(uri) + 1);
667
+ strcpy((char *)res->abs, uri);
668
+ res->root = root;
669
+ res->node = NULL;
670
+ jv *ra = jv_kind_of(root) == JV_OBJECT
671
+ ? jv_member_get(root, "$recursiveAnchor")
672
+ : NULL;
673
+ res->recursive_anchor =
674
+ ra != NULL && jv_kind_of(ra) == JV_BOOL && jv_bool(ra);
675
+ collect_anchors(c, root);
676
+ bool recursive = res->recursive_anchor;
677
+ collect_ids(c, root, uri, NULL, root, recursive);
678
+ return root;
679
+ }
680
+
681
+ static jv *resolve_uri_ref(sctx *c, const char *r, bool *remote);
682
+
683
+ static jv *find_builtin_resource(sctx *c, const char *uri) {
684
+ for (size_t i = 0; i < teptris_n_builtin_resources; i++) {
685
+ if (strcmp(teptris_builtin_resources[i].uri, uri) == 0) {
686
+ return load_builtin_resource(c, uri,
687
+ teptris_builtin_resources[i].json);
688
+ }
689
+ }
690
+ return NULL;
691
+ }
692
+
693
+ /* the dialect's vocabulary mask: a custom metaschema's $vocabulary
694
+ * decides which standard vocabularies the schema uses (8.1.2); an
695
+ * unrecognized or unresolvable $schema keeps all of them */
696
+ static void detect_vocab(sctx *c, jv *root) {
697
+ c->vocab_mask = TEPTRIS_VOC_ALL;
698
+ if (jv_kind_of(root) != JV_OBJECT) return;
699
+ jv *sv = jv_member_get(root, "$schema");
700
+ if (sv == NULL || jv_kind_of(sv) != JV_STRING) return;
701
+ const char *s = jv_str(sv);
702
+ if (strstr(s, "draft-04/") != NULL || strstr(s, "draft-06/") != NULL ||
703
+ strstr(s, "draft-07/") != NULL ||
704
+ strstr(s, "/draft/2019-09/schema") != NULL ||
705
+ strstr(s, "/draft/2020-12/schema") != NULL) {
706
+ return; /* a standard dialect: every vocabulary */
707
+ }
708
+ bool remote = false;
709
+ jv *meta = resolve_uri_ref(c, s, &remote);
710
+ if (meta == NULL || jv_kind_of(meta) != JV_OBJECT) return;
711
+ jv *voc = jv_member_get(meta, "$vocabulary");
712
+ if (voc == NULL || jv_kind_of(voc) != JV_OBJECT) return;
713
+ /* only listed vocabularies are in the dialect; core is mandatory */
714
+ c->vocab_mask = 0;
715
+ for (size_t i = 0; i < jv_member_count(voc); i++) {
716
+ const char *uri = jv_member_key(voc, i);
717
+ jv *req = jv_member_val(voc, i);
718
+ if (req == NULL || jv_kind_of(req) != JV_BOOL || !jv_bool(req)) {
719
+ continue;
720
+ }
721
+ unsigned bit = 0;
722
+ if (strstr(uri, "/vocab/applicator") != NULL) {
723
+ bit = TEPTRIS_VOC_APPLICATOR;
724
+ } else if (strstr(uri, "/vocab/unevaluated") != NULL) {
725
+ bit = TEPTRIS_VOC_UNEVALUATED;
726
+ } else if (strstr(uri, "/vocab/validation") != NULL) {
727
+ bit = TEPTRIS_VOC_VALIDATION;
728
+ } else if (strstr(uri, "/vocab/format") != NULL) {
729
+ bit = TEPTRIS_VOC_FORMAT;
730
+ } else if (strstr(uri, "/vocab/content") != NULL) {
731
+ bit = TEPTRIS_VOC_CONTENT;
732
+ }
733
+ c->vocab_mask |= bit;
734
+ }
735
+ }
736
+
737
+ /* the vocabulary a keyword belongs to (0 = core or unknown: ungated) */
738
+ static unsigned keyword_vocab(const char *k) {
739
+ static const struct { const char *name; unsigned bit; } tbl[] = {
740
+ {"allOf", TEPTRIS_VOC_APPLICATOR},
741
+ {"anyOf", TEPTRIS_VOC_APPLICATOR},
742
+ {"oneOf", TEPTRIS_VOC_APPLICATOR},
743
+ {"not", TEPTRIS_VOC_APPLICATOR},
744
+ {"if", TEPTRIS_VOC_APPLICATOR},
745
+ {"then", TEPTRIS_VOC_APPLICATOR},
746
+ {"else", TEPTRIS_VOC_APPLICATOR},
747
+ {"properties", TEPTRIS_VOC_APPLICATOR},
748
+ {"patternProperties", TEPTRIS_VOC_APPLICATOR},
749
+ {"additionalProperties", TEPTRIS_VOC_APPLICATOR},
750
+ {"additionalItems", TEPTRIS_VOC_APPLICATOR},
751
+ {"propertyNames", TEPTRIS_VOC_APPLICATOR},
752
+ {"items", TEPTRIS_VOC_APPLICATOR},
753
+ {"prefixItems", TEPTRIS_VOC_APPLICATOR},
754
+ {"contains", TEPTRIS_VOC_APPLICATOR},
755
+ {"dependentSchemas", TEPTRIS_VOC_APPLICATOR},
756
+ {"unevaluatedProperties", TEPTRIS_VOC_UNEVALUATED},
757
+ {"unevaluatedItems", TEPTRIS_VOC_UNEVALUATED},
758
+ {"type", TEPTRIS_VOC_VALIDATION},
759
+ {"enum", TEPTRIS_VOC_VALIDATION},
760
+ {"const", TEPTRIS_VOC_VALIDATION},
761
+ {"multipleOf", TEPTRIS_VOC_VALIDATION},
762
+ {"maximum", TEPTRIS_VOC_VALIDATION},
763
+ {"exclusiveMaximum", TEPTRIS_VOC_VALIDATION},
764
+ {"minimum", TEPTRIS_VOC_VALIDATION},
765
+ {"exclusiveMinimum", TEPTRIS_VOC_VALIDATION},
766
+ {"maxLength", TEPTRIS_VOC_VALIDATION},
767
+ {"minLength", TEPTRIS_VOC_VALIDATION},
768
+ {"pattern", TEPTRIS_VOC_VALIDATION},
769
+ {"maxItems", TEPTRIS_VOC_VALIDATION},
770
+ {"minItems", TEPTRIS_VOC_VALIDATION},
771
+ {"uniqueItems", TEPTRIS_VOC_VALIDATION},
772
+ {"maxContains", TEPTRIS_VOC_VALIDATION},
773
+ {"minContains", TEPTRIS_VOC_VALIDATION},
774
+ {"maxProperties", TEPTRIS_VOC_VALIDATION},
775
+ {"minProperties", TEPTRIS_VOC_VALIDATION},
776
+ {"required", TEPTRIS_VOC_VALIDATION},
777
+ {"dependentRequired", TEPTRIS_VOC_VALIDATION},
778
+ {"dependentExclusive", TEPTRIS_VOC_VALIDATION},
779
+ {"format", TEPTRIS_VOC_FORMAT},
780
+ {"contentEncoding", TEPTRIS_VOC_CONTENT},
781
+ {"contentMediaType", TEPTRIS_VOC_CONTENT},
782
+ {"contentSchema", TEPTRIS_VOC_CONTENT},
783
+ };
784
+ for (size_t i = 0; i < sizeof tbl / sizeof tbl[0]; i++) {
785
+ if (strcmp(tbl[i].name, k) == 0) return tbl[i].bit;
786
+ }
787
+ return 0;
788
+ }
789
+
790
+ static jv *resolve_uri_ref(sctx *c, const char *r, bool *remote) {
791
+ *remote = false;
792
+ char doc[TEPTRIS_URI_MAX];
793
+ char frag[TEPTRIS_URI_MAX];
794
+ teptris_uri_split(r, doc, sizeof doc, frag, sizeof frag);
795
+ if (doc[0] == '\0') {
796
+ /* a fragment-only reference names a subtree of the current
797
+ * resource (a $id like "#anchor" aliases an anchor) */
798
+ char refbuf[TEPTRIS_URI_MAX];
799
+ snprintf(refbuf, sizeof refbuf, "#%s", frag);
800
+ return resolve_local(c, refbuf);
801
+ }
802
+ /* the vendored metaschemas resolve from the built-in table: a
803
+ * parser library never touches the network, and these URIs are
804
+ * part of the dialect itself */
805
+ {
806
+ jv *builtin = find_builtin_resource(c, doc);
807
+ if (builtin != NULL) return builtin;
808
+ }
809
+ /* network targets are rejected outright, with or without a base */
810
+ if (strncmp(doc, "http://", 7) == 0 || strncmp(doc, "https://", 8) == 0) {
811
+ *remote = true;
812
+ return NULL;
813
+ }
814
+ /* candidate bases: the base URI in effect, then the resource's
815
+ * file directory (a remote $id naming a file the compile loaded);
816
+ * each candidate tries the resource table, then the filesystem.
817
+ * Relative references join whichever base is fs-backed when the
818
+ * base URI itself is not. */
819
+ char abs[TEPTRIS_URI_MAX];
820
+ jv *root = NULL;
821
+ const char *bases[2];
822
+ size_t n_bases = 0;
823
+ bases[n_bases++] = c->cur_abs;
824
+ if (c->cur_dir != NULL && c->cur_dir[0] != '\0') {
825
+ bases[n_bases++] = c->cur_dir;
826
+ }
827
+ for (size_t bi = 0; bi < n_bases && root == NULL; bi++) {
828
+ const char *base = bases[bi];
829
+ if (base == NULL || base[0] == '\0') continue;
830
+ if (!teptris_uri_join(base, doc, abs, sizeof abs)) {
831
+ if (bi == n_bases - 1) {
832
+ *remote = true;
833
+ return NULL;
834
+ }
835
+ continue;
836
+ }
837
+ for (size_t i = 0; i < c->n_resources; i++) {
838
+ if (strcmp(c->resources[i].abs, abs) == 0) {
839
+ root = c->resources[i].root;
840
+ break;
841
+ }
842
+ }
843
+ if (root == NULL) root = find_builtin_resource(c, abs);
844
+ if (root == NULL && abs[0] != '\0' &&
845
+ strncmp(abs, "http://", 7) != 0 &&
846
+ strncmp(abs, "https://", 8) != 0 && strncmp(abs, "urn:", 4) != 0) {
847
+ root = sch_load_resource(c, abs);
848
+ if (root != NULL) {
849
+ /* the loaded file joins the resource model: its own
850
+ * $ids register and its nodes record bases so its
851
+ * internal refs resolve within it */
852
+ char path[TEPTRIS_URI_MAX];
853
+ teptris_file_uri_to_path(abs, path, sizeof path);
854
+ char *slash = strrchr(path, '/');
855
+ char dirbuf[TEPTRIS_URI_MAX];
856
+ if (slash != NULL) {
857
+ size_t n = (size_t)(slash - path) + 1;
858
+ if (n < sizeof dirbuf) {
859
+ memcpy(dirbuf, path, n);
860
+ dirbuf[n] = '\0';
861
+ } else {
862
+ dirbuf[0] = '\0';
863
+ }
864
+ } else {
865
+ dirbuf[0] = '\0';
866
+ }
867
+ char *held = teptris_arena_alloc(c->a, strlen(dirbuf) + 1);
868
+ strcpy(held, dirbuf);
869
+ char abs_held[TEPTRIS_URI_MAX];
870
+ snprintf(abs_held, sizeof abs_held, "%s", abs);
871
+ char *held_abs = teptris_arena_alloc(c->a, strlen(abs_held) + 1);
872
+ strcpy(held_abs, abs_held);
873
+ bool loaded_recursive = false;
874
+ if (jv_kind_of(root) == JV_OBJECT) {
875
+ jv *ra = jv_member_get(root, "$recursiveAnchor");
876
+ loaded_recursive = ra != NULL &&
877
+ jv_kind_of(ra) == JV_BOOL && jv_bool(ra);
878
+ }
879
+ collect_ids(c, root, held_abs, held, root, loaded_recursive);
880
+ }
881
+ }
882
+ }
883
+ if (root == NULL) return NULL;
884
+ if (frag[0] == '\0') return root;
885
+ if (frag[0] == '/') {
886
+ /* JSON Pointer into the loaded resource */
887
+ const char *saved = c->cur_abs;
888
+ jv *saved_root = c->cur_root;
889
+ char refbuf[TEPTRIS_URI_MAX];
890
+ snprintf(refbuf, sizeof refbuf, "#%s", frag);
891
+ c->cur_root = root;
892
+ jv *hit = resolve_local(c, refbuf);
893
+ c->cur_root = saved_root;
894
+ c->cur_abs = saved;
895
+ return hit;
896
+ }
897
+ /* plain-name fragment inside the loaded resource: an $anchor, or
898
+ * for a $dynamicRef the resource's own $dynamicAnchor */
899
+ for (size_t i = 0; i < c->n_anchors; i++) {
900
+ if (strcmp(c->anchors[i].name, frag) == 0) {
901
+ return c->anchors[i].target;
902
+ }
903
+ }
904
+ for (size_t i = 0; i < c->n_dyn_anchors; i++) {
905
+ if (strcmp(c->dyn_anchors[i].name, frag) == 0) {
906
+ return c->dyn_anchors[i].target;
907
+ }
908
+ }
909
+ return NULL;
910
+ }
911
+
330
912
  sch_node *sch_compile_node(sctx *c, const jv *v) {
331
913
  bool found, busy;
332
914
  sch_node *memo_hit = memo_get(c, v, &found, &busy);
@@ -349,10 +931,37 @@ sch_node *sch_compile_node(sctx *c, const jv *v) {
349
931
  sch_node *n = sch_node_new(c);
350
932
  memo_put(c, v, n);
351
933
 
934
+ /* the pre-pass recorded this node's base and resource root: a
935
+ * pointer can reach a subtree whose $id chain the document walk
936
+ * never passes through at compile time */
937
+ const char *saved_abs = c->cur_abs;
938
+ jv *saved_root = c->cur_root;
939
+ const char *saved_dir = c->cur_dir;
940
+ {
941
+ size_t idx = hash_find(c->node_base_hash, c->node_base_hash_cap, v,
942
+ (const char *)c->node_bases,
943
+ sizeof(*c->node_bases));
944
+ if (idx != SIZE_MAX) {
945
+ if (c->node_bases[idx].root != saved_root) {
946
+ c->cur_root = c->node_bases[idx].root;
947
+ }
948
+ c->cur_abs = c->node_bases[idx].abs;
949
+ c->cur_dir = c->node_bases[idx].dir;
950
+ }
951
+ }
952
+ n->res_id = c->cur_root != NULL ? c->cur_root : c->root;
953
+
352
954
  for (size_t i = 0; i < jv_member_count(v); i++) {
353
955
  const char *k = jv_member_key(v, i);
354
956
  jv *val = jv_member_val(v, i);
355
957
 
958
+ unsigned kgate = keyword_vocab(k);
959
+ if (kgate != 0 && (c->vocab_mask & kgate) == 0) {
960
+ /* the dialect dropped this vocabulary: the keyword is an
961
+ * annotation (8.1.2) */
962
+ continue;
963
+ }
964
+
356
965
  if (strcmp(k, "type") == 0) {
357
966
  if (!compile_type(c, n, val, k)) return NULL;
358
967
  } else if (strcmp(k, "$ref") == 0) {
@@ -361,15 +970,28 @@ sch_node *sch_compile_node(sctx *c, const jv *v) {
361
970
  return NULL;
362
971
  }
363
972
  const char *r = jv_str(val);
364
- if (r[0] != '#') {
365
- sfail(c, "remote $ref is not supported (a parser library never touches the network)", r);
366
- return NULL;
367
- }
368
- n->ref = sch_resolve_pointer(c, r);
369
- if (n->ref == NULL) {
370
- sfail(c, "unresolvable $ref", r);
371
- return NULL;
973
+ if (r[0] == '#') {
974
+ n->ref = sch_resolve_pointer(c, r);
975
+ if (n->ref == NULL) {
976
+ sfail(c, "unresolvable $ref", r);
977
+ return NULL;
978
+ }
979
+ } else {
980
+ bool remote = false;
981
+ jv *target = resolve_uri_ref(c, r, &remote);
982
+ if (remote) {
983
+ sfail(c, "remote $ref is not supported (a parser library never touches the network)", r);
984
+ return NULL;
985
+ }
986
+ if (target == NULL) {
987
+ sfail(c, "unresolvable $ref", r);
988
+ return NULL;
989
+ }
990
+ n->ref = sch_compile_node(c, target);
372
991
  }
992
+ if (n->ref == NULL && c->err[0] != '\0') return NULL;
993
+ /* draft 07 and older: a $ref ignores every sibling */
994
+ n->ref_only = c->ref_exclusive;
373
995
  } else if (strcmp(k, "enum") == 0) {
374
996
  if (jv_kind_of(val) != JV_ARRAY) {
375
997
  sfail(c, "enum must be an array", k);
@@ -378,6 +1000,7 @@ sch_node *sch_compile_node(sctx *c, const jv *v) {
378
1000
  n->enum_vals = teptris_arena_alloc(
379
1001
  c->a, (jv_item_count(val) ? jv_item_count(val) : 1) * sizeof(jv *));
380
1002
  n->n_enum = jv_item_count(val);
1003
+ n->enum_empty = jv_item_count(val) == 0;
381
1004
  for (size_t e = 0; e < jv_item_count(val); e++) {
382
1005
  n->enum_vals[e] = jv_item(val, e);
383
1006
  }
@@ -427,7 +1050,7 @@ sch_node *sch_compile_node(sctx *c, const jv *v) {
427
1050
  n->dynamic_anchor = jv_str(val);
428
1051
  } else if (strcmp(k, "$recursiveAnchor") == 0 &&
429
1052
  jv_kind_of(val) == JV_BOOL && jv_bool(val)) {
430
- n->recursive_anchor = "$";
1053
+ n->recursive_anchor = "";
431
1054
  } else if (strcmp(k, "$dynamicRef") == 0 ||
432
1055
  strcmp(k, "$recursiveRef") == 0) {
433
1056
  if (jv_kind_of(val) != JV_STRING) {
@@ -436,18 +1059,37 @@ sch_node *sch_compile_node(sctx *c, const jv *v) {
436
1059
  }
437
1060
  const char *r = jv_str(val);
438
1061
  bool recursive = strcmp(k, "$recursiveRef") == 0;
439
- /* $recursiveRef is always the bare "#"; a pointer-form
440
- * $dynamicRef ("#/pointer") does not participate in
441
- * dynamic scope resolution - the spec and the suite both
442
- * read it exactly like a plain $ref - so only the
443
- * plain-name fragment form reaches the scope machinery */
1062
+ /* $recursiveRef is always the bare "#"; a $dynamicRef may
1063
+ * also name a URI or pointer ("#/pointer", "other.json",
1064
+ * "main.json#/$defs/inner"): none of those forms
1065
+ * participate in dynamic scope resolution - the spec and
1066
+ * the suite both read them exactly like a plain $ref - so
1067
+ * only the plain-name fragment form reaches the scope
1068
+ * machinery */
444
1069
  if (r[0] != '#' || (recursive && r[1] != '\0') ||
445
1070
  (!recursive && r[1] == '\0')) {
446
- sfail(c,
447
- "dynamic $refs are plain-name fragments "
448
- "(\"#anchor\") or pointers (\"#/a/b\")",
449
- r);
450
- return NULL;
1071
+ bool remote = false;
1072
+ jv *target = NULL;
1073
+ if (r[0] != '#') {
1074
+ target = resolve_uri_ref(c, r, &remote);
1075
+ } else {
1076
+ sfail(c,
1077
+ "dynamic $refs are plain-name fragments "
1078
+ "(\"#anchor\") or pointers (\"#/a/b\")",
1079
+ r);
1080
+ return NULL;
1081
+ }
1082
+ if (remote) {
1083
+ sfail(c, "remote dynamic $ref is not supported", r);
1084
+ return NULL;
1085
+ }
1086
+ if (target == NULL) {
1087
+ sfail(c, "unresolvable dynamic $ref", r);
1088
+ return NULL;
1089
+ }
1090
+ n->ref = sch_compile_node(c, target);
1091
+ if (n->ref == NULL && c->err[0] != '\0') return NULL;
1092
+ continue;
451
1093
  }
452
1094
  if (!recursive && strchr(r + 1, '/') != NULL) {
453
1095
  n->ref = sch_resolve_pointer(c, r);
@@ -461,11 +1103,36 @@ sch_node *sch_compile_node(sctx *c, const jv *v) {
461
1103
  * the innermost scope wins at validation time */
462
1104
  jv *target = NULL;
463
1105
  if (recursive) {
464
- target = c->rec_anchor_target != NULL
465
- ? c->rec_anchor_target
466
- : c->root;
1106
+ /* 2019-09: when the ref's own resource carries a true
1107
+ * $recursiveAnchor the resolution is dynamic (the first
1108
+ * anchor entered wins at walk time); otherwise the ref
1109
+ * does not join the dynamic machinery at all and reads
1110
+ * as a plain reference to the resource's root */
1111
+ bool dyn = false;
1112
+ for (size_t r2 = 0; r2 < c->n_resources; r2++) {
1113
+ if (c->resources[r2].root == c->cur_root &&
1114
+ c->resources[r2].recursive_anchor) {
1115
+ dyn = true;
1116
+ }
1117
+ }
1118
+ if (dyn) {
1119
+ target = c->rec_anchor_target != NULL
1120
+ ? c->rec_anchor_target
1121
+ : (c->cur_root != NULL ? c->cur_root
1122
+ : c->root);
1123
+ } else {
1124
+ n->ref = sch_compile_node(
1125
+ c, c->cur_root != NULL ? c->cur_root : c->root);
1126
+ if (n->ref == NULL && c->err[0] != '\0') return NULL;
1127
+ continue;
1128
+ }
467
1129
  } else {
468
- for (size_t d = 0; d < c->n_dyn_anchors && target == NULL; d++) {
1130
+ /* a fragment-only $dynamicRef initially resolves to
1131
+ * the matching $dynamicAnchor in the CURRENT resource
1132
+ * (the bookend); the doc-wide table is the fallback */
1133
+ target = resource_local_anchor(c, r + 1);
1134
+ for (size_t d = 0; d < c->n_dyn_anchors && target == NULL;
1135
+ d++) {
469
1136
  if (strcmp(c->dyn_anchors[d].name, r + 1) == 0) {
470
1137
  target = c->dyn_anchors[d].target;
471
1138
  }
@@ -482,6 +1149,7 @@ sch_node *sch_compile_node(sctx *c, const jv *v) {
482
1149
  if (n->dynamic_ref_target == NULL && c->err[0] != '\0') {
483
1150
  return NULL;
484
1151
  }
1152
+ n->dynamic_recursive = recursive;
485
1153
  } else if (strcmp(k, "contentSchema") == 0) {
486
1154
  n->content_schema = sch_compile_node(c, val);
487
1155
  if (n->content_schema == NULL && c->err[0] != '\0') return NULL;
@@ -710,6 +1378,10 @@ sch_node *sch_compile_node(sctx *c, const jv *v) {
710
1378
  * definitions/$anchor (containers resolved via $ref), unknown
711
1379
  * keywords (extensions per the spec): all ignored */
712
1380
  }
1381
+ c->cur_abs = saved_abs;
1382
+ c->cur_root = saved_root;
1383
+ c->cur_dir = saved_dir;
1384
+ attach_self_scope(c, n, v);
713
1385
  memo_done(c, v);
714
1386
  return n;
715
1387
  }
@@ -746,10 +1418,29 @@ bool sch_decimal_of_double(double d, int64_t *mant, int *exp) {
746
1418
  return true;
747
1419
  }
748
1420
 
1421
+ static teptris_status schema_compile_core(const char *json, size_t len,
1422
+ const char *base_dir, size_t base_len,
1423
+ struct teptris_schema **out,
1424
+ char **diag_buf, size_t *diag_len);
1425
+
1426
+ TEPTRIS_API teptris_status teptris_schema_compile_ex(
1427
+ const char *json, size_t len, const char *base_dir, size_t base_len,
1428
+ struct teptris_schema **out, char **diag_buf, size_t *diag_len) {
1429
+ return schema_compile_core(json, len, base_dir, base_len, out, diag_buf,
1430
+ diag_len);
1431
+ }
1432
+
749
1433
  TEPTRIS_API teptris_status teptris_schema_compile(const char *json, size_t len,
750
1434
  struct teptris_schema **out,
751
1435
  char **diag_buf,
752
1436
  size_t *diag_len) {
1437
+ return schema_compile_core(json, len, NULL, 0, out, diag_buf, diag_len);
1438
+ }
1439
+
1440
+ static teptris_status schema_compile_core(const char *json, size_t len,
1441
+ const char *base_dir, size_t base_len,
1442
+ struct teptris_schema **out,
1443
+ char **diag_buf, size_t *diag_len) {
753
1444
  if (json == NULL || out == NULL) {
754
1445
  return TEPTRIS_ERR_ARG;
755
1446
  }
@@ -800,8 +1491,56 @@ TEPTRIS_API teptris_status teptris_schema_compile(const char *json, size_t len,
800
1491
  c.err = sch->err;
801
1492
  c.err_cap = sizeof sch->err;
802
1493
  sch->err[0] = '\0';
1494
+ c.cur_root = root;
1495
+ if (base_dir != NULL && base_len > 0 && base_len < sizeof c.base_dir) {
1496
+ memcpy(c.base_dir, base_dir, base_len);
1497
+ c.base_dir[base_len] = '\0';
1498
+ char abs[TEPTRIS_URI_MAX];
1499
+ if (c.base_dir[strlen(c.base_dir) - 1] == '/') {
1500
+ snprintf(abs, sizeof abs, "file://%sschema.json", c.base_dir);
1501
+ } else {
1502
+ snprintf(abs, sizeof abs, "file://%s/schema.json", c.base_dir);
1503
+ }
1504
+ char *held = teptris_arena_alloc(&sch->arena, strlen(abs) + 1);
1505
+ strcpy(held, abs);
1506
+ c.cur_abs = held;
1507
+ }
1508
+ if (c.base_dir[0] != '\0') {
1509
+ char *held = teptris_arena_alloc(&sch->arena, strlen(c.base_dir) + 2);
1510
+ sprintf(held, "%s/", c.base_dir);
1511
+ c.cur_dir = held;
1512
+ }
1513
+ /* draft 07 and older make a $ref ignore its siblings; 2019-09+
1514
+ * evaluate $ref alongside them. The root's $schema decides. */
1515
+ if (jv_kind_of(root) == JV_OBJECT) {
1516
+ jv *sv = jv_member_get(root, "$schema");
1517
+ if (sv != NULL && jv_kind_of(sv) == JV_STRING) {
1518
+ const char *s = jv_str(sv);
1519
+ if (strstr(s, "draft-04") != NULL || strstr(s, "draft-06") != NULL ||
1520
+ strstr(s, "draft-07") != NULL) {
1521
+ c.ref_exclusive = true;
1522
+ }
1523
+ }
1524
+ }
803
1525
 
1526
+ bool root_recursive = false;
1527
+ if (jv_kind_of(root) == JV_OBJECT) {
1528
+ jv *ra = jv_member_get(root, "$recursiveAnchor");
1529
+ root_recursive = ra != NULL && jv_kind_of(ra) == JV_BOOL &&
1530
+ jv_bool(ra);
1531
+ }
1532
+ c.cap_resources = 8;
1533
+ c.resources = teptris_arena_alloc(&sch->arena,
1534
+ c.cap_resources * sizeof(*c.resources));
1535
+ teptris_resource *root_res = &c.resources[c.n_resources++];
1536
+ root_res->abs = c.cur_abs != NULL ? c.cur_abs : "";
1537
+ root_res->root = root;
1538
+ root_res->node = NULL;
1539
+ root_res->recursive_anchor = root_recursive;
1540
+ detect_vocab(&c, root);
804
1541
  collect_anchors(&c, root);
1542
+ collect_ids(&c, root, c.cur_abs, c.cur_dir != NULL ? c.cur_dir : c.base_dir,
1543
+ root, root_recursive);
805
1544
  sch_node *compiled = sch_compile_node(&c, root);
806
1545
  if (compiled == NULL) {
807
1546
  if (diag_buf != NULL) {