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.
- checksums.yaml +4 -4
- data/.github/workflows/conformance.yml +22 -0
- data/.github/workflows/libfuzzer.yml +83 -0
- data/.github/workflows/release.yml +12 -7
- data/.github/workflows/security.yml +88 -3
- data/.github/workflows/valgrind.yml +135 -0
- data/CHANGELOG.md +152 -15
- data/README.md +183 -13
- data/Rakefile +294 -7
- data/ext/makiri/bridge/bridge.h +28 -0
- data/ext/makiri/bridge/ruby_string.c +282 -12
- data/ext/makiri/core/mkr_alloc.c +40 -3
- data/ext/makiri/core/mkr_alloc.h +28 -5
- data/ext/makiri/core/mkr_buf.c +47 -3
- data/ext/makiri/core/mkr_buf.h +112 -3
- data/ext/makiri/core/mkr_core.c +143 -0
- data/ext/makiri/core/mkr_core.h +11 -2
- data/ext/makiri/core/mkr_hash.h +1 -1
- data/ext/makiri/core/mkr_span.h +186 -0
- data/ext/makiri/core/mkr_text.h +8 -8
- data/ext/makiri/core/mkr_utf8.c +101 -0
- data/ext/makiri/core/mkr_utf8.h +88 -0
- data/ext/makiri/extconf.rb +123 -10
- data/ext/makiri/fuzz/Makefile +95 -0
- data/ext/makiri/fuzz/check_fuzzer.cc +4 -0
- data/ext/makiri/fuzz/xml_fuzz.c +24 -0
- data/ext/makiri/fuzz/xpath_fuzz.c +109 -0
- data/ext/makiri/glue/glue.h +55 -11
- data/ext/makiri/glue/ruby_doc.c +129 -59
- data/ext/makiri/glue/ruby_html_css.c +292 -0
- data/ext/makiri/glue/{ruby_mutate.c → ruby_html_mutate.c} +248 -52
- data/ext/makiri/glue/ruby_html_node.c +859 -0
- data/ext/makiri/glue/ruby_html_serialize.c +154 -0
- data/ext/makiri/glue/ruby_node.c +74 -729
- data/ext/makiri/glue/ruby_node_set.c +167 -32
- data/ext/makiri/glue/ruby_xml.c +602 -0
- data/ext/makiri/glue/ruby_xml_node.c +1373 -0
- data/ext/makiri/glue/ruby_xpath.c +63 -30
- data/ext/makiri/glue/ruby_xpath.h +19 -0
- data/ext/makiri/lexbor_compat/compat.h +42 -9
- data/ext/makiri/lexbor_compat/compat_internal.h +1 -1
- data/ext/makiri/lexbor_compat/dom_index.c +2 -2
- data/ext/makiri/lexbor_compat/post_parse.c +100 -10
- data/ext/makiri/lexbor_compat/source_loc.c +15 -13
- data/ext/makiri/lexbor_compat/text_index.c +14 -8
- data/ext/makiri/lexbor_compat/utf8_input.c +19 -33
- data/ext/makiri/makiri.c +184 -6
- data/ext/makiri/makiri.h +43 -2
- data/ext/makiri/xml/mkr_xml.h +125 -0
- data/ext/makiri/xml/mkr_xml_chars.c +195 -0
- data/ext/makiri/xml/mkr_xml_index.c +169 -0
- data/ext/makiri/xml/mkr_xml_index.h +48 -0
- data/ext/makiri/xml/mkr_xml_mutate.c +817 -0
- data/ext/makiri/xml/mkr_xml_mutate.h +139 -0
- data/ext/makiri/xml/mkr_xml_node.c +399 -0
- data/ext/makiri/xml/mkr_xml_node.h +184 -0
- data/ext/makiri/xml/mkr_xml_tree.c +1515 -0
- data/ext/makiri/xpath/mkr_css.c +1023 -0
- data/ext/makiri/xpath/mkr_css.h +65 -0
- data/ext/makiri/xpath/mkr_xpath.c +96 -32
- data/ext/makiri/xpath/mkr_xpath.h +109 -4
- data/ext/makiri/xpath/mkr_xpath_engine_html.c +17 -0
- data/ext/makiri/xpath/mkr_xpath_engine_xml.c +12 -0
- data/ext/makiri/xpath/{mkr_xpath_eval.c → mkr_xpath_eval_body.h} +551 -241
- data/ext/makiri/xpath/{mkr_xpath_funcs.c → mkr_xpath_funcs_body.h} +318 -276
- data/ext/makiri/xpath/mkr_xpath_internal.h +177 -206
- data/ext/makiri/xpath/mkr_xpath_lex.c +95 -125
- data/ext/makiri/xpath/mkr_xpath_node_access_html.h +138 -0
- data/ext/makiri/xpath/mkr_xpath_node_access_xml.h +145 -0
- data/ext/makiri/xpath/mkr_xpath_number.c +109 -0
- data/ext/makiri/xpath/mkr_xpath_parse.c +83 -94
- data/ext/makiri/xpath/mkr_xpath_prelude_html.h +30 -0
- data/ext/makiri/xpath/mkr_xpath_prelude_xml.h +28 -0
- data/ext/makiri/xpath/mkr_xpath_shared.c +609 -0
- data/ext/makiri/xpath/mkr_xpath_value_body.h +801 -0
- data/ext/makiri/xpath/mkr_xpath_xml_selftest.c +76 -0
- data/lib/makiri/{attribute.rb → attr.rb} +7 -3
- data/lib/makiri/cdata_section.rb +19 -0
- data/lib/makiri/comment.rb +10 -0
- data/lib/makiri/compat_aliases.rb +30 -0
- data/lib/makiri/document.rb +9 -73
- data/lib/makiri/document_fragment.rb +14 -9
- data/lib/makiri/element.rb +4 -4
- data/lib/makiri/html/document.rb +106 -0
- data/lib/makiri/html/node_methods.rb +19 -0
- data/lib/makiri/html.rb +12 -0
- data/lib/makiri/node.rb +58 -15
- data/lib/makiri/node_set.rb +8 -0
- data/lib/makiri/processing_instruction.rb +10 -0
- data/lib/makiri/text.rb +1 -1
- data/lib/makiri/version.rb +1 -1
- data/lib/makiri/xml/builder.rb +263 -0
- data/lib/makiri/xml/document.rb +24 -0
- data/lib/makiri/xml/node_methods.rb +84 -0
- data/lib/makiri/xml.rb +10 -0
- data/lib/makiri/xpath_context.rb +1 -1
- data/lib/makiri.rb +24 -5
- data/script/build_native_gem.rb +2 -2
- data/script/check_alloc_failures.rb +266 -0
- data/script/check_c_safety.rb +77 -2
- data/script/check_c_safety_allowlist.yml +102 -0
- data/script/check_leaks.rb +64 -0
- data/script/leaks_harness.rb +64 -0
- data/vendor/lexbor/CMakeLists.txt +6 -0
- data/vendor/lexbor/README.md +12 -0
- data/vendor/lexbor/config.cmake +1 -1
- data/vendor/lexbor/source/lexbor/core/base.h +1 -1
- data/vendor/lexbor/source/lexbor/core/config.cmake +9 -1
- data/vendor/lexbor/source/lexbor/css/selectors/pseudo_state.c +2 -3
- data/vendor/lexbor/source/lexbor/css/selectors/state.c +3 -0
- data/vendor/lexbor/source/lexbor/dom/interfaces/element.c +21 -0
- data/vendor/lexbor/source/lexbor/dom/interfaces/element.h +5 -0
- data/vendor/lexbor/source/lexbor/encoding/decode.c +33 -4
- data/vendor/lexbor/source/lexbor/html/base.h +1 -1
- data/vendor/lexbor/source/lexbor/html/interfaces/select_element.c +4 -0
- data/vendor/lexbor/source/lexbor/html/serialize.c +545 -41
- data/vendor/lexbor/source/lexbor/html/serialize.h +2 -1
- data/vendor/lexbor/source/lexbor/html/tokenizer.h +2 -2
- data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/in_body.c +1 -1
- data/vendor/lexbor/source/lexbor/html/tree.c +6 -6
- data/vendor/lexbor/source/lexbor/selectors/selectors.c +12 -3
- data/vendor/lexbor/source/lexbor/url/base.h +1 -1
- data/vendor/lexbor/source/lexbor/url/url.c +5 -2
- data/vendor/lexbor/source/lexbor/url/url.h +9 -0
- data/vendor/lexbor/version +1 -1
- metadata +53 -9
- data/ext/makiri/glue/ruby_css.c +0 -185
- data/ext/makiri/glue/ruby_serialize.c +0 -92
- data/ext/makiri/xpath/mkr_xpath_value.c +0 -1286
- 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 - "X" 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 ϧ...; 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 */
|