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.
- checksums.yaml +4 -4
- data/ext/teptris_ext/engine/README.md +1 -1
- data/ext/teptris_ext/engine/src/include/teptris/schema.h +7 -0
- data/ext/teptris_ext/engine/src/include/teptris/version.h +2 -2
- data/ext/teptris_ext/engine/src/teptris/schema/metaschemas.c +243 -0
- data/ext/teptris_ext/engine/src/teptris/schema/metaschemas.h +15 -0
- data/ext/teptris_ext/engine/src/teptris/schema/refs.c +243 -0
- data/ext/teptris_ext/engine/src/teptris/schema/refs.h +24 -0
- data/ext/teptris_ext/engine/src/teptris/schema/schema.c +779 -40
- data/ext/teptris_ext/engine/src/teptris/schema/schema_internal.h +76 -6
- data/ext/teptris_ext/engine/src/teptris/schema/validate.c +87 -11
- data/lib/teptris/version.rb +1 -1
- metadata +5 -1
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
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) {
|