yeptris 0.6.56.1-arm-linux-eabihf → 0.6.58.1-arm-linux-eabihf

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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/ext/yeptris_native/version.h +2 -2
  3. data/lib/yeptris/3.4/native.so +0 -0
  4. data/lib/yeptris/psych/visitors.rb +123 -20
  5. data/lib/yeptris/psych.rb +91 -15
  6. data/lib/yeptris/yaml.rb +92 -3
  7. data/lib/yeptris.rb +1 -1
  8. data/libyeptris.so +0 -0
  9. data/vendor/libyeptris/CMakeLists.txt +1 -1
  10. data/vendor/libyeptris/src/CMakeLists.txt +5 -0
  11. data/vendor/libyeptris/src/include/yeptris/error.h +2 -1
  12. data/vendor/libyeptris/src/include/yeptris/events.h +7 -0
  13. data/vendor/libyeptris/src/include/yeptris/resolve.h +3 -1
  14. data/vendor/libyeptris/src/include/yeptris/validate.h +157 -0
  15. data/vendor/libyeptris/src/include/yeptris.h +1 -0
  16. data/vendor/libyeptris/src/yeptris/dom/dom.c +56 -20
  17. data/vendor/libyeptris/src/yeptris/dom/dom.h +5 -0
  18. data/vendor/libyeptris/src/yeptris/events/iterparse.c +97 -7
  19. data/vendor/libyeptris/src/yeptris/events/pull.c +25 -5
  20. data/vendor/libyeptris/src/yeptris/events/push.c +23 -1
  21. data/vendor/libyeptris/src/yeptris/events/recorder.c +33 -6
  22. data/vendor/libyeptris/src/yeptris/memory/arena.c +9 -1
  23. data/vendor/libyeptris/src/yeptris/memory/pool.c +5 -1
  24. data/vendor/libyeptris/src/yeptris/parse/engine.c +59 -0
  25. data/vendor/libyeptris/src/yeptris/parse.c +22 -8
  26. data/vendor/libyeptris/src/yeptris/resolve/json12.c +103 -0
  27. data/vendor/libyeptris/src/yeptris/resolve/resolver.h +9 -0
  28. data/vendor/libyeptris/src/yeptris/validate/internal.h +102 -0
  29. data/vendor/libyeptris/src/yeptris/validate/jsonschema.c +1022 -0
  30. data/vendor/libyeptris/src/yeptris/validate/regexp.c +972 -0
  31. data/vendor/libyeptris/src/yeptris/validate/validate.c +939 -0
  32. metadata +7 -1
