makiri 0.3.0 → 0.4.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.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/conformance.yml +22 -0
  3. data/.github/workflows/libfuzzer.yml +83 -0
  4. data/.github/workflows/security.yml +88 -3
  5. data/.github/workflows/valgrind.yml +135 -0
  6. data/CHANGELOG.md +60 -2
  7. data/README.md +81 -77
  8. data/Rakefile +194 -3
  9. data/ext/makiri/bridge/ruby_string.c +119 -66
  10. data/ext/makiri/core/mkr_alloc.c +40 -3
  11. data/ext/makiri/core/mkr_alloc.h +27 -4
  12. data/ext/makiri/core/mkr_buf.c +13 -3
  13. data/ext/makiri/core/mkr_buf.h +80 -5
  14. data/ext/makiri/core/mkr_core.c +143 -0
  15. data/ext/makiri/core/mkr_core.h +10 -1
  16. data/ext/makiri/core/mkr_span.h +186 -0
  17. data/ext/makiri/core/mkr_utf8.c +101 -0
  18. data/ext/makiri/core/mkr_utf8.h +88 -0
  19. data/ext/makiri/extconf.rb +104 -9
  20. data/ext/makiri/fuzz/Makefile +95 -0
  21. data/ext/makiri/fuzz/check_fuzzer.cc +4 -0
  22. data/ext/makiri/fuzz/xml_fuzz.c +24 -0
  23. data/ext/makiri/fuzz/xpath_fuzz.c +109 -0
  24. data/ext/makiri/glue/glue.h +8 -0
  25. data/ext/makiri/glue/ruby_doc.c +20 -24
  26. data/ext/makiri/glue/ruby_html_css.c +58 -12
  27. data/ext/makiri/glue/ruby_html_mutate.c +11 -6
  28. data/ext/makiri/glue/ruby_html_node.c +3 -32
  29. data/ext/makiri/glue/ruby_node.c +39 -0
  30. data/ext/makiri/glue/ruby_xml.c +198 -16
  31. data/ext/makiri/glue/ruby_xml_node.c +46 -59
  32. data/ext/makiri/glue/ruby_xpath.c +4 -4
  33. data/ext/makiri/lexbor_compat/source_loc.c +14 -16
  34. data/ext/makiri/lexbor_compat/utf8_input.c +5 -78
  35. data/ext/makiri/makiri.c +45 -0
  36. data/ext/makiri/xml/mkr_xml.h +2 -3
  37. data/ext/makiri/xml/mkr_xml_chars.c +67 -97
  38. data/ext/makiri/xml/mkr_xml_index.c +169 -0
  39. data/ext/makiri/xml/mkr_xml_index.h +48 -0
  40. data/ext/makiri/xml/mkr_xml_mutate.c +63 -121
  41. data/ext/makiri/xml/mkr_xml_node.c +147 -15
  42. data/ext/makiri/xml/mkr_xml_node.h +71 -6
  43. data/ext/makiri/xml/mkr_xml_tree.c +185 -149
  44. data/ext/makiri/xpath/mkr_css.c +1023 -0
  45. data/ext/makiri/xpath/mkr_css.h +65 -0
  46. data/ext/makiri/xpath/mkr_xpath.c +37 -0
  47. data/ext/makiri/xpath/mkr_xpath.h +13 -0
  48. data/ext/makiri/xpath/mkr_xpath_eval_body.h +373 -90
  49. data/ext/makiri/xpath/mkr_xpath_funcs_body.h +249 -231
  50. data/ext/makiri/xpath/mkr_xpath_internal.h +89 -9
  51. data/ext/makiri/xpath/mkr_xpath_lex.c +94 -124
  52. data/ext/makiri/xpath/mkr_xpath_node_access_xml.h +6 -3
  53. data/ext/makiri/xpath/mkr_xpath_number.c +109 -0
  54. data/ext/makiri/xpath/mkr_xpath_parse.c +79 -90
  55. data/ext/makiri/xpath/mkr_xpath_shared.c +40 -24
  56. data/ext/makiri/xpath/mkr_xpath_value_body.h +50 -24
  57. data/lib/makiri/cdata_section.rb +1 -3
  58. data/lib/makiri/comment.rb +1 -3
  59. data/lib/makiri/document.rb +8 -0
  60. data/lib/makiri/element.rb +1 -3
  61. data/lib/makiri/processing_instruction.rb +1 -3
  62. data/lib/makiri/text.rb +1 -3
  63. data/lib/makiri/version.rb +1 -1
  64. data/lib/makiri/xml/builder.rb +263 -0
  65. data/lib/makiri/xml/node_methods.rb +47 -0
  66. data/lib/makiri.rb +1 -0
  67. data/script/check_alloc_failures.rb +266 -0
  68. data/script/check_c_safety.rb +45 -2
  69. data/script/check_c_safety_allowlist.yml +19 -0
  70. data/script/check_leaks.rb +64 -0
  71. data/script/leaks_harness.rb +64 -0
  72. data/vendor/lexbor/CMakeLists.txt +6 -0
  73. data/vendor/lexbor/README.md +12 -0
  74. data/vendor/lexbor/config.cmake +1 -1
  75. data/vendor/lexbor/source/lexbor/core/base.h +1 -1
  76. data/vendor/lexbor/source/lexbor/core/config.cmake +9 -1
  77. data/vendor/lexbor/source/lexbor/css/selectors/pseudo_state.c +2 -3
  78. data/vendor/lexbor/source/lexbor/css/selectors/state.c +3 -0
  79. data/vendor/lexbor/source/lexbor/dom/interfaces/element.c +21 -0
  80. data/vendor/lexbor/source/lexbor/dom/interfaces/element.h +5 -0
  81. data/vendor/lexbor/source/lexbor/encoding/decode.c +33 -4
  82. data/vendor/lexbor/source/lexbor/html/base.h +1 -1
  83. data/vendor/lexbor/source/lexbor/html/interfaces/select_element.c +4 -0
  84. data/vendor/lexbor/source/lexbor/html/serialize.c +545 -41
  85. data/vendor/lexbor/source/lexbor/html/serialize.h +2 -1
  86. data/vendor/lexbor/source/lexbor/html/tokenizer.h +2 -2
  87. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/in_body.c +1 -1
  88. data/vendor/lexbor/source/lexbor/html/tree.c +6 -6
  89. data/vendor/lexbor/source/lexbor/selectors/selectors.c +12 -3
  90. data/vendor/lexbor/source/lexbor/url/base.h +1 -1
  91. data/vendor/lexbor/source/lexbor/url/url.c +5 -2
  92. data/vendor/lexbor/source/lexbor/url/url.h +9 -0
  93. data/vendor/lexbor/version +1 -1
  94. metadata +19 -1
