makiri 0.2.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 (130) 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/release.yml +12 -7
  5. data/.github/workflows/security.yml +88 -3
  6. data/.github/workflows/valgrind.yml +135 -0
  7. data/CHANGELOG.md +152 -15
  8. data/README.md +183 -13
  9. data/Rakefile +294 -7
  10. data/ext/makiri/bridge/bridge.h +28 -0
  11. data/ext/makiri/bridge/ruby_string.c +282 -12
  12. data/ext/makiri/core/mkr_alloc.c +40 -3
  13. data/ext/makiri/core/mkr_alloc.h +28 -5
  14. data/ext/makiri/core/mkr_buf.c +47 -3
  15. data/ext/makiri/core/mkr_buf.h +112 -3
  16. data/ext/makiri/core/mkr_core.c +143 -0
  17. data/ext/makiri/core/mkr_core.h +11 -2
  18. data/ext/makiri/core/mkr_hash.h +1 -1
  19. data/ext/makiri/core/mkr_span.h +186 -0
  20. data/ext/makiri/core/mkr_text.h +8 -8
  21. data/ext/makiri/core/mkr_utf8.c +101 -0
  22. data/ext/makiri/core/mkr_utf8.h +88 -0
  23. data/ext/makiri/extconf.rb +123 -10
  24. data/ext/makiri/fuzz/Makefile +95 -0
  25. data/ext/makiri/fuzz/check_fuzzer.cc +4 -0
  26. data/ext/makiri/fuzz/xml_fuzz.c +24 -0
  27. data/ext/makiri/fuzz/xpath_fuzz.c +109 -0
  28. data/ext/makiri/glue/glue.h +55 -11
  29. data/ext/makiri/glue/ruby_doc.c +129 -59
  30. data/ext/makiri/glue/ruby_html_css.c +292 -0
  31. data/ext/makiri/glue/{ruby_mutate.c → ruby_html_mutate.c} +248 -52
  32. data/ext/makiri/glue/ruby_html_node.c +859 -0
  33. data/ext/makiri/glue/ruby_html_serialize.c +154 -0
  34. data/ext/makiri/glue/ruby_node.c +74 -729
  35. data/ext/makiri/glue/ruby_node_set.c +167 -32
  36. data/ext/makiri/glue/ruby_xml.c +602 -0
  37. data/ext/makiri/glue/ruby_xml_node.c +1373 -0
  38. data/ext/makiri/glue/ruby_xpath.c +63 -30
  39. data/ext/makiri/glue/ruby_xpath.h +19 -0
  40. data/ext/makiri/lexbor_compat/compat.h +42 -9
  41. data/ext/makiri/lexbor_compat/compat_internal.h +1 -1
  42. data/ext/makiri/lexbor_compat/dom_index.c +2 -2
  43. data/ext/makiri/lexbor_compat/post_parse.c +100 -10
  44. data/ext/makiri/lexbor_compat/source_loc.c +15 -13
  45. data/ext/makiri/lexbor_compat/text_index.c +14 -8
  46. data/ext/makiri/lexbor_compat/utf8_input.c +19 -33
  47. data/ext/makiri/makiri.c +184 -6
  48. data/ext/makiri/makiri.h +43 -2
  49. data/ext/makiri/xml/mkr_xml.h +125 -0
  50. data/ext/makiri/xml/mkr_xml_chars.c +195 -0
  51. data/ext/makiri/xml/mkr_xml_index.c +169 -0
  52. data/ext/makiri/xml/mkr_xml_index.h +48 -0
  53. data/ext/makiri/xml/mkr_xml_mutate.c +817 -0
  54. data/ext/makiri/xml/mkr_xml_mutate.h +139 -0
  55. data/ext/makiri/xml/mkr_xml_node.c +399 -0
  56. data/ext/makiri/xml/mkr_xml_node.h +184 -0
  57. data/ext/makiri/xml/mkr_xml_tree.c +1515 -0
  58. data/ext/makiri/xpath/mkr_css.c +1023 -0
  59. data/ext/makiri/xpath/mkr_css.h +65 -0
  60. data/ext/makiri/xpath/mkr_xpath.c +96 -32
  61. data/ext/makiri/xpath/mkr_xpath.h +109 -4
  62. data/ext/makiri/xpath/mkr_xpath_engine_html.c +17 -0
  63. data/ext/makiri/xpath/mkr_xpath_engine_xml.c +12 -0
  64. data/ext/makiri/xpath/{mkr_xpath_eval.c → mkr_xpath_eval_body.h} +551 -241
  65. data/ext/makiri/xpath/{mkr_xpath_funcs.c → mkr_xpath_funcs_body.h} +318 -276
  66. data/ext/makiri/xpath/mkr_xpath_internal.h +177 -206
  67. data/ext/makiri/xpath/mkr_xpath_lex.c +95 -125
  68. data/ext/makiri/xpath/mkr_xpath_node_access_html.h +138 -0
  69. data/ext/makiri/xpath/mkr_xpath_node_access_xml.h +145 -0
  70. data/ext/makiri/xpath/mkr_xpath_number.c +109 -0
  71. data/ext/makiri/xpath/mkr_xpath_parse.c +83 -94
  72. data/ext/makiri/xpath/mkr_xpath_prelude_html.h +30 -0
  73. data/ext/makiri/xpath/mkr_xpath_prelude_xml.h +28 -0
  74. data/ext/makiri/xpath/mkr_xpath_shared.c +609 -0
  75. data/ext/makiri/xpath/mkr_xpath_value_body.h +801 -0
  76. data/ext/makiri/xpath/mkr_xpath_xml_selftest.c +76 -0
  77. data/lib/makiri/{attribute.rb → attr.rb} +7 -3
  78. data/lib/makiri/cdata_section.rb +19 -0
  79. data/lib/makiri/comment.rb +10 -0
  80. data/lib/makiri/compat_aliases.rb +30 -0
  81. data/lib/makiri/document.rb +9 -73
  82. data/lib/makiri/document_fragment.rb +14 -9
  83. data/lib/makiri/element.rb +4 -4
  84. data/lib/makiri/html/document.rb +106 -0
  85. data/lib/makiri/html/node_methods.rb +19 -0
  86. data/lib/makiri/html.rb +12 -0
  87. data/lib/makiri/node.rb +58 -15
  88. data/lib/makiri/node_set.rb +8 -0
  89. data/lib/makiri/processing_instruction.rb +10 -0
  90. data/lib/makiri/text.rb +1 -1
  91. data/lib/makiri/version.rb +1 -1
  92. data/lib/makiri/xml/builder.rb +263 -0
  93. data/lib/makiri/xml/document.rb +24 -0
  94. data/lib/makiri/xml/node_methods.rb +84 -0
  95. data/lib/makiri/xml.rb +10 -0
  96. data/lib/makiri/xpath_context.rb +1 -1
  97. data/lib/makiri.rb +24 -5
  98. data/script/build_native_gem.rb +2 -2
  99. data/script/check_alloc_failures.rb +266 -0
  100. data/script/check_c_safety.rb +77 -2
  101. data/script/check_c_safety_allowlist.yml +102 -0
  102. data/script/check_leaks.rb +64 -0
  103. data/script/leaks_harness.rb +64 -0
  104. data/vendor/lexbor/CMakeLists.txt +6 -0
  105. data/vendor/lexbor/README.md +12 -0
  106. data/vendor/lexbor/config.cmake +1 -1
  107. data/vendor/lexbor/source/lexbor/core/base.h +1 -1
  108. data/vendor/lexbor/source/lexbor/core/config.cmake +9 -1
  109. data/vendor/lexbor/source/lexbor/css/selectors/pseudo_state.c +2 -3
  110. data/vendor/lexbor/source/lexbor/css/selectors/state.c +3 -0
  111. data/vendor/lexbor/source/lexbor/dom/interfaces/element.c +21 -0
  112. data/vendor/lexbor/source/lexbor/dom/interfaces/element.h +5 -0
  113. data/vendor/lexbor/source/lexbor/encoding/decode.c +33 -4
  114. data/vendor/lexbor/source/lexbor/html/base.h +1 -1
  115. data/vendor/lexbor/source/lexbor/html/interfaces/select_element.c +4 -0
  116. data/vendor/lexbor/source/lexbor/html/serialize.c +545 -41
  117. data/vendor/lexbor/source/lexbor/html/serialize.h +2 -1
  118. data/vendor/lexbor/source/lexbor/html/tokenizer.h +2 -2
  119. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/in_body.c +1 -1
  120. data/vendor/lexbor/source/lexbor/html/tree.c +6 -6
  121. data/vendor/lexbor/source/lexbor/selectors/selectors.c +12 -3
  122. data/vendor/lexbor/source/lexbor/url/base.h +1 -1
  123. data/vendor/lexbor/source/lexbor/url/url.c +5 -2
  124. data/vendor/lexbor/source/lexbor/url/url.h +9 -0
  125. data/vendor/lexbor/version +1 -1
  126. metadata +53 -9
  127. data/ext/makiri/glue/ruby_css.c +0 -185
  128. data/ext/makiri/glue/ruby_serialize.c +0 -92
  129. data/ext/makiri/xpath/mkr_xpath_value.c +0 -1286
  130. data/lib/makiri/cdata.rb +0 -6
