makiri 0.6.0 → 0.7.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/ci.yml +2 -1
- data/.github/workflows/release.yml +28 -13
- data/CHANGELOG.md +40 -0
- data/Rakefile +9 -8
- data/ext/makiri/bridge/bridge_internal.h +28 -0
- data/ext/makiri/bridge/ruby_string.c +4 -221
- data/ext/makiri/bridge/xml_decode.c +237 -0
- data/ext/makiri/dom_adapter/cross_import.c +20 -7
- data/ext/makiri/dom_adapter/dom_index.c +22 -21
- data/ext/makiri/dom_adapter/post_parse.c +27 -22
- data/ext/makiri/dom_adapter/text_index.c +28 -19
- data/ext/makiri/extconf.rb +40 -3
- data/ext/makiri/fuzz/Makefile +1 -1
- data/ext/makiri/fuzz/xpath_fuzz.c +10 -5
- data/ext/makiri/glue/glue.h +14 -0
- data/ext/makiri/glue/ruby_doc.c +50 -24
- data/ext/makiri/glue/ruby_html_mutate.c +176 -66
- data/ext/makiri/glue/ruby_html_node.c +76 -52
- data/ext/makiri/glue/ruby_xml.c +60 -91
- data/ext/makiri/glue/ruby_xml_node.c +175 -14
- data/ext/makiri/makiri.c +8 -7
- data/ext/makiri/xml/mkr_xml.h +5 -0
- data/ext/makiri/xml/mkr_xml_chars.c +10 -0
- data/ext/makiri/xml/mkr_xml_mutate.c +228 -38
- data/ext/makiri/xml/mkr_xml_mutate.h +46 -8
- data/ext/makiri/xml/mkr_xml_node.c +21 -12
- data/ext/makiri/xml/mkr_xml_node.h +39 -9
- data/ext/makiri/xml/mkr_xml_tree.c +39 -45
- data/ext/makiri/xpath/mkr_css.c +19 -34
- data/ext/makiri/xpath/mkr_xpath.c +34 -31
- data/ext/makiri/xpath/mkr_xpath_eval_body.h +27 -29
- data/ext/makiri/xpath/mkr_xpath_funcs_body.h +29 -33
- data/ext/makiri/xpath/mkr_xpath_internal.h +1 -1
- data/ext/makiri/xpath/mkr_xpath_parse.c +125 -157
- data/ext/makiri/xpath/mkr_xpath_value_body.h +34 -45
- data/lib/makiri/attr.rb +0 -3
- data/lib/makiri/comment.rb +1 -3
- data/lib/makiri/compat_aliases.rb +1 -12
- data/lib/makiri/version.rb +1 -1
- data/script/check_c_safety_allowlist.yml +4 -10
- data/verify/Makefile +1 -1
- metadata +4 -2
- /data/ext/makiri/core/{mkr_core.c → mkr_core_selftest.c} +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/* xml_decode.c - XML 1.0 Appendix F byte-encoding autodetection and strict
|
|
2
|
+
* decode-to-UTF-8 for the XML reader. Part of the bridge layer, split out of
|
|
3
|
+
* ruby_string.c so this XML-specific charset subsystem is separate from the
|
|
4
|
+
* generic Ruby String helpers. Raw RSTRING access stays isolated in
|
|
5
|
+
* ruby_string.c; this file borrows bytes only through mkr_ruby_bytes_view.
|
|
6
|
+
* Shares the allocation-free strict-text-contract core (mkr_text_check) via
|
|
7
|
+
* bridge_internal.h. See mkr_xml_decode_input in bridge.h for the contract. */
|
|
8
|
+
#include "bridge.h"
|
|
9
|
+
#include "../makiri.h"
|
|
10
|
+
#include "bridge_internal.h"
|
|
11
|
+
|
|
12
|
+
#include <string.h>
|
|
13
|
+
|
|
14
|
+
/* rb_str_encode with no replacement flags: an undefined conversion or invalid
|
|
15
|
+
* byte sequence RAISES (Encoding::UndefinedConversionError /
|
|
16
|
+
* Encoding::InvalidByteSequenceError) instead of substituting U+FFFD. Run under
|
|
17
|
+
* rb_protect so we can remap the Ruby Encoding error to Makiri::XML::SyntaxError. */
|
|
18
|
+
static VALUE
|
|
19
|
+
mkr_xml_strict_transcode_thunk(VALUE str)
|
|
20
|
+
{
|
|
21
|
+
return rb_str_encode(str, rb_enc_from_encoding(rb_utf8_encoding()), 0, Qnil);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* --- XML 1.0 Appendix F: byte-encoding autodetection (BOM, then declaration) ---
|
|
25
|
+
*
|
|
26
|
+
* The leading byte-order mark, or NULL; *bom_len gets its length. UTF-32 BOMs are
|
|
27
|
+
* checked before the UTF-16 LE BOM they share a prefix with.
|
|
28
|
+
*
|
|
29
|
+
* *stride / *ascii_off get the interleave geometry of the ASCII column the decl
|
|
30
|
+
* scanner later extracts (default 1/0 for a single-byte stream). It is resolved
|
|
31
|
+
* HERE, at the match, rather than re-derived downstream, because that derivation
|
|
32
|
+
* needs rb_enc_find (it can autoload an encoding = a GC point) and the decl
|
|
33
|
+
* scanner reads a borrowed RSTRING view that must not be held across one - so
|
|
34
|
+
* the scanner is kept allocation-free until its reads are done. Each span read
|
|
35
|
+
* of p still finishes before the rb_enc_find in the return. */
|
|
36
|
+
static rb_encoding *
|
|
37
|
+
mkr_xml_bom_encoding(const unsigned char *p, long len, long *bom_len, long *stride, long *ascii_off)
|
|
38
|
+
{
|
|
39
|
+
mkr_span_t s = mkr_span((const char *)p, (size_t)len);
|
|
40
|
+
*bom_len = 0;
|
|
41
|
+
*stride = 1;
|
|
42
|
+
*ascii_off = 0;
|
|
43
|
+
if (mkr_span_starts(&s, "\x00\x00\xFE\xFF", 4)) { *bom_len = 4; *stride = 4; *ascii_off = 3; return rb_enc_find("UTF-32BE"); }
|
|
44
|
+
if (mkr_span_starts(&s, "\xFF\xFE\x00\x00", 4)) { *bom_len = 4; *stride = 4; *ascii_off = 0; return rb_enc_find("UTF-32LE"); }
|
|
45
|
+
if (mkr_span_starts(&s, "\xFE\xFF", 2)) { *bom_len = 2; *stride = 2; *ascii_off = 1; return rb_enc_find("UTF-16BE"); }
|
|
46
|
+
if (mkr_span_starts(&s, "\xFF\xFE", 2)) { *bom_len = 2; *stride = 2; *ascii_off = 0; return rb_enc_find("UTF-16LE"); }
|
|
47
|
+
if (mkr_span_starts(&s, "\xEF\xBB\xBF", 3)) { *bom_len = 3; return rb_utf8_encoding(); }
|
|
48
|
+
return NULL;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static int
|
|
52
|
+
mkr_decl_ws(int c)
|
|
53
|
+
{
|
|
54
|
+
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* The encoding named in the '<?xml ... encoding="NAME" ?>' declaration, or NULL.
|
|
58
|
+
* The declaration is ASCII; for a UTF-16/32-detected document its bytes are
|
|
59
|
+
* stride-interleaved, so the ASCII column is extracted (stride/off resolved by
|
|
60
|
+
* the BOM matcher) before the scan, letting a BOM-vs-declaration conflict be
|
|
61
|
+
* caught even in UTF-16.
|
|
62
|
+
*
|
|
63
|
+
* p is a borrowed RSTRING view, so this stays allocation-free until every read
|
|
64
|
+
* of p is done: the stride/off geometry is passed in (rather than derived here
|
|
65
|
+
* via rb_enc_find, which can autoload = a GC point), and the only rb_enc_find -
|
|
66
|
+
* the final name lookup - runs after the bytes have been copied into head[]. */
|
|
67
|
+
static rb_encoding *
|
|
68
|
+
mkr_xml_decl_encoding(const unsigned char *p, long len, long stride, long off)
|
|
69
|
+
{
|
|
70
|
+
/* Extract the ASCII column (per the BOM stride) through bounded reads into
|
|
71
|
+
* a bounded writer - neither side trusts the loop arithmetic. */
|
|
72
|
+
mkr_span_t in = mkr_span((const char *)p, len < 0 ? 0 : (size_t)len);
|
|
73
|
+
char head[256];
|
|
74
|
+
mkr_spanbuf_t hw = mkr_spanbuf(head, sizeof(head));
|
|
75
|
+
for (size_t i = (size_t)off; hw.pos < sizeof(head); i += (size_t)stride) {
|
|
76
|
+
int c = mkr_span_at(&in, i);
|
|
77
|
+
if (c < 0) break;
|
|
78
|
+
mkr_spanbuf_putc(&hw, (char)c);
|
|
79
|
+
}
|
|
80
|
+
mkr_span_t h = mkr_span(head, hw.pos);
|
|
81
|
+
size_t hn = hw.pos;
|
|
82
|
+
|
|
83
|
+
size_t i = 0;
|
|
84
|
+
while (mkr_decl_ws(mkr_span_at(&h, i))) i++;
|
|
85
|
+
{
|
|
86
|
+
mkr_span_t t = mkr_span_tail(&h, i);
|
|
87
|
+
if (!mkr_span_starts(&t, "<?xml", 5)) return NULL;
|
|
88
|
+
}
|
|
89
|
+
i += 5;
|
|
90
|
+
/* find a whitespace-introduced "encoding" before the '?>' */
|
|
91
|
+
for (; i + 8 <= hn; i++) {
|
|
92
|
+
if (mkr_span_at(&h, i) == '?' && mkr_span_at(&h, i + 1) == '>') return NULL; /* end of decl */
|
|
93
|
+
mkr_span_t t = mkr_span_tail(&h, i);
|
|
94
|
+
if (!mkr_decl_ws(mkr_span_at(&h, i - 1)) || !mkr_span_starts(&t, "encoding", 8)) continue;
|
|
95
|
+
size_t j = i + 8;
|
|
96
|
+
while (mkr_decl_ws(mkr_span_at(&h, j))) j++;
|
|
97
|
+
if (mkr_span_at(&h, j) != '=') return NULL;
|
|
98
|
+
j++;
|
|
99
|
+
while (mkr_decl_ws(mkr_span_at(&h, j))) j++;
|
|
100
|
+
int q = mkr_span_at(&h, j);
|
|
101
|
+
if (q != '"' && q != '\'') return NULL;
|
|
102
|
+
j++;
|
|
103
|
+
size_t ns = j;
|
|
104
|
+
while (mkr_span_at(&h, j) >= 0 && mkr_span_at(&h, j) != q) j++;
|
|
105
|
+
if (j >= hn) return NULL;
|
|
106
|
+
char name[64];
|
|
107
|
+
size_t nl = j - ns;
|
|
108
|
+
if (nl == 0 || nl >= sizeof(name)) return NULL;
|
|
109
|
+
memcpy(name, head + ns, nl);
|
|
110
|
+
name[nl] = '\0';
|
|
111
|
+
return rb_enc_find(name); /* NULL for an unknown encoding name */
|
|
112
|
+
}
|
|
113
|
+
return NULL;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Two encodings agree for conflict purposes when identical, or when either is
|
|
117
|
+
* US-ASCII (a subset of UTF-8 and the single-byte encodings). */
|
|
118
|
+
static int
|
|
119
|
+
mkr_xml_enc_compatible(rb_encoding *a, rb_encoding *b)
|
|
120
|
+
{
|
|
121
|
+
return a == b || a == rb_usascii_encoding() || b == rb_usascii_encoding();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Phase 1: resolve the input's effective byte encoding (XML 1.0 Appendix F). A
|
|
125
|
+
* BOM wins, else the '<?xml encoding=?>' declaration, else the String's own
|
|
126
|
+
* declared encoding; ASCII-8BIT ("raw bytes, no claimed encoding") is decoded by
|
|
127
|
+
* the detected encoding instead. Raises Makiri::XML::SyntaxError on any
|
|
128
|
+
* BOM/declaration/String-encoding conflict, so the caller only ever sees a
|
|
129
|
+
* single, self-consistent encoding. Reads borrowed RSTRING bytes but stays
|
|
130
|
+
* allocation-free until its reads are done (see the BOM/decl scanners). */
|
|
131
|
+
static rb_encoding *
|
|
132
|
+
mkr_xml_effective_encoding(VALUE str)
|
|
133
|
+
{
|
|
134
|
+
rb_encoding *tag = rb_enc_get(str);
|
|
135
|
+
mkr_ruby_borrowed_bytes_t v = mkr_ruby_bytes_view(str);
|
|
136
|
+
const unsigned char *raw = (const unsigned char *)v.ptr;
|
|
137
|
+
long rawlen = (long)v.len;
|
|
138
|
+
|
|
139
|
+
long bom_len = 0, bom_stride = 1, bom_off = 0;
|
|
140
|
+
rb_encoding *bom = mkr_xml_bom_encoding(raw, rawlen, &bom_len, &bom_stride, &bom_off);
|
|
141
|
+
/* rb_enc_find inside the BOM lookup can autoload an encoding (a Ruby
|
|
142
|
+
* allocation = a GC point), so re-borrow the bytes before reading them
|
|
143
|
+
* again - a borrowed view must not be held across one. The interleave
|
|
144
|
+
* geometry (stride/off) is resolved by the BOM matcher and passed through,
|
|
145
|
+
* keeping the decl scanner itself allocation-free. */
|
|
146
|
+
v = mkr_ruby_bytes_view(str);
|
|
147
|
+
raw = (const unsigned char *)v.ptr;
|
|
148
|
+
rb_encoding *decl = mkr_xml_decl_encoding(raw + bom_len, rawlen - bom_len, bom_stride, bom_off);
|
|
149
|
+
int is_binary = (tag == rb_ascii8bit_encoding());
|
|
150
|
+
|
|
151
|
+
if (bom && decl && !mkr_xml_enc_compatible(bom, decl)) {
|
|
152
|
+
rb_raise(mkr_eXmlSyntaxError,
|
|
153
|
+
"XML encoding conflict: the byte-order mark and the encoding declaration disagree");
|
|
154
|
+
}
|
|
155
|
+
if (!is_binary && bom && !mkr_xml_enc_compatible(bom, tag)) {
|
|
156
|
+
rb_raise(mkr_eXmlSyntaxError,
|
|
157
|
+
"XML encoding conflict: the byte-order mark disagrees with the string's encoding");
|
|
158
|
+
}
|
|
159
|
+
if (!is_binary && decl && !mkr_xml_enc_compatible(decl, tag)) {
|
|
160
|
+
/* A concrete String encoding is authoritative for decoding, so the
|
|
161
|
+
* declaration is not used to transcode - but a declaration that names a
|
|
162
|
+
* different encoding than the String is tagged with (e.g. a Shift_JIS
|
|
163
|
+
* String declaring encoding="UTF-8") is a self-inconsistent document and
|
|
164
|
+
* a fatal error, not a silently-ignored mismatch. */
|
|
165
|
+
rb_raise(mkr_eXmlSyntaxError,
|
|
166
|
+
"XML encoding conflict: the encoding declaration disagrees with the string's encoding");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return is_binary ? (bom ? bom : (decl ? decl : rb_utf8_encoding())) : tag;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
VALUE
|
|
173
|
+
mkr_xml_decode_input(VALUE str, size_t max_bytes)
|
|
174
|
+
{
|
|
175
|
+
/* Phase 1: resolve the single effective byte encoding (raises on conflict). */
|
|
176
|
+
rb_encoding *eff = mkr_xml_effective_encoding(str);
|
|
177
|
+
|
|
178
|
+
/* Phase 2: decode to UTF-8 (strict). UTF-8 / US-ASCII / ASCII-8BIT are
|
|
179
|
+
* already UTF-8 bytes (validated below); anything else is strict-transcoded,
|
|
180
|
+
* raising rather than substituting U+FFFD. */
|
|
181
|
+
VALUE s;
|
|
182
|
+
if (eff == rb_utf8_encoding() || eff == rb_usascii_encoding() || eff == rb_ascii8bit_encoding()) {
|
|
183
|
+
s = str;
|
|
184
|
+
} else {
|
|
185
|
+
VALUE in = str;
|
|
186
|
+
if (rb_enc_get(str) != eff) { in = rb_str_dup(str); rb_enc_associate(in, eff); }
|
|
187
|
+
int state = 0;
|
|
188
|
+
s = rb_protect(mkr_xml_strict_transcode_thunk, in, &state);
|
|
189
|
+
if (state != 0) {
|
|
190
|
+
VALUE exc = rb_errinfo();
|
|
191
|
+
rb_set_errinfo(Qnil);
|
|
192
|
+
char msg[256];
|
|
193
|
+
mkr_ruby_exception_message(exc, msg, sizeof msg);
|
|
194
|
+
rb_raise(mkr_eXmlSyntaxError,
|
|
195
|
+
"XML input could not be decoded to UTF-8: %s", msg);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
mkr_ruby_borrowed_bytes_t sv_bytes = mkr_ruby_bytes_view(s);
|
|
200
|
+
const char *ptr = sv_bytes.ptr;
|
|
201
|
+
long len = (long)sv_bytes.len;
|
|
202
|
+
long off = 0;
|
|
203
|
+
/* §4.3.3: a leading BOM is the encoding signature, not document content -
|
|
204
|
+
* strip a U+FEFF (the transcode above turns any UTF-16/32 BOM into one). */
|
|
205
|
+
mkr_span_t sv = mkr_span(ptr, (size_t)len);
|
|
206
|
+
if (mkr_span_starts(&sv, "\xEF\xBB\xBF", 3)) {
|
|
207
|
+
off = 3; len -= 3;
|
|
208
|
+
mkr_span_skip(&sv, 3);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* Fail closed on an over-budget input BEFORE the validation scan and the
|
|
212
|
+
* caller's GVL-release copy (an input whose UTF-8 length exceeds the arena
|
|
213
|
+
* budget can never parse). max_bytes == 0 disables the check (__decode). */
|
|
214
|
+
if (max_bytes != 0 && (size_t)len > max_bytes) {
|
|
215
|
+
rb_raise(mkr_eXmlLimitExceeded, "XML input exceeds the byte budget");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* Strict UTF-8 validation via the shared, allocation-free core - no GC point
|
|
219
|
+
* while `ptr` is borrowed: an embedded NUL or any invalid UTF-8 is fatal (no
|
|
220
|
+
* U+FFFD repair - unlike the HTML mkr_utf8_sanitize path). The whole-string
|
|
221
|
+
* `s` is consulted for the cached coderange (it covers the BOM-stripped
|
|
222
|
+
* suffix too - the BOM is one complete UTF-8 character), while the validated
|
|
223
|
+
* bytes are the stripped suffix `ptr + off`. */
|
|
224
|
+
switch (mkr_text_check(s, ptr + off, (size_t)len)) {
|
|
225
|
+
case MKR_TEXT_HAS_NUL:
|
|
226
|
+
rb_raise(mkr_eXmlSyntaxError, "XML input must not contain a NUL byte");
|
|
227
|
+
case MKR_TEXT_INVALID_UTF8:
|
|
228
|
+
rb_raise(mkr_eXmlSyntaxError, "XML input must be valid UTF-8");
|
|
229
|
+
case MKR_TEXT_OK:
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
/* Build the result through the VALUE, not the borrowed ptr (rb_str_subseq
|
|
233
|
+
* allocates, so the ptr must not be what it copies from). */
|
|
234
|
+
VALUE u = rb_str_subseq(s, off, len);
|
|
235
|
+
rb_enc_associate(u, rb_utf8_encoding());
|
|
236
|
+
return u; /* validated, UTF-8-tagged, BOM-stripped */
|
|
237
|
+
}
|
|
@@ -237,6 +237,21 @@ h2x_make(mkr_xml_doc_t *xdoc, lxb_dom_node_t *s, const char *pdef, uint32_t pdef
|
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
/* If +n+ is an HTML <template> element, set *content to its content fragment
|
|
241
|
+
* (which may itself be NULL) and return true; else return false. The shared
|
|
242
|
+
* template detection for both import directions - the two callers then apply
|
|
243
|
+
* their own (different) rule for a template whose content is NULL. */
|
|
244
|
+
static bool
|
|
245
|
+
mkr_lxb_template_content(lxb_dom_node_t *n, lxb_dom_document_fragment_t **content)
|
|
246
|
+
{
|
|
247
|
+
if (n->type == LXB_DOM_NODE_TYPE_ELEMENT
|
|
248
|
+
&& n->local_name == LXB_TAG_TEMPLATE && n->ns == LXB_NS_HTML) {
|
|
249
|
+
*content = lxb_html_interface_template(n)->content;
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
240
255
|
/* The children to translate under +s+. An HTML <template> keeps its content in a
|
|
241
256
|
* separate document fragment, NOT the normal child chain, so a plain first_child
|
|
242
257
|
* walk would silently drop template contents. We descend into the content fragment
|
|
@@ -245,9 +260,8 @@ h2x_make(mkr_xml_doc_t *xdoc, lxb_dom_node_t *s, const char *pdef, uint32_t pdef
|
|
|
245
260
|
static lxb_dom_node_t *
|
|
246
261
|
h2x_children_of(lxb_dom_node_t *s)
|
|
247
262
|
{
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
lxb_dom_document_fragment_t *content = lxb_html_interface_template(s)->content;
|
|
263
|
+
lxb_dom_document_fragment_t *content;
|
|
264
|
+
if (mkr_lxb_template_content(s, &content)) {
|
|
251
265
|
return content != NULL ? lxb_dom_interface_node(content)->first_child : NULL;
|
|
252
266
|
}
|
|
253
267
|
return s->first_child;
|
|
@@ -384,10 +398,9 @@ x2h_make(lxb_dom_document_t *hdoc, const mkr_xml_node_t *s, lxb_dom_node_t **out
|
|
|
384
398
|
static lxb_dom_node_t *
|
|
385
399
|
x2h_link_target(lxb_dom_node_t *el)
|
|
386
400
|
{
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (content != NULL) return lxb_dom_interface_node(content);
|
|
401
|
+
lxb_dom_document_fragment_t *content;
|
|
402
|
+
if (mkr_lxb_template_content(el, &content) && content != NULL) {
|
|
403
|
+
return lxb_dom_interface_node(content);
|
|
391
404
|
}
|
|
392
405
|
return el;
|
|
393
406
|
}
|
|
@@ -126,44 +126,36 @@ mkr_dom_index_build(mkr_dom_index_t *idx, lxb_dom_document_t *doc)
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/* The four sizing allocations, all freed once through `fail:` (single-exit).
|
|
130
|
+
* `cursor` is scratch (not stored on idx) and is freed on the success path too. */
|
|
131
|
+
mkr_dom_index_attr_slot_t *slots = NULL;
|
|
132
|
+
lxb_dom_node_t **tag_nodes = NULL;
|
|
133
|
+
size_t *tag_off = NULL;
|
|
134
|
+
size_t *cursor = NULL;
|
|
135
|
+
size_t cap = 0;
|
|
136
|
+
|
|
129
137
|
/* Size the attr->owner table (load factor <= 0.5). */
|
|
130
|
-
mkr_dom_index_attr_slot_t *slots = NULL;
|
|
131
|
-
size_t cap = 0;
|
|
132
138
|
if (n_attrs > 0) {
|
|
133
139
|
size_t need;
|
|
134
|
-
if (!mkr_size_mul(n_attrs, 2, &need) || !mkr_pow2_ceil(need, &cap))
|
|
135
|
-
return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
|
|
136
|
-
}
|
|
140
|
+
if (!mkr_size_mul(n_attrs, 2, &need) || !mkr_pow2_ceil(need, &cap)) goto fail;
|
|
137
141
|
if (cap < 8) cap = 8;
|
|
138
142
|
slots = mkr_callocarray(cap, sizeof(*slots));
|
|
139
|
-
if (slots == NULL)
|
|
143
|
+
if (slots == NULL) goto fail;
|
|
140
144
|
}
|
|
141
145
|
|
|
142
146
|
/* Size the tag CSR: prefix-sum the per-tag counts into offsets, plus a
|
|
143
147
|
* cursor (copy of the offsets) used to scatter in document order. */
|
|
144
|
-
lxb_dom_node_t **tag_nodes = NULL;
|
|
145
|
-
size_t *tag_off = NULL;
|
|
146
|
-
size_t *cursor = NULL;
|
|
147
148
|
if (n_indexed > 0) {
|
|
148
149
|
size_t noff = (size_t)tag_max + 2;
|
|
149
150
|
size_t off_bytes;
|
|
150
|
-
if (!mkr_size_mul(noff, sizeof(*cursor), &off_bytes))
|
|
151
|
-
free(slots);
|
|
152
|
-
return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
|
|
153
|
-
}
|
|
151
|
+
if (!mkr_size_mul(noff, sizeof(*cursor), &off_bytes)) goto fail;
|
|
154
152
|
tag_off = mkr_reallocarray(NULL, noff, sizeof(*tag_off));
|
|
155
153
|
cursor = mkr_reallocarray(NULL, noff, sizeof(*cursor));
|
|
156
154
|
tag_nodes = mkr_reallocarray(NULL, n_indexed, sizeof(*tag_nodes));
|
|
157
|
-
if (tag_off == NULL || cursor == NULL || tag_nodes == NULL)
|
|
158
|
-
free(slots); free(tag_off); free(cursor); free(tag_nodes);
|
|
159
|
-
return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
|
|
160
|
-
}
|
|
155
|
+
if (tag_off == NULL || cursor == NULL || tag_nodes == NULL) goto fail;
|
|
161
156
|
tag_off[0] = 0;
|
|
162
157
|
for (uintptr_t t = 0; t <= tag_max; t++) {
|
|
163
|
-
if (!mkr_size_add(tag_off[t], counts[t], &tag_off[t + 1]))
|
|
164
|
-
free(slots); free(tag_off); free(cursor); free(tag_nodes);
|
|
165
|
-
return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
|
|
166
|
-
}
|
|
158
|
+
if (!mkr_size_add(tag_off[t], counts[t], &tag_off[t + 1])) goto fail;
|
|
167
159
|
}
|
|
168
160
|
memcpy(cursor, tag_off, off_bytes);
|
|
169
161
|
}
|
|
@@ -205,6 +197,15 @@ mkr_dom_index_build(mkr_dom_index_t *idx, lxb_dom_document_t *doc)
|
|
|
205
197
|
free(cursor);
|
|
206
198
|
idx->built = true;
|
|
207
199
|
return LXB_STATUS_OK;
|
|
200
|
+
|
|
201
|
+
fail:
|
|
202
|
+
/* Reached only before the idx->... assignments above transfer ownership, so
|
|
203
|
+
* these are all still locals (phase 2 has no failure path). */
|
|
204
|
+
free(slots);
|
|
205
|
+
free(tag_nodes);
|
|
206
|
+
free(tag_off);
|
|
207
|
+
free(cursor);
|
|
208
|
+
return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
|
|
208
209
|
}
|
|
209
210
|
|
|
210
211
|
static lxb_dom_node_t *
|
|
@@ -22,46 +22,45 @@ mkr_parse_tracked(const lxb_char_t *src, size_t len, void **out_lines)
|
|
|
22
22
|
{
|
|
23
23
|
*out_lines = NULL;
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
/* All resources declared up front and freed once through `fail:` (single-exit),
|
|
26
|
+
* so the correct free-set lives in one place instead of being re-spelled at
|
|
27
|
+
* each early return. Every destructor below is NULL-safe (guarded / by
|
|
28
|
+
* contract), so ordering across the acquisition points stays trivial. */
|
|
29
|
+
lxb_html_parser_t *parser = NULL;
|
|
30
|
+
lxb_html_document_t *doc = NULL;
|
|
31
|
+
mkr_pos_recorder_t *rec = NULL;
|
|
32
|
+
lxb_status_t st;
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
parser = lxb_html_parser_create();
|
|
35
|
+
if (parser == NULL || lxb_html_parser_init(parser) != LXB_STATUS_OK) goto fail;
|
|
36
|
+
|
|
37
|
+
doc = lxb_html_parse_chunk_begin(parser);
|
|
38
|
+
if (doc == NULL) goto fail;
|
|
38
39
|
|
|
39
40
|
/* Install the recorder, chaining the parser's own tree-building callback
|
|
40
41
|
* (set by chunk_begin). If the recorder can't be allocated we simply parse
|
|
41
42
|
* without source tracking. */
|
|
42
|
-
|
|
43
|
+
rec = mkr_pos_recorder_create(src);
|
|
43
44
|
if (rec != NULL) {
|
|
44
45
|
lxb_html_tokenizer_t *tkz = lxb_html_parser_tokenizer(parser);
|
|
46
|
+
/* Lexbor has a setter and a ctx getter for the token-done callback but no
|
|
47
|
+
* getter for the callback function itself, so read that one field
|
|
48
|
+
* directly (the ctx uses the public getter). */
|
|
45
49
|
mkr_pos_recorder_set_delegate(rec, tkz->callback_token_done,
|
|
46
|
-
tkz
|
|
50
|
+
lxb_html_tokenizer_callback_token_done_ctx(tkz));
|
|
47
51
|
lxb_html_tokenizer_callback_token_done_set(tkz, mkr_pos_token_cb, rec);
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
|
|
54
|
+
st = lxb_html_parse_chunk_process(parser, src, len);
|
|
51
55
|
if (st == LXB_STATUS_OK) {
|
|
52
56
|
st = lxb_html_parse_chunk_end(parser);
|
|
53
57
|
}
|
|
54
|
-
|
|
55
|
-
if (st != LXB_STATUS_OK) {
|
|
56
|
-
mkr_pos_recorder_destroy(rec);
|
|
57
|
-
lxb_html_document_destroy(doc);
|
|
58
|
-
lxb_html_parser_destroy(parser);
|
|
59
|
-
return NULL;
|
|
60
|
-
}
|
|
58
|
+
if (st != LXB_STATUS_OK) goto fail;
|
|
61
59
|
|
|
62
60
|
if (rec != NULL) {
|
|
63
61
|
mkr_pos_assign_to_dom(rec, lxb_dom_interface_node(doc));
|
|
64
62
|
mkr_pos_recorder_destroy(rec);
|
|
63
|
+
rec = NULL; /* consumed */
|
|
65
64
|
*out_lines = mkr_lines_build(src, len);
|
|
66
65
|
}
|
|
67
66
|
|
|
@@ -69,6 +68,12 @@ mkr_parse_tracked(const lxb_char_t *src, size_t len, void **out_lines)
|
|
|
69
68
|
* tokenizer and tree, never the document). */
|
|
70
69
|
lxb_html_parser_destroy(parser);
|
|
71
70
|
return doc;
|
|
71
|
+
|
|
72
|
+
fail:
|
|
73
|
+
mkr_pos_recorder_destroy(rec); /* NULL-safe */
|
|
74
|
+
if (doc != NULL) lxb_html_document_destroy(doc);
|
|
75
|
+
if (parser != NULL) lxb_html_parser_destroy(parser);
|
|
76
|
+
return NULL;
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
mkr_parsed_t *
|
|
@@ -115,6 +115,18 @@ mkr_tix_is_container(const lxb_dom_node_t *n)
|
|
|
115
115
|
|| n->type == LXB_DOM_NODE_TYPE_DOCUMENT_FRAGMENT;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/* The non-empty character-data payload of a TEXT/CDATA node, or NULL (for a
|
|
119
|
+
* non-text node or an empty one). The single "does this node contribute a text
|
|
120
|
+
* slice" test, shared by the count and fill passes so the two-pass sizing and
|
|
121
|
+
* filling can never disagree about which nodes yield a slice. */
|
|
122
|
+
static const lexbor_str_t *
|
|
123
|
+
mkr_tix_text_slice(lxb_dom_node_t *n)
|
|
124
|
+
{
|
|
125
|
+
if (!mkr_tix_is_text(n)) return NULL;
|
|
126
|
+
const lexbor_str_t *d = &lxb_dom_interface_character_data(n)->data;
|
|
127
|
+
return (d->data != NULL && d->length != 0) ? d : NULL;
|
|
128
|
+
}
|
|
129
|
+
|
|
118
130
|
/* Pass 1: count text slices and container (element/fragment) nodes under root
|
|
119
131
|
* (inclusive), so the slice arrays and the range table are each sized once. */
|
|
120
132
|
static void
|
|
@@ -124,9 +136,8 @@ mkr_tix_count(lxb_dom_node_t *root, size_t *out_slices, size_t *out_containers)
|
|
|
124
136
|
for (lxb_dom_node_t *n = root; n != NULL; n = mkr_dom_preorder_next(n, root)) {
|
|
125
137
|
if (mkr_tix_is_container(n)) {
|
|
126
138
|
nc++;
|
|
127
|
-
} else if (
|
|
128
|
-
|
|
129
|
-
if (d->data != NULL && d->length != 0) ns++;
|
|
139
|
+
} else if (mkr_tix_text_slice(n) != NULL) {
|
|
140
|
+
ns++;
|
|
130
141
|
}
|
|
131
142
|
}
|
|
132
143
|
*out_slices = ns;
|
|
@@ -205,23 +216,21 @@ mkr_text_idx_build(lxb_dom_node_t *root)
|
|
|
205
216
|
}
|
|
206
217
|
top->child = child->next; /* advance the cursor for our return */
|
|
207
218
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return NULL;
|
|
219
|
-
}
|
|
220
|
-
t->slices[i].ptr = (const char *)d->data;
|
|
221
|
-
t->slices[i].len = d->length;
|
|
222
|
-
t->prefix[i + 1] = total;
|
|
223
|
-
t->nslices = i + 1;
|
|
219
|
+
const lexbor_str_t *d = mkr_tix_text_slice(child);
|
|
220
|
+
if (d != NULL) {
|
|
221
|
+
size_t i = t->nslices;
|
|
222
|
+
size_t total;
|
|
223
|
+
if (!mkr_size_add(t->prefix[i], d->length, &total)) {
|
|
224
|
+
/* cumulative text length overflows size_t - give up the
|
|
225
|
+
* index (fail closed); the caller falls back to a walk. */
|
|
226
|
+
free(st);
|
|
227
|
+
mkr_text_idx_destroy(t);
|
|
228
|
+
return NULL;
|
|
224
229
|
}
|
|
230
|
+
t->slices[i].ptr = (const char *)d->data;
|
|
231
|
+
t->slices[i].len = d->length;
|
|
232
|
+
t->prefix[i + 1] = total;
|
|
233
|
+
t->nslices = i + 1;
|
|
225
234
|
} else if (mkr_tix_is_container(child)) {
|
|
226
235
|
size_t r = mkr_tix_range_insert(t, child, t->nslices);
|
|
227
236
|
/* grow_reserve may move st; top is re-fetched at the loop head and
|
data/ext/makiri/extconf.rb
CHANGED
|
@@ -28,6 +28,10 @@ abort "Lexbor source not found at #{LEXBOR_SRC}. Did you `git submodule update -
|
|
|
28
28
|
|
|
29
29
|
cmake = find_executable("cmake") or abort "cmake is required to build Lexbor."
|
|
30
30
|
|
|
31
|
+
# Windows (RubyInstaller / MinGW-UCRT gcc). Used below to force the GNU
|
|
32
|
+
# toolchain for the Lexbor build and to keep the Ruby import-library link.
|
|
33
|
+
windows = RbConfig::CONFIG["target_os"] =~ /mingw|mswin/
|
|
34
|
+
|
|
31
35
|
# Optionally build the vendored Lexbor itself under AddressSanitizer. This is the
|
|
32
36
|
# ONLY way to catch overflows *inside* Lexbor's bump (mraw) arena: a sub-allocation
|
|
33
37
|
# overrunning into the next one stays within one big malloc'd chunk, so the heap
|
|
@@ -55,6 +59,13 @@ unless stamp_ok
|
|
|
55
59
|
Dir.chdir(LEXBOR_BLD) do
|
|
56
60
|
cmd = [
|
|
57
61
|
cmake,
|
|
62
|
+
# On Windows the runner ships Visual Studio, so a bare cmake defaults to the
|
|
63
|
+
# MSVC generator -> lexbor_static.lib under a Release/ subdir, breaking the
|
|
64
|
+
# hardcoded liblexbor_static.a path below. Force the GNU toolchain via Ninja
|
|
65
|
+
# (bundled by lukka/get-cmake; single-config, so no Release/ subdir) and pin
|
|
66
|
+
# the MinGW-UCRT gcc so cmake can't auto-probe MSVC. "MinGW Makefiles" is
|
|
67
|
+
# avoided: cmake refuses it when sh.exe is on PATH (MSYS2/Git-Bash put it there).
|
|
68
|
+
*(windows ? ["-G", "Ninja", "-DCMAKE_C_COMPILER=gcc"] : []),
|
|
58
69
|
"-DLEXBOR_BUILD_SHARED=OFF",
|
|
59
70
|
"-DLEXBOR_BUILD_STATIC=ON",
|
|
60
71
|
"-DLEXBOR_BUILD_TESTS=OFF",
|
|
@@ -65,10 +76,18 @@ unless stamp_ok
|
|
|
65
76
|
"-DCMAKE_INSTALL_PREFIX=#{LEXBOR_DST}",
|
|
66
77
|
*(lexbor_asan ? ["-DLEXBOR_BUILD_WITH_ASAN=ON"] : []),
|
|
67
78
|
LEXBOR_SRC,
|
|
68
|
-
]
|
|
79
|
+
]
|
|
69
80
|
warn "makiri: building vendored Lexbor (mode=#{lexbor_mode})"
|
|
70
|
-
system(
|
|
71
|
-
|
|
81
|
+
# Multi-arg system() bypasses the shell, so args pass verbatim on every
|
|
82
|
+
# platform. Do NOT shelljoin: Shellwords escapes `=` as `\=`, which a POSIX
|
|
83
|
+
# shell unwraps but Windows cmd.exe does not, mangling every -DNAME=VALUE
|
|
84
|
+
# (cmake then ignores CMAKE_INSTALL_PREFIX etc. and installs to the default).
|
|
85
|
+
system(*cmd) or abort "cmake configure failed for Lexbor."
|
|
86
|
+
nproc = Etc.respond_to?(:nprocessors) ? Etc.nprocessors : 4
|
|
87
|
+
# `-- -jN` forwards to make and is wrong for Ninja; use cmake's portable
|
|
88
|
+
# --parallel (cmake >= 3.12, satisfied by get-cmake) on Windows.
|
|
89
|
+
build_parallel = windows ? ["--parallel", nproc.to_s] : ["--", "-j#{nproc}"]
|
|
90
|
+
system(cmake, "--build", ".", "--target", "install", *build_parallel) or
|
|
72
91
|
abort "cmake build/install failed for Lexbor."
|
|
73
92
|
end
|
|
74
93
|
File.write(lexbor_stamp, lexbor_mode)
|
|
@@ -77,6 +96,13 @@ end
|
|
|
77
96
|
$INCFLAGS << " -I#{File.join(LEXBOR_DST, 'include').shellescape}"
|
|
78
97
|
$INCFLAGS << " -I#{File.join(EXT_DIR).shellescape}"
|
|
79
98
|
|
|
99
|
+
# On Windows, Lexbor's public headers default LXB_API to __declspec(dllimport)
|
|
100
|
+
# (lexbor/core/def.h), emitting __imp_* references that don't resolve against the
|
|
101
|
+
# STATIC archive we link. Lexbor's own static CMake target defines LEXBOR_STATIC to
|
|
102
|
+
# neutralize this, but we link the .a directly (not via CMake's exported target),
|
|
103
|
+
# so we must define it ourselves. Harmless (and a no-op macro) on other platforms.
|
|
104
|
+
$CFLAGS << " -DLEXBOR_STATIC" if windows
|
|
105
|
+
|
|
80
106
|
# Hard-link the static archive rather than pass -L/-llexbor_static, to
|
|
81
107
|
# avoid accidentally linking a system-installed Lexbor.
|
|
82
108
|
lexbor_archive = File.join(LEXBOR_DST, "lib", "liblexbor_static.a")
|
|
@@ -176,6 +202,10 @@ elsif RbConfig::CONFIG["target_os"] =~ /linux/
|
|
|
176
202
|
$LIBRUBYARG = "" # the ruby executable already provides them
|
|
177
203
|
$LIBRUBYARG_SHARED = ""
|
|
178
204
|
$LIBRUBYARG_STATIC = ""
|
|
205
|
+
# else (mingw/mswin): intentionally NOT blanked. PE/COFF has no dynamic_lookup
|
|
206
|
+
# equivalent, so a MinGW extension MUST link the Ruby import library - we fall
|
|
207
|
+
# through to mkmf's default libruby link. The precompiled x64-mingw-ucrt gem
|
|
208
|
+
# still loads on any compatible Ruby of that ABI (they share ruby.dll's name).
|
|
179
209
|
end
|
|
180
210
|
|
|
181
211
|
# Export ONLY Init_makiri from the compiled extension. `-fvisibility=hidden`
|
|
@@ -194,6 +224,13 @@ elsif RbConfig::CONFIG["target_os"] =~ /linux/
|
|
|
194
224
|
# are already hidden by -fvisibility=hidden, leaving just RUBY_FUNC_EXPORTED
|
|
195
225
|
# Init_makiri in the dynamic symbol table.
|
|
196
226
|
$DLDFLAGS << " -Wl,--exclude-libs,ALL"
|
|
227
|
+
elsif windows
|
|
228
|
+
# PE DLLs export nothing unless __declspec(dllexport); Init_makiri (marked
|
|
229
|
+
# RUBY_FUNC_EXPORTED) is the only such export, which already turns off MinGW
|
|
230
|
+
# ld's auto-export-all. Belt-and-suspenders parity with the arms above: keep
|
|
231
|
+
# the export table to Init_makiri so a co-loaded Lexbor-based ext (e.g.
|
|
232
|
+
# nokolexbor) can't bind our private lxb_*.
|
|
233
|
+
$DLDFLAGS << " -Wl,--exclude-all-symbols"
|
|
197
234
|
end
|
|
198
235
|
|
|
199
236
|
# Recursively pick up C sources under ext/makiri/, excluding standalone
|
data/ext/makiri/fuzz/Makefile
CHANGED
|
@@ -86,11 +86,16 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
|
|
|
86
86
|
mkr_xpath_context_t *ctx = mkr_xpath_context_new(doc->doc_node, doc->doc_node);
|
|
87
87
|
if (ctx) {
|
|
88
88
|
mkr_xpath_set_engine_kind(ctx, 1); /* XML engine */
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
/* Shrink the per-evaluate budgets ON THE CONTEXT the evaluator actually
|
|
90
|
+
* reads: mkr_xpath_eval_compiled consults ctx->limits, not a local struct,
|
|
91
|
+
* so tighten via mkr_ctx_limits(ctx) (as xml_xpath_fuzz.c does) - otherwise
|
|
92
|
+
* a hostile expression would run under the large defaults and stall the
|
|
93
|
+
* fuzzer. */
|
|
94
|
+
mkr_xpath_limits_t *L = mkr_ctx_limits(ctx);
|
|
95
|
+
L->max_eval_ops = 5 * 1000 * 1000; /* 5M ops - enough for a real query */
|
|
96
|
+
L->max_nodeset_size = 10000;
|
|
97
|
+
L->max_string_bytes = 1024 * 1024;
|
|
98
|
+
L->max_recursion_depth = 64;
|
|
94
99
|
|
|
95
100
|
mkr_xpath_value_t out = {0};
|
|
96
101
|
mkr_xpath_error_t eval_err = {0};
|
data/ext/makiri/glue/glue.h
CHANGED
|
@@ -105,6 +105,20 @@ void mkr_import_fragment_children(lxb_dom_document_t *doc, lxb_dom_node_t *root,
|
|
|
105
105
|
void mkr_emit_append(lxb_dom_node_t *imported, void *u);
|
|
106
106
|
void mkr_emit_before(lxb_dom_node_t *imported, void *u);
|
|
107
107
|
|
|
108
|
+
/* Shared transient-fragment-parser scaffold (glue/ruby_doc.c): create+init an
|
|
109
|
+
* HTML parser, sanitize +html+ to UTF-8 bytes, run +parse+ (which calls the
|
|
110
|
+
* appropriate lxb_html_parse_fragment* and returns the fragment root), free the
|
|
111
|
+
* decoded input, and destroy the parser - returning the root, which is owned by
|
|
112
|
+
* its (possibly transient) document and survives the parser's destruction.
|
|
113
|
+
* Raises on parser-create / OOM / parse failure. Both fragment paths
|
|
114
|
+
* (Document#fragment via a tag-id context, inner_html=/outer_html= via an element
|
|
115
|
+
* context) share this so the create/sanitize/free/destroy + error cleanup lives
|
|
116
|
+
* once. */
|
|
117
|
+
typedef lxb_dom_node_t *(*mkr_fragment_parse_fn)(lxb_html_parser_t *parser,
|
|
118
|
+
const lxb_char_t *hsrc, size_t hlen,
|
|
119
|
+
void *ctx);
|
|
120
|
+
lxb_dom_node_t *mkr_run_fragment_parser(VALUE html, mkr_fragment_parse_fn parse, void *ctx);
|
|
121
|
+
|
|
108
122
|
/* Node#clone_node(deep=false): shallow/deep DOM clone owned by this node's
|
|
109
123
|
* document (import_node + <template>-content fixup), detached from any parent.
|
|
110
124
|
* Implemented in ruby_doc.c (next to the import machinery), bound in
|