@@ -21,44 +21,6 @@
21
21
  #define XMLNS_NS_URI "http://www.w3.org/2000/xmlns/"
22
22
  #define LIT_LEN(s) ((uint32_t)(sizeof(s) - 1))
23
23
 
24
- static int
25
- slice_eq(const char *a, uint32_t al, const char *b, uint32_t bl)
26
- {
27
- return al == bl && (al == 0 || memcmp(a, b, al) == 0);
28
- }
29
-
30
- /* Validate that [s, s+len) is a well-formed XML 1.0 QName and split it into
31
- * prefix:local (pfx_len 0 = no prefix). Mirrors scan_name + split_qname in the
32
- * tree builder: the whole name is NameStartChar NameChar* (a colon is a NameChar),
33
- * then at most one colon separates two NCNames. 0 on success, -1 if malformed. */
34
- static int
35
- qname_split(const char *s, uint32_t len, const char **pfx, uint32_t *pl,
36
- const char **loc, uint32_t *ll)
37
- {
38
- if (len == 0) return -1;
39
- const char *p = s, *end = s + len;
40
- uint32_t cp;
41
- int bl = mkr_xml_utf8_decode(p, end, &cp);
42
- if (bl == 0 || !mkr_xml_is_name_start(cp)) return -1;
43
- p += bl;
44
- while (p < end) {
45
- bl = mkr_xml_utf8_decode(p, end, &cp);
46
- if (bl == 0 || !mkr_xml_is_name_char(cp)) return -1;
47
- p += bl;
48
- }
49
- const char *colon = memchr(s, ':', len);
50
- if (colon == NULL) { *pfx = s; *pl = 0; *loc = s; *ll = len; return 0; }
51
- uint32_t prefix_len = (uint32_t)(colon - s);
52
- const char *ls = colon + 1;
53
- uint32_t local_len = len - prefix_len - 1;
54
- if (prefix_len == 0 || local_len == 0) return -1; /* ":x" or "x:" */
55
- if (memchr(ls, ':', local_len) != NULL) return -1; /* a second colon */
56
- if (mkr_xml_utf8_decode(ls, ls + local_len, &cp) == 0 || !mkr_xml_is_name_start(cp))
57
- return -1; /* local must be an NCName */
58
- *pfx = s; *pl = prefix_len; *loc = ls; *ll = local_len;
59
- return 0;
60
- }
61
-
62
24
  /* Nearest in-scope binding for +prefix+ (pl 0 = the default namespace) at or
63
25
  * above +node+, walking the real tree (no scope dictionary is threaded). */
64
26
  static int
@@ -70,7 +32,7 @@ resolve_in_scope(const mkr_xml_node_t *node, const char *prefix, uint32_t pl,
70
32
  for (mkr_xml_node_t *a = e->attrs; a != NULL; a = a->next) {
71
33
  const char *p, *u; uint32_t apl, ul;
72
34
  if (mkr_xml_node_xmlns_decl(a, &p, &apl, &u, &ul)
73
- && apl == pl && (pl == 0 || memcmp(p, prefix, pl) == 0)) {
35
+ && mkr_bytes_eq(p, apl, prefix, pl)) {
74
36
  *uri = u; *ulen = ul; return 1;
75
37
  }
76
38
  }
@@ -91,58 +53,45 @@ is_connected(const mkr_xml_node_t *node)
91
53
  return top->type == MKR_XML_NODE_TYPE_DOCUMENT;
92
54
  }
93
55
 
94
- /* Resolve the namespace for the QName [qname,qlen] (already split into pfx/pl)
95
- * applied at +scope+ (the element whose in-scope declarations apply). +is_attr+
96
- * selects attribute rules (xmlns* declarations live in the xmlns namespace, an
97
- * unprefixed attribute is in no namespace). +connected+ decides what an unbound
98
- * prefix means: an error in the live tree, deferred (unresolved) when detached.
99
- * Mirrors mkr_xml_tree.c §7. */
56
+ /* Resolve the namespace for QName +qn+ applied at +scope+ (the element whose
57
+ * in-scope declarations apply). +is_attr+ selects attribute rules (xmlns*
58
+ * declarations live in the xmlns namespace, an unprefixed attribute is in no
59
+ * namespace). +connected+ decides what an unbound prefix means: an error in the
60
+ * live tree, deferred (unresolved) when detached. Mirrors mkr_xml_tree.c §7. */
100
61
  static mkr_xml_mut_status_t