@@ -0,0 +1,195 @@
1
+ /* mkr_xml_chars.c - XML Char validation + character/entity reference expansion.
2
+ *
3
+ * Ruby-free. §9.1: only the 5 predefined entities (lt gt amp apos quot) and
4
+ * numeric character references (&#nn; / &#xhh;) are expanded; any other &name;
5
+ * is an undefined entity (SyntaxError) - there is no DTD, so no entity can be
6
+ * defined, which makes billion-laughs structurally impossible. §9.2: every
7
+ * resulting codepoint (literal or from a numeric reference) must be an XML 1.0
8
+ * Char; control characters / surrogates / out-of-range are rejected. The input
9
+ * is already valid UTF-8 (§2.1).
10
+ *
11
+ * All input reads go through the bounded reader (core mkr_span_t) and the
12
+ * strict core decoder (mkr_utf8_decode1) - an out-of-bounds read is
13
+ * structurally impossible, not a per-site convention; all output goes through
14
+ * the bounded writer (mkr_spanbuf_t). The lint (raw_scan_call /
15
+ * raw_cursor_member) keeps it that way.
16
+ */
17
+ #include "mkr_xml.h"
18
+ #include "mkr_xml_node.h"
19
+ #include "../core/mkr_core.h" /* mkr_span_t + mkr_utf8_decode1 */
20
+
21
+ #include <string.h>
22
+
23
+ int
24
+ mkr_xml_is_char(uint32_t c)
25
+ {
26
+ /* XML 1.0 §2.2 Char: #x9 | #xA | #xD | [#x20-#xD7FF] |
27
+ * [#xE000-#xFFFD] | [#x10000-#x10FFFF] */
28
+ return c == 0x9u || c == 0xAu || c == 0xDu
29
+ || (c >= 0x20u && c <= 0xD7FFu)
30
+ || (c >= 0xE000u && c <= 0xFFFDu)
31
+ || (c >= 0x10000u && c <= 0x10FFFFu);
32
+ }
33
+
34
+ /* Validate that [src, src+len) is entirely XML 1.0 Char, with NO entity/reference
35
+ * recognition (for comment/CDATA/PI content, where '&' and '<' are literal). 0 if
36
+ * all valid, -1 on the first malformed UTF-8 or non-Char (caller raises SYNTAX).
37
+ * The strict decode (truncation / bad continuation / overlong / surrogate /
38
+ * out-of-range -> 0) is the core mkr_utf8_decode1 - self-contained, not trusting
39
+ * the caller, even if some future caller feeds unvalidated bytes. */
40
+ int
41
+ mkr_xml_validate_chars(const char *src, uint32_t len)
42
+ {
43
+ mkr_span_t s = mkr_span(src, len);
44
+ while (mkr_span_left(&s) > 0) {
45
+ uint32_t cp;
46
+ int bl = mkr_utf8_decode1_span(&s, &cp);
47
+ if (bl == 0 || !mkr_xml_is_char(cp)) return -1;
48
+ mkr_span_skip(&s, (size_t)bl);
49
+ }
50
+ return 0;
51
+ }
52
+
53
+ /* XML 1.0 §2.3 NameStartChar / NameChar (the full Unicode sets, not just ASCII).
54
+ * Element/attribute QNames and PI targets are validated against these so an
55
+ * ill-formed name never reaches the DOM (§9.2b). */
56
+ int
57
+ mkr_xml_is_name_start(uint32_t c)
58
+ {
59
+ return c == ':' || (c >= 'A' && c <= 'Z') || c == '_' || (c >= 'a' && c <= 'z')
60
+ || (c >= 0xC0u && c <= 0xD6u) || (c >= 0xD8u && c <= 0xF6u)
61
+ || (c >= 0xF8u && c <= 0x2FFu) || (c >= 0x370u && c <= 0x37Du)
62
+ || (c >= 0x37Fu && c <= 0x1FFFu) || (c >= 0x200Cu && c <= 0x200Du)
63
+ || (c >= 0x2070u && c <= 0x218Fu) || (c >= 0x2C00u && c <= 0x2FEFu)
64
+ || (c >= 0x3001u && c <= 0xD7FFu) || (c >= 0xF900u && c <= 0xFDCFu)
65
+ || (c >= 0xFDF0u && c <= 0xFFFDu) || (c >= 0x10000u && c <= 0xEFFFFu);
66
+ }
67
+
68
+ int
69
+ mkr_xml_is_name_char(uint32_t c)
70
+ {
71
+ return mkr_xml_is_name_start(c) || c == '-' || c == '.' || (c >= '0' && c <= '9')
72
+ || c == 0xB7u || (c >= 0x300u && c <= 0x36Fu) || (c >= 0x203Fu && c <= 0x2040u);
73
+ }
74
+
75
+ static int
76
+ utf8_encode(uint32_t cp, char *out)
77
+ {
78
+ if (cp < 0x80u) { out[0] = (char)cp; return 1; }
79
+ if (cp < 0x800u) { out[0] = (char)(0xC0u | (cp >> 6));
80
+ out[1] = (char)(0x80u | (cp & 0x3Fu)); return 2; }
81
+ if (cp < 0x10000u) { out[0] = (char)(0xE0u | (cp >> 12));
82
+ out[1] = (char)(0x80u | ((cp >> 6) & 0x3Fu));
83
+ out[2] = (char)(0x80u | (cp & 0x3Fu)); return 3; }
84
+ out[0] = (char)(0xF0u | (cp >> 18));
85
+ out[1] = (char)(0x80u | ((cp >> 12) & 0x3Fu));
86
+ out[2] = (char)(0x80u | ((cp >> 6) & 0x3Fu));
87
+ out[3] = (char)(0x80u | (cp & 0x3Fu));
88
+ return 4;
89
+ }
90
+
91
+ /* Expand references in [src, src+len) and validate XML Char. Output is never
92
+ * longer than the input (every &...; reference is >= 4 chars and yields <= 4
93
+ * bytes), so a buffer of `len` bytes suffices - and the bounded arena writer
94
+ * (mkr_xml_arena_spanbuf + core mkr_spanbuf) enforces that bound rather than
95
+ * trusting it (a write past it fails closed instead of overrunning the arena).
96
+ * Returns the arena slice and sets *out_len;
97
+ * returns NULL on an undefined entity / bad reference / non-XML-Char (sets
98
+ * *status = MKR_XML_ERR_SYNTAX) or arena OOM/LIMIT. */
99
+ const char *
100
+ mkr_xml_expand(mkr_xml_doc_t *doc, const char *src, uint32_t len,
101
+ mkr_xml_expand_mode_t mode, uint32_t *out_len, mkr_xml_status_t *status)
102
+ {
103
+ /* Fail-closed contract guards: this is a public primitive (mkr_xml.h), so it
104
+ * must not dereference a NULL output or document even though the in-tree
105
+ * callers always pass real ones. out_len/status are required outputs (a NULL
106
+ * leaves no channel to report through -> return NULL); a len>0 expansion
107
+ * needs a real doc + source. All are programming errors, never recoverable
108
+ * input. */
109
+ if (out_len == NULL || status == NULL) return NULL;
110
+ if (len == 0) { *out_len = 0; return ""; }
111
+ if (doc == NULL || src == NULL) { *status = MKR_XML_ERR_INTERNAL; return NULL; }
112
+
113
+ /* All output goes through the bounded writer, so no code below can overrun
114
+ * the buffer - a write that would exceed `len` is refused and latches ok=0. */
115
+ mkr_spanbuf_t b = mkr_xml_arena_spanbuf(doc, len);
116
+ if (!b.ok) { *status = doc->oom; return NULL; } /* backing alloc failed */
117
+ mkr_span_t s = mkr_span(src, len);
118
+
119
+ while (mkr_span_left(&s) > 0) {
120
+ if (mkr_span_peek(&s) != '&') {
121
+ uint32_t cp;
122
+ int bl = mkr_utf8_decode1_span(&s, &cp);
123
+ if (bl == 0 || !mkr_xml_is_char(cp)) { *status = MKR_XML_ERR_SYNTAX; return NULL; }
124
+ /* §3.3.3: in an attribute value a *literal* TAB/LF/CR normalizes to a
125
+ * space (reference-derived whitespace is preserved - see below). */
126
+ if (mode == MKR_XML_EXPAND_ATTR
127
+ && (cp == 0x9u || cp == 0xAu || cp == 0xDu)) {
128
+ mkr_spanbuf_putc(&b, ' ');
129
+ mkr_span_skip(&s, (size_t)bl);
130
+ continue;
131
+ }
132
+ mkr_spanbuf_write(&b, mkr_span_mark(&s), (size_t)bl);
133
+ mkr_span_skip(&s, (size_t)bl);
134
+ continue;
135
+ }
136
+
137
+ /* a reference: '&' ... ';' */
138
+ mkr_span_skip(&s, 1); /* past '&' */
139
+ if (mkr_span_peek(&s) == '#') { /* numeric character reference */
140
+ mkr_span_skip(&s, 1);
141
+ int hex = 0;
142
+ /* §4.1: the hex marker is a lowercase 'x' only - "&#X58;" is not-wf
143
+ * (an uppercase 'X' is not a decimal digit either, so it is rejected
144
+ * as "no digits" below). */
145
+ if (mkr_span_peek(&s) == 'x') { hex = 1; mkr_span_skip(&s, 1); }
146
+ uint32_t base = hex ? 16u : 10u;
147
+ uint32_t cp = 0;
148
+ int ndigits = 0;
149
+ for (;;) {
150
+ int d = mkr_span_peek(&s);
151
+ if (d < 0 || d == ';') break;
152
+ uint32_t dig;
153
+ if (d >= '0' && d <= '9') dig = (uint32_t)(d - '0');
154
+ else if (hex && d >= 'a' && d <= 'f') dig = (uint32_t)(d - 'a' + 10);
155
+ else if (hex && d >= 'A' && d <= 'F') dig = (uint32_t)(d - 'A' + 10);
156
+ else { *status = MKR_XML_ERR_SYNTAX; return NULL; }
157
+ /* check BEFORE the multiply-add so a giant &#999...; can never wrap
158
+ * uint32_t into the valid range and be falsely accepted. */
159
+ if (cp > (0x10FFFFu - dig) / base) { *status = MKR_XML_ERR_SYNTAX; return NULL; }
160
+ cp = cp * base + dig;
161
+ ndigits++;
162
+ mkr_span_skip(&s, 1);
163
+ }
164
+ if (mkr_span_peek(&s) != ';' || ndigits == 0) { *status = MKR_XML_ERR_SYNTAX; return NULL; } /* no ';' / no digits */
165
+ mkr_span_skip(&s, 1); /* past ';' */
166
+ if (!mkr_xml_is_char(cp)) { *status = MKR_XML_ERR_SYNTAX; return NULL; }
167
+ /* encode into a safe 4-byte local, then hand the exact length to the
168
+ * bounded writer (the encode itself can never overrun `enc`). */
169
+ char enc[4];
170
+ mkr_spanbuf_write(&b, enc, (size_t)utf8_encode(cp, enc));
171
+ } else { /* named entity */
172
+ size_t nlen;
173
+ if (!mkr_span_find(&s, ';', &nlen)) { *status = MKR_XML_ERR_SYNTAX; return NULL; } /* unterminated */
174
+ const char *ns = mkr_span_mark(&s);
175
+ mkr_span_skip(&s, nlen + 1); /* name + ';' */
176
+ char ch;
177
+ if (mkr_bytes_eq(ns, nlen, "lt", 2)) ch = '<';
178
+ else if (mkr_bytes_eq(ns, nlen, "gt", 2)) ch = '>';
179
+ else if (mkr_bytes_eq(ns, nlen, "amp", 3)) ch = '&';
180
+ else if (mkr_bytes_eq(ns, nlen, "apos", 4)) ch = '\'';
181
+ else if (mkr_bytes_eq(ns, nlen, "quot", 4)) ch = '"';
182
+ else { *status = MKR_XML_ERR_SYNTAX; return NULL; } /* undefined entity */
183
+ mkr_spanbuf_putc(&b, ch);
184
+ }
185
+ }
186
+
187
+ /* By construction the buffer was never overrun; finish returns NULL if a
188
+ * write was refused (the "output <= input" invariant broke - our bug). The
189
+ * backing alloc was already checked at init, so a NULL here means a refused
190
+ * write: fail closed rather than return a truncated expansion. */
191
+ const char *out = mkr_spanbuf_finish(&b);
192
+ if (out == NULL) { *status = MKR_XML_ERR_INTERNAL; return NULL; }
193
+ *out_len = (uint32_t)b.pos;
194
+ return out;
195
+ }
@@ -0,0 +1,169 @@
1
+ /* Element-name index for the XML arena (see mkr_xml_index.h). Open-addressing
2
+ * hash keyed by (local name + namespace URI); each entry holds the
3
+ * document-ordered elements with that name. Two tree passes: count elements to
4
+ * size the table, then fill in document order. Heap-allocated (not the arena),
5
+ * freed on invalidate. OOM fails closed (NULL -> caller walks the tree). */
6
+
7
+ #include "mkr_xml_index.h"
8
+ #include "../core/mkr_core.h"
9
+
10
+ #include <stdlib.h>
11
+ #include <string.h>
12
+
13
+ typedef struct {
14
+ const char *local; /* borrowed from the first element (arena-stable) */
15
+ const char *ns_uri; /* may be NULL (no namespace) */
16
+ uint32_t local_len;
17
+ uint32_t ns_uri_len;
18
+ mkr_xml_node_t **nodes; /* document order */
19
+ size_t count, cap;
20
+ } mkr_xml_index_entry_t;
21
+
22
+ struct mkr_xml_name_index {
23
+ mkr_xml_index_entry_t *buckets;
24
+ size_t cap; /* power of two; 0 only for an empty document */
25
+ };
26
+
27
+ /* FNV-1a over the local name then the namespace URI. */
28
+ static uint64_t
29
+ key_hash(const char *local, size_t local_len, const char *ns_uri, size_t ns_uri_len)
30
+ {
31
+ uint64_t h = 1469598103934665603ULL;
32
+ for (size_t i = 0; i < local_len; i++) { h ^= (unsigned char)local[i]; h *= 1099511628211ULL; }
33
+ h ^= 0xff; h *= 1099511628211ULL; /* separator so "ab"+"" != "a"+"b" */
34
+ for (size_t i = 0; i < ns_uri_len; i++) { h ^= (unsigned char)ns_uri[i]; h *= 1099511628211ULL; }
35
+ return h;
36
+ }
37
+
38
+ static int
39
+ key_eq(const mkr_xml_index_entry_t *e, const char *local, size_t local_len,
40
+ const char *ns_uri, size_t ns_uri_len)
41
+ {
42
+ if (e->local_len != local_len || e->ns_uri_len != ns_uri_len) return 0;
43
+ if (local_len && memcmp(e->local, local, local_len) != 0) return 0;
44
+ if (ns_uri_len && memcmp(e->ns_uri, ns_uri, ns_uri_len) != 0) return 0;
45
+ return 1;
46
+ }
47
+
48
+
49
+ /* Find the entry for the key, or the empty slot to create it (open addressing,
50
+ * power-of-two mask). The table is sized so it never fills (load < 0.5). */
51
+ static mkr_xml_index_entry_t *
52
+ slot_for(mkr_xml_name_index_t *idx, const char *local, size_t local_len,
53
+ const char *ns_uri, size_t ns_uri_len)
54
+ {
55
+ size_t mask = idx->cap - 1;
56
+ size_t i = (size_t)key_hash(local, local_len, ns_uri, ns_uri_len) & mask;
57
+ for (;;) {
58
+ mkr_xml_index_entry_t *e = &idx->buckets[i];
59
+ if (e->local == NULL && e->count == 0) return e; /* empty */
60
+ if (key_eq(e, local, local_len, ns_uri, ns_uri_len)) return e;
61
+ i = (i + 1) & mask;
62
+ }
63
+ }
64
+
65
+ /* Append +node+ to its key's bucket, creating the entry if new. 0 / -1 (OOM). */
66
+ static int
67
+ index_push(mkr_xml_name_index_t *idx, mkr_xml_node_t *node)
68
+ {
69
+ mkr_xml_index_entry_t *e =
70
+ slot_for(idx, node->local, node->local_len, node->ns_uri, node->ns_uri_len);
71
+ if (e->local == NULL && e->count == 0) { /* fresh entry: borrow the key */
72
+ e->local = node->local; e->local_len = node->local_len;
73
+ e->ns_uri = node->ns_uri; e->ns_uri_len = node->ns_uri_len;
74
+ }
75
+ if (mkr_grow_reserve((void **)&e->nodes, &e->cap, e->count + 1, sizeof(*e->nodes)) != MKR_OK) {
76
+ return -1; /* grows geometrically + overflow-safely internally */
77
+ }
78
+ e->nodes[e->count++] = node;
79
+ return 0;
80
+ }
81
+
82
+ /* Count elements (document order is irrelevant here) to size the table. */
83
+ static size_t
84
+ count_elements(mkr_xml_node_t *root)
85
+ {
86
+ size_t n = 0;
87
+ for (mkr_xml_node_t *cur = root; cur != NULL; cur = mkr_xml_preorder_next(root, cur)) {
88
+ if (cur->type == MKR_XML_NODE_TYPE_ELEMENT) n++;
89
+ }
90
+ return n;
91
+ }
92
+
93
+ static mkr_xml_name_index_t *
94
+ build(mkr_xml_doc_t *doc)
95
+ {
96
+ mkr_xml_node_t *root = doc->doc_node;
97
+ if (root == NULL) return NULL;
98
+
99
+ mkr_xml_name_index_t *idx = (mkr_xml_name_index_t *)mkr_callocarray(1, sizeof(*idx));
100
+ if (idx == NULL) return NULL;
101
+
102
+ size_t n = count_elements(root);
103
+ /* Size for load factor < 0.5 (2n+1 slots). The overflow-checked sizer fails
104
+ * closed - unlike the old next_pow2, which saturated to a too-small table on
105
+ * overflow, where open addressing could never find a free slot. */
106
+ size_t want;
107
+ if (!mkr_size_mul(n, 2, &want) || !mkr_size_add(want, 1, &want)
108
+ || !mkr_pow2_ceil(want, &idx->cap)) { free(idx); return NULL; }
109
+ if (idx->cap < 8) idx->cap = 8; /* small floor */
110
+ idx->buckets = (mkr_xml_index_entry_t *)mkr_callocarray(idx->cap, sizeof(*idx->buckets));
111
+ if (idx->buckets == NULL) { free(idx); return NULL; }
112
+
113
+ /* Fill pass: pre-order (document order), elements only. */
114
+ for (mkr_xml_node_t *cur = root; cur != NULL; cur = mkr_xml_preorder_next(root, cur)) {
115
+ if (cur->type == MKR_XML_NODE_TYPE_ELEMENT) {
116
+ if (index_push(idx, cur) != 0) { mkr_xml_name_index_free(idx); return NULL; }
117
+ }
118
+ }
119
+ return idx;
120
+ }
121
+
122
+ mkr_xml_name_index_t *
123
+ mkr_xml_name_index_get(mkr_xml_doc_t *doc)
124
+ {
125
+ if (doc == NULL) return NULL;
126
+ if (doc->name_index != NULL) return (mkr_xml_name_index_t *)doc->name_index;
127
+ mkr_xml_name_index_t *idx = build(doc);
128
+ doc->name_index = idx; /* NULL on OOM: caller walks, retries next time */
129
+ return idx;
130
+ }
131
+
132
+ void
133
+ mkr_xml_name_index_free(mkr_xml_name_index_t *idx)
134
+ {
135
+ if (idx == NULL) return;
136
+ if (idx->buckets != NULL) {
137
+ for (size_t i = 0; i < idx->cap; i++) free(idx->buckets[i].nodes);
138
+ free(idx->buckets);
139
+ }
140
+ free(idx);
141
+ }
142
+
143
+ void
144
+ mkr_xml_name_index_invalidate(mkr_xml_doc_t *doc)
145
+ {
146
+ if (doc == NULL || doc->name_index == NULL) return;
147
+ mkr_xml_name_index_free((mkr_xml_name_index_t *)doc->name_index);
148
+ doc->name_index = NULL;
149
+ }
150
+
151
+ mkr_xml_node_t *const *
152
+ mkr_xml_name_index_lookup(const mkr_xml_name_index_t *idx,
153
+ const char *local, size_t local_len,
154
+ const char *ns_uri, size_t ns_uri_len, size_t *out_count)
155
+ {
156
+ if (out_count != NULL) *out_count = 0;
157
+ if (idx == NULL || idx->cap == 0 || local == NULL) return NULL;
158
+ size_t mask = idx->cap - 1;
159
+ size_t i = (size_t)key_hash(local, local_len, ns_uri, ns_uri_len) & mask;
160
+ for (;;) {
161
+ const mkr_xml_index_entry_t *e = &idx->buckets[i];
162
+ if (e->local == NULL && e->count == 0) return NULL; /* miss */
163
+ if (key_eq(e, local, local_len, ns_uri, ns_uri_len)) {
164
+ if (out_count != NULL) *out_count = e->count;
165
+ return e->nodes;
166
+ }
167
+ i = (i + 1) & mask;
168
+ }
169
+ }
@@ -0,0 +1,48 @@
1
+ #ifndef MKR_XML_INDEX_H
2
+ #define MKR_XML_INDEX_H
3
+
4
+ #include "mkr_xml_node.h"
5
+ #include <stddef.h>
6
+
7
+ #ifdef __cplusplus
8
+ extern "C" {
9
+ #endif
10
+
11
+ /*
12
+ * Element-name index for the XML arena: maps each (local name + namespace URI)
13
+ * to the document-ordered list of elements bearing it, so a document-rooted
14
+ * descendant name test (//entry, css("entry")) is answered from the bucket
15
+ * instead of walking the whole tree. The HTML side has the analogous tag-id
16
+ * index; XML element names are arbitrary strings, so this is keyed by the name
17
+ * bytes (borrowed from the arena, stable until the next mutation).
18
+ *
19
+ * Lazily built and cached on the document; dropped by
20
+ * mkr_xml_name_index_invalidate from the single XML mutation hook (the same
21
+ * discipline as the HTML attr/text indices). Build OOM fails closed: the getter
22
+ * returns NULL and the caller walks the tree.
23
+ */
24
+ typedef struct mkr_xml_name_index mkr_xml_name_index_t;
25
+
26
+ /* The document's element-name index, built and cached on first call. Returns
27
+ * NULL on OOM (caller falls back to a tree walk). */
28
+ mkr_xml_name_index_t *mkr_xml_name_index_get(mkr_xml_doc_t *doc);
29
+
30
+ /* Drop the cached index after a structural mutation (no-op when unbuilt). */
31
+ void mkr_xml_name_index_invalidate(mkr_xml_doc_t *doc);
32
+
33
+ /* Free an index directly (used by mkr_xml_doc_destroy via the invalidate hook). */
34
+ void mkr_xml_name_index_free(mkr_xml_name_index_t *idx);
35
+
36
+ /* The document-ordered elements with local name +local+ and namespace URI
37
+ * +ns_uri+ (ns_uri == NULL / ns_uri_len == 0 means the no-namespace bucket).
38
+ * Returns the borrowed bucket and sets *out_count, or NULL with *out_count 0. */
39
+ mkr_xml_node_t *const *mkr_xml_name_index_lookup(const mkr_xml_name_index_t *idx,
40
+ const char *local, size_t local_len,
41
+ const char *ns_uri, size_t ns_uri_len,
42
+ size_t *out_count);
43
+
44
+ #ifdef __cplusplus
45
+ }
46
+ #endif
47
+
48
+ #endif /* MKR_XML_INDEX_H */