@@ -0,0 +1,157 @@
1
+ /* validate.h — the validation face (issue #882).
2
+ *
3
+ * Two checks over one engine pass:
4
+ *
5
+ * 1. SCHEMA-FAMILY validation (`yeptris_validate`): parse + validate
6
+ * fused. Under YEPTRIS_SCHEMA_12_CORE the walk checks explicit-tag
7
+ * consistency (spec 10.3 resolution) and mapping-key uniqueness
8
+ * (spec 3.2.1.3: keys compare by canonical form — 0o13 and 0xB
9
+ * are the same key). Under YEPTRIS_SCHEMA_12_JSON the spec 10.2
10
+ * family applies: every PLAIN scalar must match a JSON production
11
+ * (the table's `*` -> Error row), mapping keys must be strings,
12
+ * non-JSON tags are rejected; core checks still run.
13
+ *
14
+ * 2. JSON SCHEMA validation (`yeptris_json_schema_validate`): the
15
+ * schema arrives as bytes (parsed with the same engine), the
16
+ * target against it — draft 2020-12 keyword subset (see the entry
17
+ * comment). Unknown keywords are ignored, per 2020-12.
18
+ *
19
+ * Every diagnostic carries a JSON-pointer instance path ("/a/b/0" —
20
+ * ~0/~1 escaped), a YEPTRIS_DIAG_* code, a bounded expected/got
21
+ * detail, and the offending node's 1-based line/column (0 = unknown:
22
+ * scalars whose bytes the DOM owns in its arena have no input
23
+ * position; the path still identifies them exactly).
24
+ *
25
+ * Results are WHOLE REPORTS: one call, no per-diagnostic callback —
26
+ * the caller drains the flat array and frees the report once.
27
+ */
28
+ #ifndef YEPTRIS_VALIDATE_H
29
+ #define YEPTRIS_VALIDATE_H
30
+
31
+ #include <stddef.h>
32
+ #include <stdint.h>
33
+
34
+ #include <yeptris/api.h>
35
+ #include <yeptris/error.h>
36
+ #include <yeptris/parse.h> /* YeptrisDocument, YeptrisSchema */
37
+ #include <yeptris/types.h>
38
+
39
+ #ifdef __cplusplus
40
+ extern "C" {
41
+ #endif
42
+
43
+ /* Diagnostic codes. Appending only — bindings hard-code the values
44
+ * (test_abi pins the head). */
45
+ typedef enum {
46
+ YEPTRIS_DIAG_NONE = 0,
47
+ /* the schema-family walk */
48
+ YEPTRIS_DIAG_TAG_MISMATCH = 1, /* explicit tag contradicts content */
49
+ YEPTRIS_DIAG_KEY_NOT_STRING = 2, /* json12: mapping key is not a string */
50
+ YEPTRIS_DIAG_NON_JSON_TYPE = 3, /* json12: plain scalar matches no
51
+ * JSON production / carries a
52
+ * non-JSON tag (spec 10.2.2 Error) */
53
+ YEPTRIS_DIAG_DUPLICATE_KEY = 4, /* equal keys in one mapping */
54
+ /* the JSON Schema walk */
55
+ YEPTRIS_DIAG_TYPE = 5,
56
+ YEPTRIS_DIAG_REQUIRED = 6,
57
+ YEPTRIS_DIAG_ENUM = 7,
58
+ YEPTRIS_DIAG_CONST = 8,
59
+ YEPTRIS_DIAG_PATTERN = 9,
60
+ YEPTRIS_DIAG_MINIMUM = 10,
61
+ YEPTRIS_DIAG_MAXIMUM = 11,
62
+ YEPTRIS_DIAG_EXCLUSIVE_MINIMUM = 12,
63
+ YEPTRIS_DIAG_EXCLUSIVE_MAXIMUM = 13,
64
+ YEPTRIS_DIAG_MIN_LENGTH = 14,
65
+ YEPTRIS_DIAG_MAX_LENGTH = 15,
66
+ YEPTRIS_DIAG_MIN_ITEMS = 16,
67
+ YEPTRIS_DIAG_MAX_ITEMS = 17,
68
+ YEPTRIS_DIAG_UNIQUE_ITEMS = 18,
69
+ YEPTRIS_DIAG_ADDITIONAL_PROPERTY = 19,
70
+ YEPTRIS_DIAG_ANY_OF = 20,
71
+ YEPTRIS_DIAG_ONE_OF = 21,
72
+ YEPTRIS_DIAG_NOT = 22,
73
+ YEPTRIS_DIAG_REF = 23, /* $ref unresolvable / chain too deep */
74
+ YEPTRIS_DIAG_FALSE_SCHEMA = 24, /* the schema is `false` here */
75
+ YEPTRIS_DIAG_DEPTH = 25, /* recursion guard tripped */
76
+ YEPTRIS_DIAG_LIMIT = 26, /* report truncated (the sentinel
77
+ * rides the LAST slot; more
78
+ * diagnostics were dropped) */
79
+ } YeptrisDiagCode;
80
+
81
+ /* Bounded diagnostic text; always NUL-terminated (over-long paths and
82
+ * details truncate — the same discipline as the error channel). */
83
+ #define YEPTRIS_DIAG_PATH_MAX 256
84
+ #define YEPTRIS_DIAG_DETAIL_MAX 128
85
+
86
+ typedef struct yeptris_diagnostic {
87
+ uint32_t code; /* YeptrisDiagCode */
88
+ uint32_t line; /* 1-based; 0 = unknown */
89
+ uint32_t col; /* 1-based; 0 = unknown */
90
+ uint32_t offset;
91
+ char path[YEPTRIS_DIAG_PATH_MAX];
92
+ char detail[YEPTRIS_DIAG_DETAIL_MAX];
93
+ } yeptris_diagnostic;
94
+ YEPTRIS_CT_ASSERT(sizeof(yeptris_diagnostic) ==
95
+ 16 + YEPTRIS_DIAG_PATH_MAX + YEPTRIS_DIAG_DETAIL_MAX);
96
+
97
+ /* The diagnostics list. Opaque; free it with
98
+ * yeptris_validate_report_free (one call, everything). */
99
+ typedef struct yeptris_validate_report yeptris_validate_report;
100
+
101
+ /* Diagnostic count (0 = the document is valid). */
102
+ YEPTRIS_API size_t yeptris_validate_report_count(const yeptris_validate_report* report);
103
+
104
+ /* Diagnostic i (0-based); NULL out of range. Memory: borrowed from the
105
+ * report — valid until yeptris_validate_report_free. */
106
+ YEPTRIS_API const yeptris_diagnostic*
107
+ yeptris_validate_report_at(const yeptris_validate_report* report, size_t index);
108
+
109
+ YEPTRIS_API void yeptris_validate_report_free(yeptris_validate_report* report);
110
+
111
+ /* Parses `buf` (the same front door as yeptris_parse_ex: encodings,
112
+ * BOM, NEL normalization) and validates every document of the stream
113
+ * against `schema`. Paths are root-relative per document.
114
+ *
115
+ * Status: YEPTRIS_OK (valid; *out is a report — possibly with zero
116
+ * diagnostics for an empty stream); YEPTRIS_ERROR_SCHEMA (invalid:
117
+ * drain *out); YEPTRIS_ERROR_PARSE / DEPTH / ENCODING / MEMORY (the
118
+ * parse itself failed; *out stays NULL); YEPTRIS_ERROR_ARG.
119
+ *
120
+ * Memory: the caller owns *out; free it with
121
+ * yeptris_validate_report_free. */
122
+ YEPTRIS_API YeptrisStatus yeptris_validate(const char* buf, size_t len, YeptrisSchema schema,
123
+ yeptris_validate_report** out);
124
+
125
+ /* Validates an already-parsed document (or stream). The document is
126
+ * not modified; the report borrows nothing from it. */
127
+ YEPTRIS_API YeptrisStatus yeptris_validate_document(YeptrisDocument doc, YeptrisSchema schema,
128
+ yeptris_validate_report** out);
129
+
130
+ /* JSON Schema draft 2020-12 subset (#882 slice 2). `schema` is parsed
131
+ * with the same engine (JSON is YAML flow); document 0 of the stream
132
+ * is the schema. Keyword subset: type, enum, const, pattern, minimum/
133
+ * maximum/exclusiveMinimum/exclusiveMaximum (numeric, 2020-12 form),
134
+ * minLength/maxLength (code points), minItems/maxItems, uniqueItems,
135
+ * items (+ prefixItems; the draft-07 array form rides as prefixItems),
136
+ * properties, required, additionalProperties (bool or schema),
137
+ * allOf/anyOf/oneOf/not, $ref (LOCAL "#/..." pointers only — $defs and
138
+ * definitions both resolve). Boolean schemas (`true`/`false`) apply.
139
+ * Unknown keywords and known keywords with unreadable values are
140
+ * ignored (2020-12). Instance types ride the core-schema resolution
141
+ * (the YAML->JSON data model): "y" is boolean, 1.0 is integer.
142
+ *
143
+ * Status: as yeptris_validate, plus YEPTRIS_ERROR_ARG when the schema
144
+ * bytes hold no document, and YEPTRIS_ERROR_PARSE when they do not
145
+ * parse. A $ref that resolves nowhere is a DIAG_REF diagnostic, not a
146
+ * status. Recursion is depth-guarded (the engine's guard): a
147
+ * self-referential $ref over a deep or cyclic chain reports DIAG_DEPTH
148
+ * / DIAG_REF — never a crash. */
149
+ YEPTRIS_API YeptrisStatus yeptris_json_schema_validate(const char* schema, size_t schema_len,
150
+ const char* buf, size_t len,
151
+ yeptris_validate_report** out);
152
+
153
+ #ifdef __cplusplus
154
+ }
155
+ #endif
156
+
157
+ #endif /* YEPTRIS_VALIDATE_H */
@@ -18,6 +18,7 @@
18
18
  #include <yeptris/parse.h>