101
- resolve_ns(const mkr_xml_node_t *scope, const char *qname, uint32_t qlen,
102
- const char *pfx, uint32_t pl, int is_attr, int connected,
103
- const char **uri, uint32_t *ulen)
62
+ resolve_ns(const mkr_xml_node_t *scope, const mkr_xml_qname_t *qn, int is_attr,
63
+ int connected, const char **uri, uint32_t *ulen)
104
64
  {
105
65
  *uri = NULL; *ulen = 0;
106
- if (is_attr) {
107
- int is_default_decl = (qlen == 5 && memcmp(qname, "xmlns", 5) == 0);
108
- int is_pfx_decl = (qlen > 6 && memcmp(qname, "xmlns:", 6) == 0);
109
- if (is_default_decl || is_pfx_decl) {
110
- *uri = XMLNS_NS_URI; *ulen = LIT_LEN(XMLNS_NS_URI);
111
- return MKR_XML_MUT_OK;
112
- }
66
+ if (is_attr && mkr_xml_xmlns_prefix(qn->qname, qn->qname_len, NULL, NULL)) {
67
+ *uri = XMLNS_NS_URI; *ulen = LIT_LEN(XMLNS_NS_URI); /* an xmlns declaration */
68
+ return MKR_XML_MUT_OK;
113
69
  }
114
- if (pl == 0) {
70
+ if (qn->prefix_len == 0) {
115
71
  if (is_attr) return MKR_XML_MUT_OK; /* unprefixed attribute -> no namespace */
116
72
  const char *u; uint32_t ul; /* unprefixed element -> the default ns */
117
73
  if (resolve_in_scope(scope, "", 0, &u, &ul) && ul > 0) { *uri = u; *ulen = ul; }
118
74
  return MKR_XML_MUT_OK;
119
75
  }
120
- if (slice_eq(pfx, pl, "xml", 3)) { /* the predefined xml: prefix */
76
+ if (mkr_bytes_eq(qn->prefix, qn->prefix_len, "xml", 3)) { /* the predefined xml: prefix */
121
77
  *uri = XML_NS_URI; *ulen = LIT_LEN(XML_NS_URI);
122
78
  return MKR_XML_MUT_OK;
123
79
  }
124
- if (slice_eq(pfx, pl, "xmlns", 5)) return MKR_XML_MUT_BAD_NAME; /* xmlns: as a normal name */
80
+ if (mkr_bytes_eq(qn->prefix, qn->prefix_len, "xmlns", 5)) return MKR_XML_MUT_BAD_NAME; /* xmlns: as a normal name */
125
81
  const char *u; uint32_t ul;
126
- if (!resolve_in_scope(scope, pfx, pl, &u, &ul) || ul == 0) {
82
+ if (!resolve_in_scope(scope, qn->prefix, qn->prefix_len, &u, &ul) || ul == 0) {
127
83
  return connected ? MKR_XML_MUT_UNBOUND_NS : MKR_XML_MUT_OK; /* defer when detached */
128
84
  }
129
85
  *uri = u; *ulen = ul;
130
86
  return MKR_XML_MUT_OK;
131
87
  }
132
88
 
133
- /* Copy the QName into the arena as ONE contiguous slice and point qname/local/
134
- * prefix into it (the read API and serializers borrow these). +loc+ points into
135
- * +name+ (from qname_split). */
89
+ /* Copy +qn+ into the arena and point the node's name fields at it (shared
90
+ * mkr_xml_qname_assign), mapping its OOM to the mutation status. */
136
91
  static mkr_xml_mut_status_t
137
- assign_qname(mkr_xml_doc_t *doc, mkr_xml_node_t *node, const char *name, uint32_t nl,
138
- const char *loc, uint32_t ll, uint32_t pl)
92
+ assign_qname(mkr_xml_doc_t *doc, mkr_xml_node_t *node, const mkr_xml_qname_t *qn)
139
93
  {
140
- const char *q = mkr_xml_arena_bytes(doc, name, nl);
141
- if (nl > 0 && q == NULL) return MKR_XML_MUT_OOM;
142
- node->qname = q; node->qname_len = nl;
143
- node->local = q + (size_t)(loc - name); node->local_len = ll;
144
- node->prefix = q; node->prefix_len = pl;
145
- return MKR_XML_MUT_OK;
94
+ return mkr_xml_qname_assign(doc, node, qn) == 0 ? MKR_XML_MUT_OK : MKR_XML_MUT_OOM;
146
95
  }
147
96
 
148
97
  void
@@ -171,23 +120,20 @@ mkr_xml_rename(mkr_xml_doc_t *doc, mkr_xml_node_t *node, const char *name, uint3
171
120
  {
172
121
  if (node->type != MKR_XML_NODE_TYPE_ELEMENT && node->type != MKR_XML_NODE_TYPE_ATTRIBUTE)
173
122
  return MKR_XML_MUT_TYPE;
174
- const char *pfx, *loc; uint32_t pl, ll;
175
- if (qname_split(name, nlen, &pfx, &pl, &loc, &ll) != 0) return MKR_XML_MUT_BAD_NAME;
123
+ mkr_xml_qname_t qn;
124
+ if (mkr_xml_qname_split(name, nlen, &qn) != 0) return MKR_XML_MUT_BAD_NAME;
176
125
 
177
126
  int is_attr = (node->type == MKR_XML_NODE_TYPE_ATTRIBUTE);
178
127
  const mkr_xml_node_t *scope = is_attr ? node->parent : node;
179
128
  const char *uri; uint32_t ulen;
180
129
  int connected = scope != NULL && is_connected(scope);
181
- mkr_xml_mut_status_t st = resolve_ns(scope, name, nlen, pfx, pl, is_attr, connected, &uri, &ulen);
130
+ mkr_xml_mut_status_t st = resolve_ns(scope, &qn, is_attr, connected, &uri, &ulen);
182
131
  if (st != MKR_XML_MUT_OK) return st;
183
132
 
184
- /* Copy the new qname BEFORE writing any field, so an OOM leaves node intact. */
185
- const char *q = mkr_xml_arena_bytes(doc, name, nlen);
186
- if (nlen > 0 && q == NULL) return MKR_XML_MUT_OOM;
187
- node->qname = q; node->qname_len = nlen;
188
- node->local = q + (size_t)(loc - name); node->local_len = ll;
189
- node->prefix = q; node->prefix_len = pl;
190
- node->ns_uri = uri; node->ns_uri_len = ulen;
133
+ /* Copy the new qname BEFORE writing ns_uri, so an OOM leaves node intact. */
134
+ st = assign_qname(doc, node, &qn);
135
+ if (st != MKR_XML_MUT_OK) return st;
136
+ node->ns_uri = uri; node->ns_uri_len = ulen;
191
137
  return MKR_XML_MUT_OK;
192
138
  }
193
139
 
@@ -197,21 +143,22 @@ mkr_xml_set_attribute(mkr_xml_doc_t *doc, mkr_xml_node_t *el, const char *name,
197
143
  {
198
144
  if (out) *out = NULL;
199
145
  if (el->type != MKR_XML_NODE_TYPE_ELEMENT) return MKR_XML_MUT_TYPE;
200
- const char *pfx, *loc; uint32_t pl, ll;
201
- if (qname_split(name, nlen, &pfx, &pl, &loc, &ll) != 0) return MKR_XML_MUT_BAD_NAME;
146
+ mkr_xml_qname_t qn;
147
+ if (mkr_xml_qname_split(name, nlen, &qn) != 0) return MKR_XML_MUT_BAD_NAME;
202
148
  /* A prefixed namespace declaration (xmlns:foo="") must not bind a prefix to
203
149
  * the empty namespace: XML Namespaces 1.0 forbids it and it serializes to
204
150
  * invalid XML. (Undeclaring the DEFAULT namespace, xmlns="", is allowed.) */
205
- if (vlen == 0 && pl == 5 && slice_eq(pfx, pl, "xmlns", 5)) return MKR_XML_MUT_BAD_NS_DECL;
151
+ if (vlen == 0 && qn.prefix_len == 5 && mkr_bytes_eq(qn.prefix, qn.prefix_len, "xmlns", 5))
152
+ return MKR_XML_MUT_BAD_NS_DECL;
206
153
  if (vlen > 0 && mkr_xml_validate_chars(val, vlen) != 0) return MKR_XML_MUT_BAD_CHARS;
207
154
 
208
155
  const char *uri; uint32_t ulen;
209
- mkr_xml_mut_status_t st = resolve_ns(el, name, nlen, pfx, pl, 1, is_connected(el), &uri, &ulen);
156
+ mkr_xml_mut_status_t st = resolve_ns(el, &qn, 1, is_connected(el), &uri, &ulen);
210
157
  if (st != MKR_XML_MUT_OK) return st;
211
158
 
212
159
  /* An existing attribute with the same raw QName -> replace its value. */
213
160
  for (mkr_xml_node_t *a = el->attrs; a != NULL; a = a->next) {
214
- if (slice_eq(a->qname, a->qname_len, name, nlen)) {
161
+ if (mkr_bytes_eq(a->qname, a->qname_len, name, nlen)) {
215
162
  const char *nv = mkr_xml_arena_bytes(doc, val, vlen);
216
163
  if (vlen > 0 && nv == NULL) return MKR_XML_MUT_OOM;
217
164
  a->value = nv ? nv : ""; a->value_len = vlen;
@@ -224,7 +171,7 @@ mkr_xml_set_attribute(mkr_xml_doc_t *doc, mkr_xml_node_t *el, const char *name,
224
171
  /* New attribute - allocate node + qname + value before linking. */
225
172
  mkr_xml_node_t *attr = mkr_xml_arena_node(doc, MKR_XML_NODE_TYPE_ATTRIBUTE);
226
173
  if (attr == NULL) return MKR_XML_MUT_OOM;
227
- st = assign_qname(doc, attr, name, nlen, loc, ll, pl);
174
+ st = assign_qname(doc, attr, &qn);
228
175
  if (st != MKR_XML_MUT_OK) return st; /* abandoned in the arena; freed with doc */
229
176
  const char *nv = mkr_xml_arena_bytes(doc, val, vlen);
230
177
  if (vlen > 0 && nv == NULL) return MKR_XML_MUT_OOM;
@@ -249,7 +196,7 @@ mkr_xml_remove_attribute(mkr_xml_node_t *el, const char *name, uint32_t nlen)
249
196
  if (el->type != MKR_XML_NODE_TYPE_ELEMENT) return 0;
250
197
  mkr_xml_node_t *prev = NULL;
251
198
  for (mkr_xml_node_t *a = el->attrs; a != NULL; prev = a, a = a->next) {
252
- if (slice_eq(a->qname, a->qname_len, name, nlen)) {
199
+ if (mkr_bytes_eq(a->qname, a->qname_len, name, nlen)) {
253
200
  if (prev) prev->next = a->next; else el->attrs = a->next;
254
201
  a->next = NULL; a->parent = NULL;
255
202
  return 1;
@@ -344,12 +291,12 @@ mkr_xml_mut_status_t
344
291
  mkr_xml_new_element(mkr_xml_doc_t *doc, const char *name, uint32_t nlen, mkr_xml_node_t **out)
345
292
  {
346
293
  *out = NULL;
347
- const char *pfx, *loc; uint32_t pl, ll;
348
- if (qname_split(name, nlen, &pfx, &pl, &loc, &ll) != 0) return MKR_XML_MUT_BAD_NAME;
349
- if (slice_eq(pfx, pl, "xmlns", 5)) return MKR_XML_MUT_BAD_NAME; /* xmlns: is not an element prefix */
294
+ mkr_xml_qname_t qn;
295
+ if (mkr_xml_qname_split(name, nlen, &qn) != 0) return MKR_XML_MUT_BAD_NAME;
296
+ if (mkr_bytes_eq(qn.prefix, qn.prefix_len, "xmlns", 5)) return MKR_XML_MUT_BAD_NAME; /* xmlns: is not an element prefix */
350
297
  mkr_xml_node_t *el = mkr_xml_arena_node(doc, MKR_XML_NODE_TYPE_ELEMENT);
351
298
  if (el == NULL) return MKR_XML_MUT_OOM;
352
- mkr_xml_mut_status_t st = assign_qname(doc, el, name, nlen, loc, ll, pl);
299
+ mkr_xml_mut_status_t st = assign_qname(doc, el, &qn);
353
300
  if (st != MKR_XML_MUT_OK) return st;
354
301
  *out = el; /* ns_uri stays unresolved until insertion */
355
302
  return MKR_XML_MUT_OK;
@@ -382,8 +329,8 @@ mkr_xml_new_pi(mkr_xml_doc_t *doc, const char *target, uint32_t tlen,
382
329
  {
383
330
  *out = NULL;
384
331
  /* PITarget is an NCName (a QName with no colon) and not "xml" in any case. */
385
- const char *pfx, *loc; uint32_t pl, ll;
386
- if (qname_split(target, tlen, &pfx, &pl, &loc, &ll) != 0 || pl != 0) return MKR_XML_MUT_BAD_NAME;
332
+ mkr_xml_qname_t qn;
333
+ if (mkr_xml_qname_split(target, tlen, &qn) != 0 || qn.prefix_len != 0) return MKR_XML_MUT_BAD_NAME;
387
334
  if (is_reserved_pi_target(target, tlen)) return MKR_XML_MUT_BAD_NAME;
388
335
  if (dlen > 0 && mkr_xml_validate_chars(data, dlen) != 0) return MKR_XML_MUT_BAD_CHARS;
389
336
  mkr_xml_mut_status_t seq = mkr_xml_check_value_seq(MKR_XML_NODE_TYPE_PI, data, dlen);
@@ -410,12 +357,13 @@ static mkr_xml_mut_status_t
410
357
  resolve_node_ns(mkr_xml_node_t *e, int connected)
411
358
  {
412
359
  const char *uri; uint32_t ulen;
413
- mkr_xml_mut_status_t st =
414
- resolve_ns(e, e->qname, e->qname_len, e->prefix, e->prefix_len, 0, connected, &uri, &ulen);
360
+ mkr_xml_qname_t eq = mkr_xml_qname_of(e);
361
+ mkr_xml_mut_status_t st = resolve_ns(e, &eq, 0, connected, &uri, &ulen);
415
362
  if (st != MKR_XML_MUT_OK) return st;
416
363
  e->ns_uri = uri; e->ns_uri_len = ulen;
417
364
  for (mkr_xml_node_t *a = e->attrs; a != NULL; a = a->next) {
418
- st = resolve_ns(e, a->qname, a->qname_len, a->prefix, a->prefix_len, 1, connected, &uri, &ulen);
365
+ mkr_xml_qname_t aq = mkr_xml_qname_of(a);
366
+ st = resolve_ns(e, &aq, 1, connected, &uri, &ulen);
419
367
  if (st != MKR_XML_MUT_OK) return st;
420
368
  a->ns_uri = uri; a->ns_uri_len = ulen;
421
369
  }
@@ -425,15 +373,11 @@ resolve_node_ns(mkr_xml_node_t *e, int connected)
425
373
  static mkr_xml_mut_status_t
426
374
  resolve_subtree(mkr_xml_node_t *root, int connected)
427
375
  {
428
- for (mkr_xml_node_t *cur = root; cur != NULL;) {
376
+ for (mkr_xml_node_t *cur = root; cur != NULL; cur = mkr_xml_preorder_next(root, cur)) {
429
377
  if (cur->type == MKR_XML_NODE_TYPE_ELEMENT) {
430
378
  mkr_xml_mut_status_t st = resolve_node_ns(cur, connected);
431
379
  if (st != MKR_XML_MUT_OK) return st;
432
380
  }
433
- if (cur->first_child != NULL) { cur = cur->first_child; continue; }
434
- while (cur != root && cur->next == NULL) cur = cur->parent;
435
- if (cur == root) break;
436
- cur = cur->next;
437
381
  }
438
382
  return MKR_XML_MUT_OK;
439
383
  }
@@ -464,11 +408,8 @@ copy_one(mkr_xml_doc_t *doc, const mkr_xml_node_t *src, bool with_ns)
464
408
  mkr_xml_node_t *n = mkr_xml_arena_node(doc, src->type);
465
409
  if (n == NULL) return NULL;
466
410
  if (src->qname != NULL && src->qname_len > 0) { /* element / attribute QName */
467
- if (assign_qname(doc, n, src->qname, src->qname_len,
468
- src->qname + (size_t)(src->local - src->qname),
469
- src->local_len, src->prefix_len) != MKR_XML_MUT_OK) {
470
- return NULL;
471
- }
411
+ mkr_xml_qname_t qn = mkr_xml_qname_of(src);
412
+ if (assign_qname(doc, n, &qn) != MKR_XML_MUT_OK) return NULL;
472
413
  } else if (src->local != NULL && src->local_len > 0) { /* PI target */
473
414
  const char *t = mkr_xml_arena_bytes(doc, src->local, src->local_len);
474
415
  if (t == NULL) return NULL;
@@ -713,19 +654,20 @@ mkr_xml_mutate_selftest(void)
713
654
  mkr_xml_doc_t *doc = mkr_xml_doc_new();
714
655
  if (doc == NULL) return 1;
715
656
  int rc = 0;
716
- const char *pfx, *loc; uint32_t pl, ll;
657
+ mkr_xml_qname_t qn;
717
658
 
718
- /* 1. QName validation + split */
719
- if (qname_split("a:b", 3, &pfx, &pl, &loc, &ll) != 0 || pl != 1 || ll != 1) { rc = 2; goto done; }
720
- if (qname_split("1bad", 4, &pfx, &pl, &loc, &ll) == 0) { rc = 3; goto done; } /* bad NameStartChar */
721
- if (qname_split("a:b:c", 5, &pfx, &pl, &loc, &ll) == 0) { rc = 4; goto done; } /* two colons */
722
- if (qname_split(":x", 2, &pfx, &pl, &loc, &ll) == 0) { rc = 5; goto done; } /* empty prefix */
723
- if (qname_split("x:", 2, &pfx, &pl, &loc, &ll) == 0) { rc = 6; goto done; } /* empty local */
659
+ /* 1. QName validation + split (the shared mkr_xml_qname_split) */
660
+ if (mkr_xml_qname_split("a:b", 3, &qn) != 0 || qn.prefix_len != 1 || qn.local_len != 1) { rc = 2; goto done; }
661
+ if (mkr_xml_qname_split("1bad", 4, &qn) == 0) { rc = 3; goto done; } /* bad NameStartChar */
662
+ if (mkr_xml_qname_split("a:b:c", 5, &qn) == 0) { rc = 4; goto done; } /* two colons */
663
+ if (mkr_xml_qname_split(":x", 2, &qn) == 0) { rc = 5; goto done; } /* empty prefix */
664
+ if (mkr_xml_qname_split("x:", 2, &qn) == 0) { rc = 6; goto done; } /* empty local */
724
665
 
725
666
  /* 2. build a root element, exercise attribute set/replace */
726
667
  mkr_xml_node_t *r = mkr_xml_arena_node(doc, MKR_XML_NODE_TYPE_ELEMENT);
727
668
  if (r == NULL) { rc = 7; goto done; }
728
- if (assign_qname(doc, r, "r", 1, "r", 1, 0) != MKR_XML_MUT_OK) { rc = 8; goto done; }
669
+ mkr_xml_qname_t rq = { "r", 1, "r", 0, "r", 1 };
670
+ if (assign_qname(doc, r, &rq) != MKR_XML_MUT_OK) { rc = 8; goto done; }
729
671
 
730
672
  mkr_xml_node_t *at = NULL;
731
673
  if (mkr_xml_set_attribute(doc, r, "id", 2, "x", 1, &at) != MKR_XML_MUT_OK
@@ -757,14 +699,14 @@ mkr_xml_mutate_selftest(void)
757
699
 
758
700
  /* 4. the predefined xml: prefix resolves with no declaration */
759
701
  if (mkr_xml_set_attribute(doc, r, "xml:lang", 8, "en", 2, &at) != MKR_XML_MUT_OK
760
- || !slice_eq(at->ns_uri, at->ns_uri_len, XML_NS_URI, LIT_LEN(XML_NS_URI))) { rc = 13; goto done; }
702
+ || !mkr_bytes_eq(at->ns_uri, at->ns_uri_len, XML_NS_URI, LIT_LEN(XML_NS_URI))) { rc = 13; goto done; }
761
703
 
762
704
  /* 5. adding an xmlns:* declaration lands in the xmlns namespace and then binds
763
705
  * a previously-unbound prefix */
764
706
  if (mkr_xml_set_attribute(doc, r, "xmlns:p", 7, "urn:p", 5, &at) != MKR_XML_MUT_OK
765
- || !slice_eq(at->ns_uri, at->ns_uri_len, XMLNS_NS_URI, LIT_LEN(XMLNS_NS_URI))) { rc = 14; goto done; }
707
+ || !mkr_bytes_eq(at->ns_uri, at->ns_uri_len, XMLNS_NS_URI, LIT_LEN(XMLNS_NS_URI))) { rc = 14; goto done; }
766
708
  if (mkr_xml_set_attribute(doc, r, "p:k", 3, "v", 1, &at) != MKR_XML_MUT_OK
767
- || !slice_eq(at->ns_uri, at->ns_uri_len, "urn:p", 5)) { rc = 15; goto done; }
709
+ || !mkr_bytes_eq(at->ns_uri, at->ns_uri_len, "urn:p", 5)) { rc = 15; goto done; }
768
710
 
769
711
  /* 6. remove by name (idempotent) */
770
712
  if (mkr_xml_remove_attribute(r, "id", 2) != 1) { rc = 16; goto done; }
@@ -813,7 +755,7 @@ mkr_xml_mutate_selftest(void)
813
755
  if (mkr_xml_new_element(doc, "p:c", 3, &ne) != MKR_XML_MUT_OK) { rc = 31; goto done; }
814
756
  if (mkr_xml_insert_child(doc, pr, ne) != MKR_XML_MUT_OK
815
757
  || pr->first_child != ne || ne->parent != pr
816
- || !slice_eq(ne->ns_uri, ne->ns_uri_len, "urn:p", 5)) { rc = 32; goto done; }
758
+ || !mkr_bytes_eq(ne->ns_uri, ne->ns_uri_len, "urn:p", 5)) { rc = 32; goto done; }
817
759
  if (mkr_xml_insert_child(doc, ne, tx) != MKR_XML_MUT_OK || ne->first_child != tx) { rc = 33; goto done; }
818
760
 
819
761
  /* 12. an unbound prefix is a hard error in the live tree, leaving it unchanged */
@@ -830,8 +772,8 @@ mkr_xml_mutate_selftest(void)
830
772
  if (mkr_xml_insert_child(doc, wrap, inner) != MKR_XML_MUT_OK
831
773
  || inner->ns_uri_len != 0) { rc = 37; goto done; } /* deferred while detached */
832
774
  if (mkr_xml_insert_child(doc, pr, wrap) != MKR_XML_MUT_OK
833
- || !slice_eq(wrap->ns_uri, wrap->ns_uri_len, "urn:p", 5)
834
- || !slice_eq(inner->ns_uri, inner->ns_uri_len, "urn:p", 5)) { rc = 38; goto done; }
775
+ || !mkr_bytes_eq(wrap->ns_uri, wrap->ns_uri_len, "urn:p", 5)
776
+ || !mkr_bytes_eq(inner->ns_uri, inner->ns_uri_len, "urn:p", 5)) { rc = 38; goto done; }
835
777
 
836
778
  /* 14. cycle rejection: a node cannot be inserted under its own descendant */
837
779
  if (mkr_xml_insert_child(doc, ne, pr) != MKR_XML_MUT_CYCLE) { rc = 39; goto done; }
@@ -859,7 +801,7 @@ mkr_xml_mutate_selftest(void)
859
801
  mkr_xml_node_t *imp = NULL;
860
802
  int irc = mkr_xml_import_subtree(doc2, pr, &imp);
861
803
  if (irc != MKR_XML_MUT_OK || imp == NULL || imp == pr
862
- || imp->qname_len != 2 || memcmp(imp->qname, "pr", 2) != 0
804
+ || !mkr_bytes_eq(imp->qname, imp->qname_len, "pr", 2)
863
805
  || imp->first_child == NULL || imp->first_child == pr->first_child) { rc = 46; }
864
806
  mkr_xml_doc_destroy(doc2);
865
807
  if (rc != 0) goto done;
@@ -2,12 +2,39 @@
2
2
  * Ruby-free. See mkr_xml_node.h and docs/xml_parser_plan.ja.md §8.1/§8.2. */
3
3
  #include "mkr_xml_node.h"
4
4
  #include "mkr_xml.h"
5
+ #include "mkr_xml_index.h"
5
6
  #include "../core/mkr_core.h"
6
7
 
7
8
  #include <stddef.h> /* max_align_t */
8
9
  #include <stdlib.h>
9
10
  #include <string.h>
10
11
 
12
+ /* ASan red-zoning for the bump arena. Like Lexbor's mraw, our arena carves many
13
+ * sub-allocations out of one malloc'd chunk, so a write past one cut into the
14
+ * next stays inside that single malloc and is INVISIBLE to a plain ASan build
15
+ * (the heap red-zones only guard the chunk's outer boundary - this is exactly
16
+ * how the v3.0.0 :lexbor-contains overflow hid). So we poison each fresh chunk
17
+ * and unpoison only the bytes a cut actually hands out, leaving the alignment
18
+ * tail and not-yet-cut space poisoned: an intra-arena overrun then writes into
19
+ * poisoned memory and ASan reports it. Auto-active whenever our ext is built
20
+ * with -fsanitize=address (rake sanitize / fuzz:sanitize) - no extra flag,
21
+ * because unlike vendored Lexbor this is our own TU. A no-op otherwise. */
22
+ #if defined(__SANITIZE_ADDRESS__)
23
+ # define MKR_XML_HAVE_ASAN 1
24
+ #elif defined(__has_feature)
25
+ # if __has_feature(address_sanitizer)
26
+ # define MKR_XML_HAVE_ASAN 1
27
+ # endif
28
+ #endif
29
+ #if defined(MKR_XML_HAVE_ASAN)
30
+ # include <sanitizer/asan_interface.h>
31
+ # define MKR_ARENA_POISON(p, n) __asan_poison_memory_region((p), (n))
32
+ # define MKR_ARENA_UNPOISON(p, n) __asan_unpoison_memory_region((p), (n))
33
+ #else
34
+ # define MKR_ARENA_POISON(p, n) ((void)(p), (void)(n))
35
+ # define MKR_ARENA_UNPOISON(p, n) ((void)(p), (void)(n))
36
+ #endif
37
+
11
38
  /* Alignment for arena cuts: the strictest fundamental alignment, so any object
12
39
  * fits. The chunk payload starts at an aligned offset past the header, and every
13
40
  * cut size is rounded up to ARENA_ALIGN, so each returned pointer is aligned. */
@@ -37,9 +64,12 @@ void
37
64
  mkr_xml_doc_destroy(mkr_xml_doc_t *doc)
38
65
  {
39
66
  if (doc == NULL) return;
67
+ mkr_xml_name_index_invalidate(doc); /* free the heap-side element-name index */
40
68
  /* whole-arena free: no individual node/byte free anywhere (read-only). */
69
+ const size_t hdr = align_up(sizeof(mkr_xml_arena_chunk_t), ARENA_ALIGN);
41
70
  for (mkr_xml_arena_chunk_t *c = doc->chunks; c != NULL; ) {
42
71
  mkr_xml_arena_chunk_t *n = c->next;
72
+ MKR_ARENA_UNPOISON((char *)c, hdr + c->cap); /* hand clean memory back to ASan's allocator */
43
73
  free(c);
44
74
  c = n;
45
75
  }
@@ -98,15 +128,23 @@ arena_alloc(mkr_xml_doc_t *doc, size_t size)
98
128
  nc->cap = cap;
99
129
  doc->chunks = nc;
100
130
  c = nc;
131
+ /* poison the whole payload; each cut unpoisons only what it returns. */
132
+ MKR_ARENA_POISON((char *)nc + hdr, cap);
101
133
  }
102
134
  void *p = (char *)c + hdr + c->used;
103
135
  c->used += need;
104
136
  doc->arena_bytes += need;
137
+ /* Hand out exactly +size+ usable bytes; the [size, need) alignment tail (and
138
+ * everything past it in the chunk) stays poisoned as a red-zone, so a write
139
+ * one byte past the request is caught instead of silently clobbering the
140
+ * next cut. ASan tolerates the sub-granule trailing byte (ARENA_ALIGN cuts
141
+ * keep each slot on an 8-byte shadow boundary). */
142
+ MKR_ARENA_UNPOISON(p, size);
105
143
  return p;
106
144
  }
107
145
 
108
146
  static int
109
- valid_xn_type(uint8_t type)
147
+ valid_xn_type(mkr_xml_node_type_t type)
110
148
  {
111
149
  switch (type) {
112
150
  case MKR_XML_NODE_TYPE_ELEMENT: case MKR_XML_NODE_TYPE_ATTRIBUTE: case MKR_XML_NODE_TYPE_TEXT:
@@ -120,7 +158,7 @@ valid_xn_type(uint8_t type)
120
158
  }
121
159
 
122
160
  mkr_xml_node_t *
123
- mkr_xml_arena_node(mkr_xml_doc_t *doc, uint8_t type)
161
+ mkr_xml_arena_node(mkr_xml_doc_t *doc, mkr_xml_node_type_t type)
124
162
  {
125
163
  if (doc == NULL) return NULL; /* fail-closed: never deref a NULL document */
126
164
  /* guard against a caller passing a bogus type (programming error) so it can
@@ -149,11 +187,13 @@ mkr_xml_arena_bytes(mkr_xml_doc_t *doc, const char *src, uint32_t len)
149
187
  return p;
150
188
  }
151
189
 
152
- /* A raw, uninitialised arena buffer of exactly +len+ bytes, for the one caller
153
- * that must fill it itself (entity expansion writes the expanded, never-longer
154
- * output here). Budget-/overflow-checked via the same choke point. */
155
- char *
156
- mkr_xml_arena_scratch_bytes(mkr_xml_doc_t *doc, size_t len)
190
+ /* A raw, uninitialised arena buffer of exactly +len+ bytes. PRIVATE: the only
191
+ * way to fill arena bytes by hand is mkr_xml_arena_spanbuf below, which wraps
192
+ * this in a core mkr_spanbuf (cursor + bounds check) so a caller can never
193
+ * overrun it. Budget-/
194
+ * overflow-checked via the same choke point. */
195
+ static char *
196
+ arena_scratch_bytes(mkr_xml_doc_t *doc, size_t len)
157
197
  {
158
198
  /* a 0-length buffer needs no storage - never cut a whole chunk for it. The
159
199
  * sentinel is a valid non-NULL pointer the caller must not write to (there is
@@ -162,21 +202,113 @@ mkr_xml_arena_scratch_bytes(mkr_xml_doc_t *doc, size_t len)
162
202
  return arena_alloc(doc, len);
163
203
  }
164
204
 
205
+ /* Carve +cap+ arena bytes and wrap them in a core mkr_spanbuf (see
206
+ * mkr_xml_node.h). The spanbuf owns the cursor + bounds check, so no caller can
207
+ * overrun the cut; this is the sole sanctioned hand-fill path and the only thing
208
+ * that touches the private arena_scratch_bytes. On alloc failure the buffer is
209
+ * NULL, so the returned writer is already not-ok (every write a no-op). */
210
+ mkr_spanbuf_t
211
+ mkr_xml_arena_spanbuf(mkr_xml_doc_t *doc, size_t cap)
212
+ {
213
+ return mkr_spanbuf(arena_scratch_bytes(doc, cap), cap);
214
+ }
215
+
165
216
  int
166
- mkr_xml_node_xmlns_decl(const mkr_xml_node_t *a, const char **prefix, uint32_t *plen,
167
- const char **uri, uint32_t *ulen)
217
+ mkr_xml_xmlns_prefix(const char *name, uint32_t len, const char **prefix, uint32_t *plen)
168
218
  {
169
- if (a->qname_len == 5 && memcmp(a->qname, "xmlns", 5) == 0) {
170
- *prefix = ""; *plen = 0;
171
- } else if (a->qname_len > 6 && memcmp(a->qname, "xmlns:", 6) == 0) {
172
- *prefix = a->local; *plen = a->local_len;
219
+ const char *p; uint32_t pl;
220
+ mkr_span_t s = mkr_span(name, len);
221
+ if (len == 5 && mkr_span_starts(&s, "xmlns", 5)) { /* read through the span - no raw name deref (NULL-safe) */
222
+ p = ""; pl = 0;
223
+ } else if (len > 6 && mkr_span_starts(&s, "xmlns:", 6)) {
224
+ p = name + 6; pl = len - 6;
173
225
  } else {
174
226
  return 0;
175
227
  }
228
+ if (prefix != NULL) *prefix = p;
229
+ if (plen != NULL) *plen = pl;
230
+ return 1;
231
+ }
232
+
233
+ int
234
+ mkr_xml_node_xmlns_decl(const mkr_xml_node_t *a, const char **prefix, uint32_t *plen,
235
+ const char **uri, uint32_t *ulen)
236
+ {
237
+ /* a built node's local IS qname+6 for "xmlns:p", so the byte detector's
238
+ * prefix slice equals the node's local - one shared rule, value read here. */
239
+ if (!mkr_xml_xmlns_prefix(a->qname, a->qname_len, prefix, plen)) return 0;
176
240
  *uri = a->value ? a->value : ""; *ulen = a->value_len;
177
241
  return 1;
178
242
  }
179
243
 
244
+ mkr_xml_node_t *
245
+ mkr_xml_preorder_next(const mkr_xml_node_t *root, mkr_xml_node_t *cur)
246
+ {
247
+ if (cur->first_child != NULL) return cur->first_child;
248
+ /* climb until a node with a next sibling, stopping at (and not above) root */
249
+ while (cur != root && cur->next == NULL) cur = cur->parent;
250
+ if (cur == root) return NULL;
251
+ return cur->next;
252
+ }
253
+
254
+ int
255
+ mkr_xml_qname_split(const char *name, uint32_t len, mkr_xml_qname_t *out)
256
+ {
257
+ if (len == 0) return -1;
258
+ mkr_span_t s = mkr_span(name, len);
259
+ uint32_t cp;
260
+ int bl = mkr_utf8_decode1_span(&s, &cp);
261
+ if (bl == 0 || !mkr_xml_is_name_start(cp)) return -1;
262
+ mkr_span_skip(&s, (size_t)bl);
263
+ while (mkr_span_left(&s) > 0) {
264
+ bl = mkr_utf8_decode1_span(&s, &cp);
265
+ if (bl == 0 || !mkr_xml_is_name_char(cp)) return -1;
266
+ mkr_span_skip(&s, (size_t)bl);
267
+ }
268
+ out->qname = name; out->qname_len = len;
269
+ mkr_span_t q = mkr_span(name, len);
270
+ size_t colon_at;
271
+ if (!mkr_span_find(&q, ':', &colon_at)) { /* no prefix */
272
+ out->prefix = name; out->prefix_len = 0;
273
+ out->local = name; out->local_len = len;
274
+ return 0;
275
+ }
276
+ uint32_t pl = (uint32_t)colon_at;
277
+ const char *ls = name + colon_at + 1;
278
+ uint32_t ll = len - pl - 1;
279
+ if (pl == 0 || ll == 0) return -1; /* ":x" or "x:" */
280
+ mkr_span_t lsp = mkr_span(ls, ll);
281
+ size_t second;
282
+ if (mkr_span_find(&lsp, ':', &second)) return -1; /* a second colon */
283
+ if (mkr_utf8_decode1_span(&lsp, &cp) == 0 || !mkr_xml_is_name_start(cp))
284
+ return -1; /* local must be an NCName */
285
+ out->prefix = name; out->prefix_len = pl;
286
+ out->local = ls; out->local_len = ll;
287
+ return 0;
288
+ }
289
+
290
+ int
291
+ mkr_xml_qname_assign(mkr_xml_doc_t *doc, mkr_xml_node_t *node, const mkr_xml_qname_t *qn)
292
+ {
293
+ /* Contract: qn->local must alias INTO [qn->qname, qn->qname+qname_len) - the
294
+ * rebase below adds (local - qname) onto the arena copy, so a non-aliasing
295
+ * local would mint an out-of-bounds slice (wrong-but-plausible data, the worst
296
+ * failure mode here). Both in-tree producers (split_qname in mkr_xml_tree.c,
297
+ * mkr_xml_qname_split above) honour it; pin it so a violation fails closed
298
+ * (INTERNAL, sticky) instead of silently passing. */
299
+ if (!(qn->local >= qn->qname && qn->local_len <= qn->qname_len
300
+ && (size_t)(qn->local - qn->qname) <= qn->qname_len - qn->local_len)) {
301
+ if (doc != NULL) doc->oom = MKR_XML_ERR_INTERNAL;
302
+ return -1;
303
+ }
304
+ const char *q = mkr_xml_arena_bytes(doc, qn->qname, qn->qname_len);
305
+ if (qn->qname_len > 0 && q == NULL) return -1; /* OOM: node left untouched */
306
+ node->qname = q; node->qname_len = qn->qname_len;
307
+ node->local = q + (size_t)(qn->local - qn->qname); node->local_len = qn->local_len;
308
+ node->prefix = q; node->prefix_len = qn->prefix_len;
309
+ return 0;
310
+ }
311
+
180
312
  /* ---- self-test (Makiri.__c_selftest) - mirrors tmp/xml_spike/arena_spike.c --- */
181
313
  int
182
314
  mkr_xml_node_selftest(void)
@@ -191,7 +323,7 @@ mkr_xml_node_selftest(void)
191
323
  char nm[] = "Feed";
192
324
  const char *local = mkr_xml_arena_bytes(doc, nm, 4);
193
325
  if (root == NULL || root->first_child != NULL || root->type != MKR_XML_NODE_TYPE_ELEMENT
194
- || local == NULL || local == nm || memcmp(local, "Feed", 4) != 0) {
326
+ || local == NULL || local == nm || !mkr_bytes_eq(local, 4, "Feed", 4)) {
195
327
  mkr_xml_doc_destroy(doc); return idx;
196
328
  }
197
329
  /* pointer alignment */
@@ -256,7 +388,7 @@ mkr_xml_node_selftest(void)
256
388
  idx++; /* 8 */
257
389
  if (mkr_xml_arena_node(NULL, MKR_XML_NODE_TYPE_ELEMENT) != NULL
258
390
  || mkr_xml_arena_bytes(NULL, "x", 1) != NULL
259
- || mkr_xml_arena_scratch_bytes(NULL, 1) != NULL) {
391
+ || mkr_xml_arena_spanbuf(NULL, 1).ok) { /* alloc fails -> not ok */
260
392
  return idx;
261
393
  }
262
394