19
19
  #include <yeptris/resolve.h>
20
20
  #include <yeptris/types.h>
21
+ #include <yeptris/validate.h>
21
22
  #include <yeptris/version.h>
22
23
 
23
24
  #endif /* YEPTRIS_H */
@@ -26,16 +26,35 @@ static int cap_arena(const yep_dom* d, uint32_t need) {
26
26
  return d->input_len == 0 || (size_t)(d->str_len + need) <= d->input_len * 128 + (1u << 20);
27
27
  }
28
28
 
29
+ /* Doubling with the uint32 ceiling (the audit's wrap finding): the old
30
+ * `while (cap < target) cap *= 2` wrapped to 0 and span forever once a
31
+ * cap crossed 2^31 with a target above it. A cap that cannot double
32
+ * inside uint32 — or a target past the 32-bit space at all — is an
33
+ * alloc failure, never a wrapped capacity. */
34
+ static int dom_cap_next(uint32_t cap, size_t target, uint32_t* out) {
35
+ if (target > UINT32_MAX) {
36
+ return 0;
37
+ }
38
+ while (cap < target) {
39
+ if (cap > UINT32_MAX / 2) {
40
+ return 0;
41
+ }
42
+ cap *= 2;
43
+ }
44
+ *out = cap;
45
+ return 1;
46
+ }
47
+
29
48
  int dom_grow_nodes(yep_dom* d, uint32_t need) {
30
- if (d->ncount + need <= d->ncap) {
49
+ if ((size_t)d->ncount + need <= d->ncap) {
31
50
  return 1;
32
51
  }
33
52
  if (!cap_nodes(d)) {
34
53
  return 0;
35
54
  }
36
- uint32_t ncap = d->ncap ? d->ncap * 2 : 64;
37
- while (ncap < d->ncount + need) {
38
- ncap *= 2;
55
+ uint32_t ncap;
56
+ if (!dom_cap_next(d->ncap ? d->ncap : 64, (size_t)d->ncount + need, &ncap)) {
57
+ return 0;
39
58
  }
40
59
  yep_dnode* nd = yep_pool_alloc(d->pool, (size_t)ncap * sizeof(yep_dnode), 16);
41
60
  if (nd == NULL) {
@@ -50,15 +69,15 @@ int dom_grow_nodes(yep_dom* d, uint32_t need) {
50
69
  }
51
70
 
52
71
  int dom_grow_docs(yep_dom* d, uint32_t need) {
53
- if (d->dcount + need <= d->dcap) {
72
+ if ((size_t)d->dcount + need <= d->dcap) {
54
73
  return 1;
55
74
  }
56
75
  if (d->input_len != 0 && (size_t)d->dcount > d->input_len / 2 + 16) {
57
76
  return 0;
58
77
  }
59
- uint32_t ncap = d->dcap ? d->dcap * 2 : 16;
60
- while (ncap < d->dcount + need) {
61
- ncap *= 2;
78
+ uint32_t ncap;
79
+ if (!dom_cap_next(d->dcap ? d->dcap : 16, (size_t)d->dcount + need, &ncap)) {
80
+ return 0;
62
81
  }
63
82
  uint32_t* nd = yep_pool_alloc(d->pool, (size_t)ncap * sizeof(uint32_t), 16);
64
83
  if (nd == NULL) {
@@ -75,15 +94,15 @@ int dom_grow_docs(yep_dom* d, uint32_t need) {
75
94
  /* Doubles the arena until cap bytes fit; 0 on OOM. Offsets stored in
76
95
  * nodes stay valid across the move. */
77
96
  static int str_grow(yep_dom* d, uint32_t need) {
78
- if (d->str_len + need <= d->str_cap) {
97
+ if ((size_t)d->str_len + need <= d->str_cap) {
79
98
  return 1;
80
99
  }
81
100
  if (!cap_arena(d, need)) {
82
101
  return 0;
83
102
  }
84
- uint32_t cap = d->str_cap ? d->str_cap : 256;
85
- while (cap < d->str_len + need) {
86
- cap *= 2;
103
+ uint32_t cap;
104
+ if (!dom_cap_next(d->str_cap ? d->str_cap : 256, (size_t)d->str_len + need, &cap)) {
105
+ return 0;
87
106
  }
88
107
  char* ns = yep_alloc(d->sys, cap);
89
108
  if (ns == NULL) {
@@ -100,8 +119,15 @@ static int str_grow(yep_dom* d, uint32_t need) {
100
119
 
101
120
  yep_sview yep_dom_str_put(yep_dom* d, const char* p, uint32_t len) {
102
121
  yep_sview sv = {YEP_SV_INPUT, 0}; /* empty: arena offset 0 */
103
- if (len == 0 || !str_grow(d, len)) {
104
- return sv; /* len 0 or OOM: empty view */
122
+ if (len == 0) {
123
+ return sv;
124
+ }
125
+ if (!str_grow(d, len)) {
126
+ /* OOM: an empty view here would silently corrupt the document
127
+ * (the value reads as ""). Record it; dom_place/dom_open_node
128
+ * fail the parse with the memory status. */
129
+ d->oom = 1;
130
+ return sv;
105
131
  }
106
132
  memcpy(d->str + d->str_len, p, len);
107
133
  sv.off = YEP_SV_INPUT | d->str_len;
@@ -172,6 +198,11 @@ uint32_t dom_open_node(yep_dom* d, uint8_t kind, const yep_view* tag, const yep_
172
198
  n->kind = kind;
173
199
  n->tag = dom_str_in(d, tag, 0);
174
200
  n->anchor = dom_str_in(d, anchor, anchor_borrowed);
201
+ if (d->oom) {
202
+ /* the tag/anchor arena write failed: refuse the node (the slot
203
+ * is uncommitted — the next open re-initializes it) */
204
+ return UINT32_MAX;
205
+ }
175
206
  n->style = style;
176
207
  n->implicit = implicit;
177
208
  n->flow = flow;
@@ -213,8 +244,8 @@ static int dom_anchor_set(yep_dom* d, uint32_t ordinal, uint32_t node) {
213
244
  cap = sized;
214
245
  }
215
246
  }
216
- while (cap < ordinal) {
217
- cap *= 2;
247
+ if (!dom_cap_next(cap, ordinal, &cap)) {
248
+ return -1; /* the wrap-checked doubling (audit round 5) */
218
249
  }
219
250
  uint32_t* na = yep_alloc(d->sys, cap * sizeof(*na));
220
251
  if (na == NULL) {
@@ -271,9 +302,9 @@ static int dom_mut_grow(yep_dom* d, uint32_t need) {
271
302
  if (need <= d->mut_tbl_cap) {
272
303
  return 1;
273
304
  }
274
- uint32_t cap = d->mut_tbl_cap ? d->mut_tbl_cap : 64;
275
- while (cap < need) {
276
- cap *= 2;
305
+ uint32_t cap;
306
+ if (!dom_cap_next(d->mut_tbl_cap ? d->mut_tbl_cap : 64, need, &cap)) {
307
+ return 0; /* the wrap-checked doubling (audit round 5) */
277
308
  }
278
309
  uint8_t* na = yep_alloc(d->sys, (size_t)cap / 8 + 1);
279
310
  uint16_t* nd = yep_alloc(d->sys, (size_t)cap * sizeof(uint16_t));
@@ -335,8 +366,13 @@ void dom_mut_set_depth(yep_dom* d, uint32_t id, uint16_t depth) {
335
366
  }
336
367
 
337
368
  /* Places a completed node: value for a pending key, child of the top
338
- * collection, or document root. */
369
+ * collection, or document root. Every builder path ends here, so it is
370
+ * also the OOM choke: an arena write that failed (d->oom) must fail
371
+ * the node, never leave a silently-empty value in the tree. */
339
372
  int dom_place(yep_dom* d, uint32_t id) {
373
+ if (d->oom) {
374
+ return -1;
375
+ }
340
376
  if (d->depth == 0) {
341
377
  if (!dom_grow_docs(d, 1)) {
342
378
  return -1;
@@ -119,6 +119,11 @@ typedef struct yep_dom {
119
119
  * OFFSETS into it, so growth never dangles (dom_str_put) */
120
120
  char* str;
121
121
  uint32_t str_len, str_cap;
122
+ /* an arena write failed (str_put under OOM): the value would have
123
+ * silently become the empty view — doc corruption. Sticky; every
124
+ * builder path checks it at dom_place/dom_open_node and fails the
125
+ * parse with the memory status. */
126
+ int oom;
122
127
  const char* input_base; /* borrowed parse input (offset base) */
123
128
  /* handle arena: thread-safe (atomic bump) — query APIs allocate
124
129
  * YeptrisNode wrappers here, so read-only document sharing across
@@ -11,12 +11,20 @@
11
11
 
12
12
  #include "../../include/yeptris/events.h"
13
13
  #include "../common/simd_text.h"
14
+ #include "../encoding/encoding.h"
15
+ #include "../memory/allocator.h"
14
16
  #include "../parse/engine.h"
15
17
  #include "capture.h"
16
18
 
17
19
  typedef struct {
18
20
  yep_rec_store* store;
19
21
  int doc_ends; /* DOCUMENT_END events seen this run */
22
+ int oom; /* the capture failed: yep_rec_on_event returned
23
+ * nonzero — the engine's rc (-2) is shared with the
24
+ * normal document-abort, so without this bit an OOM
25
+ * was indistinguishable from doc-complete and the
26
+ * final event vanished silently (the audit's
27
+ * finding) */
20
28
  } yep_iter_sink;
21
29
 
22
30
  static int iter_on_event(void* ctx, const yep_event* ev) {
@@ -31,6 +39,7 @@ static int iter_on_event(void* ctx, const yep_event* ev) {
31
39
  }
32
40
  int rc = yep_rec_on_event(s->store, ev);
33
41
  if (rc != 0) {
42
+ s->oom = 1;
34
43
  return rc;
35
44
  }
36
45
  if (s->doc_ends >= 1) {
@@ -44,6 +53,11 @@ struct yeptris_iterparse {
44
53
  size_t len;
45
54
  size_t cursor;
46
55
  int done;
56
+ int gated; /* the encoding front-end ran once: after it, buf
57
+ * indexes the space every engine run's pos lives
58
+ * in (the resume arithmetic requires it) */
59
+ unsigned char* owned; /* UTF-16/32 transcode output (buf moved) */
60
+ char* norm; /* NEL/LS/PS-normalized copy (buf moved) */
47
61
  int emitted_stream_start;
48
62
  int pending_stream_end; /* deliver a final lone STREAM_END slice */
49
63
  YeptrisStatus status;
@@ -76,6 +90,55 @@ YEPTRIS_API const YeptrisEvent* yeptris_iterparse_next(YeptrisIterparse it, size
76
90
  return NULL;
77
91
  }
78
92
  for (;;) {
93
+ /* the encoding front-end, ONCE (the yep_engine_run law): the
94
+ * resume arithmetic below adds the engine's pos to it->cursor,
95
+ * and pos lives in the buffer space the RUN parsed — never the
96
+ * raw bytes. The front-end's consumed prefix broke that twice:
97
+ * the BOM it strips (pos cannot see it, so a BOM'd stream
98
+ * re-parsed the previous document's tail), and the UTF-16/32
99
+ * transcode (pos counts UTF-8 bytes, cursor counts raw ones —
100
+ * slices corrupted). The NEL/LS/PS break normalizer inside the
101
+ * run shifts pos the same way, so the gate owns both: encode
102
+ * to UTF-8 and normalize here, move buf, and the spaces agree. */
103
+ if (!it->gated) {
104
+ it->gated = 1;
105
+ const yep_allocator* sys = yep_system_allocator();
106
+ yep_encoding enc = YEP_ENC_UNKNOWN;
107
+ size_t bom = yep_bom_sniff((const unsigned char*)it->buf, it->len, &enc);
108
+ if (enc == YEP_ENC_UTF16LE || enc == YEP_ENC_UTF16BE || enc == YEP_ENC_UTF32LE ||
109
+ enc == YEP_ENC_UTF32BE) {
110
+ size_t tlen = 0;
111
+ size_t terr = 0;
112
+ int rc = yep_transcode_to_utf8(sys, enc, (const unsigned char*)it->buf + bom,
113
+ it->len - bom, &it->owned, &tlen, &terr);
114
+ if (rc != 0) {
115
+ it->done = 1;
116
+ it->status = YEPTRIS_ERROR_ENCODING;
117
+ it->err_line = 1;
118
+ it->err_col = 1;
119
+ return NULL;
120
+ }
121
+ /* the copy IS the post-gate buffer: it opens at the
122
+ * first content byte (the mark never entered it), so
123
+ * cursor restarts at zero — keeping `bom` here skipped
124
+ * the copy's first unit and corrupted the first slice */
125
+ it->buf = (const char*)it->owned;
126
+ it->len = tlen;
127
+ it->cursor = 0;
128
+ } else {
129
+ it->cursor = bom; /* the mark is the front-end's prefix */
130
+ }
131
+ size_t nlen = 0;
132
+ int kept_raw = 0;
133
+ char* norm = yep_engine_normalize_breaks(sys, it->buf + it->cursor,
134
+ it->len - it->cursor, &nlen, &kept_raw);
135
+ if (norm != NULL) {
136
+ it->norm = norm;
137
+ it->buf = norm;
138
+ it->len = nlen;
139
+ it->cursor = 0; /* the copy starts at its own zero */
140
+ }
141
+ }
79
142
  if (it->done && it->pending_stream_end && it->next >= it->store.n) {
80
143
  it->pending_stream_end = 0;
81
144
  if (!it->emitted_stream_start) {
@@ -133,7 +196,7 @@ YEPTRIS_API const YeptrisEvent* yeptris_iterparse_next(YeptrisIterparse it, size
133
196
  * document's start on the resumed run */
134
197
  yep_rec_reset(&it->store);
135
198
  it->next = 0;
136
- yep_iter_sink s = {&it->store, 0};
199
+ yep_iter_sink s = {&it->store, 0, 0};
137
200
  yep_engine* eng = yep_engine_create(yep_system_allocator());
138
201
  if (eng == NULL) {
139
202
  it->done = 1;
@@ -153,15 +216,39 @@ YEPTRIS_API const YeptrisEvent* yeptris_iterparse_next(YeptrisIterparse it, size
153
216
  yep_engine_prepare(eng, &pst);
154
217
  int rc = yep_engine_run(eng, it->buf + it->cursor, it->len - it->cursor, &sink);
155
218
  size_t pos = yep_engine_pos(eng);
156
- const yep_error* err = yep_engine_error(eng);
157
- if (rc == -1 && err != NULL) {
158
- it->err_line = err->line;
159
- it->err_col = err->col;
219
+ /* the error slot lives inside the engine and destroy munmaps it
220
+ * (glibc serves the ~260KB engine with mmap): copy the verdict
221
+ * out BEFORE the free */
222
+ yep_err_code err_code = YEP_ERR_NONE;
223
+ uint32_t err_line = 0, err_col = 0;
224
+ {
225
+ const yep_error* err = yep_engine_error(eng);
226
+ if (err != NULL) {
227
+ err_code = err->code;
228
+ err_line = err->line;
229
+ err_col = err->col;
230
+ }
160
231
  }
161
232
  yep_engine_destroy(eng);
162
- if (rc == -1) {
233
+ /* A failed run with a SET channel is a parse-class error and the
234
+ * CODE picks the public status; a failed run with a CLEAN
235
+ * channel is the capture sink dying — memory, never a parse
236
+ * error, and never a silent stream end (the old code fell into
237
+ * the doc-complete arm on rc == -2 and the final event
238
+ * vanished). The abort (rc == -2 without s.oom) stays the
239
+ * normal per-document completion. */
240
+ int failed = rc == -1 || s.oom;
241
+ if (failed) {
163
242
  it->done = 1;
164
- it->status = err != NULL ? YEPTRIS_ERROR_PARSE : YEPTRIS_ERROR_MEMORY;
243
+ if (err_code != YEP_ERR_NONE) {
244
+ it->err_line = err_line;
245
+ it->err_col = err_col;
246
+ it->status = err_code == YEP_ERR_MEMORY ? YEPTRIS_ERROR_MEMORY
247
+ : err_code == YEP_ERR_ENCODING ? YEPTRIS_ERROR_ENCODING
248
+ : YEPTRIS_ERROR_PARSE;
249
+ } else {
250
+ it->status = YEPTRIS_ERROR_MEMORY;
251
+ }
165
252
  if (it->store.n == 0) {
166
253
  if (!it->emitted_stream_start) {
167
254
  /* the engine emitted +STR before failing: match it */
@@ -202,6 +289,9 @@ YEPTRIS_API void yeptris_iterparse_free(YeptrisIterparse it) {
202
289
  if (it == NULL) {
203
290
  return;
204
291
  }
292
+ const yep_allocator* sys = yep_system_allocator();
293
+ yep_free(sys, it->owned);
294
+ yep_free(sys, it->norm);
205
295
  yep_rec_free(&it->store);
206
296
  free(it->mat);
207
297
  free(it);
@@ -44,13 +44,33 @@ YEPTRIS_API YeptrisPullParser yeptris_pull_new(const char* buf, size_t len) {
44
44
  yep_text_active()->scan_stats(buf, len, &pst);
45
45
  yep_engine_prepare(eng, &pst);
46
46
  int rc = yep_engine_run(eng, buf, len, &sink);
47
- const yep_error* err = yep_engine_error(eng);
48
- if (err != NULL && rc != 0) {
49
- p->err_line = err->line;
50
- p->err_col = err->col;
47
+ /* the error slot lives INSIDE the engine, and the engine is a
48
+ * ~260KB allocation glibc serves with mmap — destroy munmaps it,
49
+ * so the slot's verdict must be copied out BEFORE destroy (the
50
+ * round-5 CI segfaulted reading err->code after the free). */
51
+ yep_err_code err_code = YEP_ERR_NONE;
52
+ uint32_t err_line = 0, err_col = 0;
53
+ {
54
+ const yep_error* err = yep_engine_error(eng);
55
+ if (err != NULL) {
56
+ err_code = err->code;
57
+ err_line = err->line;
58
+ err_col = err->col;
59
+ }
51
60
  }
52
61
  yep_engine_destroy(eng);
53
- p->status = rc == 0 ? YEPTRIS_OK : YEPTRIS_ERROR_PARSE;
62
+ /* the error CODE decides the public status — the parse route's law
63
+ * (the recorder twin carries the full comment). A clean channel
64
+ * under a failed run is the capture sink failing: memory. */
65
+ if (rc != 0 && err_code != YEP_ERR_NONE) {
66
+ p->err_line = err_line;
67
+ p->err_col = err_col;
68
+ p->status = err_code == YEP_ERR_MEMORY ? YEPTRIS_ERROR_MEMORY
69
+ : err_code == YEP_ERR_ENCODING ? YEPTRIS_ERROR_ENCODING
70
+ : YEPTRIS_ERROR_PARSE;
71
+ } else {
72
+ p->status = rc == 0 ? YEPTRIS_OK : YEPTRIS_ERROR_MEMORY;
73
+ }
54
74
  return p;
55
75
  }
56
76
 
@@ -61,6 +61,12 @@ static int push_on_event(void* ctx, const yep_event* ev) {
61
61
  e.flow = ev->flow;
62
62
  e.line = ev->line;
63
63
  e.col = ev->col;
64
+ /* the cross-model identity law (#179, test_event_marks): push,
65
+ * pull and the recorder emit identical mark data — dropping the
66
+ * end marks here made the push route's events half-marked where
67
+ * pull/recorder carried all four fields */
68
+ e.end_line = ev->end_line;
69
+ e.end_col = ev->end_col;
64
70
  e.value = ev->value.len > 0 ? (const char*)ev->value.p : NULL;
65
71
  e.value_len = ev->value.len;
66
72
  e.anchor = ev->anchor.len > 0 ? (const char*)ev->anchor.p : NULL;
@@ -95,10 +101,26 @@ YEPTRIS_API YeptrisStatus yeptris_push_parse(const char* buf, size_t len, yeptri
95
101
  yep_text_active()->scan_stats(buf, len, &pst);
96
102
  yep_engine_prepare(eng, &pst);
97
103
  int rc = yep_engine_run(eng, buf, len, &sink);
104
+ /* the error slot lives inside the engine and destroy munmaps it
105
+ * (glibc serves the ~260KB engine with mmap): copy the verdict out
106
+ * BEFORE the free — reading err->code after destroy segfaulted the
107
+ * round-5 CI */
108
+ yep_err_code err_code = YEP_ERR_NONE;
109
+ {
110
+ const yep_error* err = yep_engine_error(eng);
111
+ if (err != NULL) {
112
+ err_code = err->code;
113
+ }
114
+ }
98
115
  yep_engine_destroy(eng);
99
116
  if (rc == 0) {
100
117
  return YEPTRIS_OK;
101
118
  }
102
- /* -2: the callback aborted — reported as a parse stop, not a bug */
119
+ /* the engine's own memory failures report MEMORY (the parse route's
120
+ * law); the callback abort keeps its documented contract — a parse
121
+ * stop, clean channel */
122
+ if (err_code == YEP_ERR_MEMORY) {
123
+ return YEPTRIS_ERROR_MEMORY;
124
+ }
103
125
  return YEPTRIS_ERROR_PARSE;
104
126
  }
@@ -37,6 +37,23 @@ YEPTRIS_API YeptrisRecorder yeptris_recorder_new_ex(YeptrisSchema schema) {
37
37
  return r;
38
38
  }
39
39
 
40
+ /* The engine's error code decides the public status — the parse
41
+ * route's law (parse.c maps YEP_ERR_* to the pinned codes). A clean
42
+ * channel means the SINK failed: the capture ran out of memory, the
43
+ * bytes never were a parse error. The MEMORY arm: the engine's own
44
+ * allocation failures (e_fail(YEP_ERR_MEMORY)) used to read as a parse
45
+ * error here — yep_engine_error always returns the slot, so the old
46
+ * `err != NULL` discriminator could never fire (the audit's finding). */
47
+ static YeptrisStatus rec_status_from_err(const yep_error* err) {
48
+ if (err == NULL || err->code == YEP_ERR_NONE || err->code == YEP_ERR_MEMORY) {
49
+ return YEPTRIS_ERROR_MEMORY;
50
+ }
51
+ if (err->code == YEP_ERR_ENCODING) {
52
+ return YEPTRIS_ERROR_ENCODING;
53
+ }
54
+ return YEPTRIS_ERROR_PARSE;
55
+ }
56
+
40
57
  YEPTRIS_API YeptrisStatus yeptris_recorder_feed(YeptrisRecorder rec, const char* chunk, size_t len,
41
58
  int final) {
42
59
  if (rec == NULL || (chunk == NULL && len != 0)) {
@@ -71,15 +88,11 @@ YEPTRIS_API YeptrisStatus yeptris_recorder_feed(YeptrisRecorder rec, const char*
71
88
  int rc = yep_engine_step(rec->eng, chunk, len, final, &sink);
72
89
  if (rc != 0) {
73
90
  const yep_error* err = yep_engine_error(rec->eng);
74
- if (err != NULL) {
91
+ if (err != NULL && err->code != YEP_ERR_NONE) {
75
92
  rec->err_line = err->line;
76
93
  rec->err_col = err->col;
77
- rec->status = YEPTRIS_ERROR_PARSE;
78
- } else {
79
- /* a clean error channel means the SINK failed: the capture
80
- * ran out of memory, the bytes never were a parse error */
81
- rec->status = YEPTRIS_ERROR_MEMORY;
82
94
  }
95
+ rec->status = rec_status_from_err(err);
83
96
  rec->final_fed = 1; /* a failed feed kills the stream */
84
97
  return rec->status;
85
98
  }
@@ -107,6 +120,20 @@ YEPTRIS_API const char* yeptris_recorder_arena(YeptrisRecorder rec, size_t* len)
107
120
  return rec->store.arena;
108
121
  }
109
122
 
123
+ YEPTRIS_API YeptrisStatus yeptris_recorder_status(const YeptrisRecorder rec, uint32_t* line,
124
+ uint32_t* col) {
125
+ if (rec == NULL) {
126
+ return YEPTRIS_ERROR_ARG;
127
+ }
128
+ if (line != NULL) {
129
+ *line = rec->err_line;
130
+ }
131
+ if (col != NULL) {
132
+ *col = rec->err_col;
133
+ }
134
+ return rec->status;
135
+ }
136
+
110
137
  YEPTRIS_API void yeptris_recorder_free(YeptrisRecorder rec) {
111
138
  if (rec == NULL) {
112
139
  return;
@@ -98,7 +98,15 @@ void* yep_arena_alloc(yep_arena* arena, size_t size, size_t align) {
98
98
  uintptr_t base = (uintptr_t)b + sizeof(yep_arena_block);
99
99
  uintptr_t start = yep_round_up(base + b->used, align);
100
100
 
101
- if (start + size > base + b->size) {
101
+ /* the fit test in subtraction form (the audit's finding): the old
102
+ * `start + size > base + b->size` wraps when size rides past the
103
+ * pointer space and the request "fits". The limit-first clause
104
+ * covers the round_up overshoot — start can sit up to align-1
105
+ * bytes past the limit, where `limit - start` would underflow — so
106
+ * both comparisons are wrap-free without leaning on the
107
+ * hostile-size guard above. */
108
+ uintptr_t limit = base + b->size;
109
+ if (start >= limit || size > limit - start) {
102
110
  /* Amortized doubling: each new block is at least twice the last,
103
111
  * unless the request itself is larger. */
104
112
  size_t payload = arena->last_block_size * 2;
@@ -64,7 +64,11 @@ void* yep_pool_alloc(yep_pool* pool, size_t size, size_t align) {
64
64
  uintptr_t base = (uintptr_t)b + sizeof(yep_block);
65
65
  uintptr_t start = yep_round_up(base + b->used, align);
66
66
 
67
- if (start + size > base + b->size) {
67
+ /* the fit test in subtraction form — the arena twin carries the
68
+ * full comment (wrap-free comparisons, round_up overshoot covered
69
+ * by the limit-first clause) */
70
+ uintptr_t limit = base + b->size;
71
+ if (start >= limit || size > limit - start) {
68
72
  /* Dedicated-or-default new block, sized so the aligned request
69
73
  * always fits (malloc returns at least 16-byte-aligned memory, so
70
74
  * the correction is < align). */