makiri 0.5.1 → 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/libfuzzer.yml +1 -1
- data/.github/workflows/release.yml +28 -13
- data/.github/workflows/verify.yml +37 -0
- data/CHANGELOG.md +52 -0
- data/README.md +16 -0
- data/Rakefile +14 -7
- data/ext/makiri/bridge/bridge.h +28 -4
- data/ext/makiri/bridge/bridge_internal.h +28 -0
- data/ext/makiri/bridge/ruby_string.c +29 -221
- data/ext/makiri/bridge/xml_decode.c +237 -0
- data/ext/makiri/core/mkr_alloc.c +6 -0
- data/ext/makiri/core/mkr_alloc.h +13 -4
- data/ext/makiri/core/mkr_buf.h +1 -0
- data/ext/makiri/core/mkr_text.h +28 -9
- data/ext/makiri/core/mkr_utf8.h +8 -1
- 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 +15 -7
- data/ext/makiri/fuzz/xml_xpath_fuzz.c +87 -0
- 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 +181 -71
- 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 +30 -31
- data/ext/makiri/xpath/mkr_xpath_funcs_body.h +76 -71
- 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/ext/makiri/xpath/mkr_xpath_xml_selftest.c +7 -10
- 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.rb +11 -0
- data/script/check_c_safety_allowlist.yml +7 -16
- data/suppressions/ruby.supp +22 -0
- data/verify/Makefile +237 -0
- data/verify/cbmc_models.c +38 -0
- data/verify/harness_alloc.c +257 -0
- data/verify/harness_buf.c +167 -0
- data/verify/harness_hash.c +28 -0
- data/verify/harness_span.c +213 -0
- data/verify/harness_spanbuf.c +98 -0
- data/verify/harness_utf8.c +70 -0
- data/verify/harness_utf8_chain.c +64 -0
- data/verify/harness_utf8_words.c +54 -0
- data/verify/harness_xml_chars.c +59 -0
- data/verify/harness_xml_expand.c +72 -0
- data/verify/harness_xml_parse.c +42 -0
- data/verify/harness_xpath_lex.c +51 -0
- data/verify/harness_xpath_number.c +68 -0
- data/verify/harness_xpath_parse.c +57 -0
- data/verify/selftest_main.c +16 -0
- data/verify/stub.c +10 -0
- data/verify/verify.h +54 -0
- metadata +25 -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
|
+
}
|
data/ext/makiri/core/mkr_alloc.c
CHANGED
|
@@ -39,6 +39,12 @@ mkr_reallocarray(void *ptr, size_t count, size_t elem)
|
|
|
39
39
|
free(ptr);
|
|
40
40
|
return NULL;
|
|
41
41
|
}
|
|
42
|
+
if (elem == 0) {
|
|
43
|
+
/* Fail closed like mkr_callocarray rather than fall through to a
|
|
44
|
+
* realloc(ptr, 0) whose free-or-not is implementation-defined; the
|
|
45
|
+
* caller keeps ownership of ptr. */
|
|
46
|
+
return NULL;
|
|
47
|
+
}
|
|
42
48
|
size_t bytes;
|
|
43
49
|
if (!mkr_size_mul(count, elem, &bytes)) {
|
|
44
50
|
return NULL; /* overflow: leave ptr unchanged */
|
data/ext/makiri/core/mkr_alloc.h
CHANGED
|
@@ -48,8 +48,10 @@ mkr_size_mul(size_t a, size_t b, size_t *out)
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/* Compute a new capacity (in elements) that is >= need and grows geometrically
|
|
51
|
-
* from cap, with
|
|
52
|
-
*
|
|
51
|
+
* from cap, with new_cap*elem guaranteed to fit in size_t. Succeeds EXACTLY
|
|
52
|
+
* when need*elem fits: if the geometric target overshoots what elem allows, it
|
|
53
|
+
* falls back to `need` rather than failing, so the only failure is `need`
|
|
54
|
+
* itself being too large. (cbmc-alloc proves success <=> need*elem fits.) */
|
|
53
55
|
static inline bool
|
|
54
56
|
mkr_grow_capacity(size_t cap, size_t need, size_t elem, size_t *new_cap)
|
|
55
57
|
{
|
|
@@ -59,14 +61,21 @@ mkr_grow_capacity(size_t cap, size_t need, size_t elem, size_t *new_cap)
|
|
|
59
61
|
nc *= 2;
|
|
60
62
|
}
|
|
61
63
|
size_t bytes;
|
|
62
|
-
if (!mkr_size_mul(nc, elem, &bytes))
|
|
64
|
+
if (!mkr_size_mul(nc, elem, &bytes)) {
|
|
65
|
+
/* Geometric overshoot doesn't fit; retry at exactly `need` so a
|
|
66
|
+
* representable need never fails from the *2 headroom alone. */
|
|
67
|
+
nc = need;
|
|
68
|
+
if (!mkr_size_mul(nc, elem, &bytes)) return false;
|
|
69
|
+
}
|
|
63
70
|
*new_cap = nc;
|
|
64
71
|
return true;
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
/* realloc(ptr, count * elem) with the multiply overflow-checked. Returns NULL
|
|
68
75
|
* on overflow or OOM; the original ptr is then unchanged (caller still owns
|
|
69
|
-
* it). count == 0 frees and returns NULL
|
|
76
|
+
* it). count == 0 frees ptr and returns NULL; elem == 0 (with count > 0)
|
|
77
|
+
* returns NULL and leaves ptr unchanged (no implementation-defined
|
|
78
|
+
* realloc(ptr, 0)). */
|
|
70
79
|
void *mkr_reallocarray(void *ptr, size_t count, size_t elem);
|
|
71
80
|
|
|
72
81
|
/* calloc(count, elem) with the multiply overflow-checked, result zeroed.
|
data/ext/makiri/core/mkr_buf.h
CHANGED
|
@@ -143,6 +143,7 @@ static inline void
|
|
|
143
143
|
mkr_spanbuf_write(mkr_spanbuf_t *b, const void *src, size_t n)
|
|
144
144
|
{
|
|
145
145
|
if (!b->ok || n == 0) return;
|
|
146
|
+
if (src == NULL) { b->ok = false; return; } /* fail closed, never memcpy(,NULL,n) */
|
|
146
147
|
if (b->pos > b->cap || n > b->cap - b->pos) { b->ok = false; return; }
|
|
147
148
|
memcpy(b->buf + b->pos, src, n);
|
|
148
149
|
b->pos += n;
|
data/ext/makiri/core/mkr_text.h
CHANGED
|
@@ -22,17 +22,28 @@ extern "C" {
|
|
|
22
22
|
/* A borrowed byte slice whose contents are guaranteed to satisfy Makiri's
|
|
23
23
|
* engine text contract: valid UTF-8, no interior NUL, and NUL-terminated at
|
|
24
24
|
* ptr[len]. The XPath engine's public string entry points accept ONLY this
|
|
25
|
-
* type, so a raw `const char *` cannot be passed without a compile error.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
25
|
+
* type, so a raw `const char *` cannot be passed without a compile error.
|
|
26
|
+
* Constructors:
|
|
27
|
+
* - mkr_verified_text_from_view() at the Ruby boundary (bridge), fed a view
|
|
28
|
+
* that mkr_ruby_verified_text() already validated;
|
|
29
|
+
* - mkr_verified_text_lit() below, for compile-time string literals (which
|
|
30
|
+
* meet the contract by construction).
|
|
31
|
+
* There is deliberately no Ruby-free constructor over runtime bytes, so
|
|
32
|
+
* unvalidated bytes have no path into the engine. (ptr is non-owning: the
|
|
33
|
+
* caller keeps it alive for the duration of the engine call.) */
|
|
31
34
|
typedef struct {
|
|
32
35
|
const char *ptr;
|
|
33
36
|
size_t len;
|
|
34
37
|
} mkr_verified_text_t;
|
|
35
38
|
|
|
39
|
+
/* Wrap a compile-time string literal as a verified-text token. The literal is
|
|
40
|
+
* NUL-terminated, NUL-free, and valid UTF-8 by construction, so it meets the
|
|
41
|
+
* engine text contract without validation - the sole Ruby-free mint, for
|
|
42
|
+
* selftest / fuzz harness literals that the bridge cannot reach. Do not pass a
|
|
43
|
+
* char* variable: sizeof(pointer) would not be the byte length. */
|
|
44
|
+
#define mkr_verified_text_lit(lit) \
|
|
45
|
+
((mkr_verified_text_t){ (lit), sizeof(lit) - 1 })
|
|
46
|
+
|
|
36
47
|
/* ---------------------------------------------------------------- */
|
|
37
48
|
/* string-type lattice */
|
|
38
49
|
/* ---------------------------------------------------------------- */
|
|
@@ -59,10 +70,18 @@ typedef struct {
|
|
|
59
70
|
* raw slice today. When something does, name it mkr_borrowed_bytes_t per the
|
|
60
71
|
* convention above; do not resurrect a placeholder before there is a user.
|
|
61
72
|
*
|
|
73
|
+
* One bridge-only variant sits BETWEEN bytes and text on the contract axis:
|
|
74
|
+
* mkr_ruby_borrowed_data_t (bridge.h) is "valid UTF-8 but interior NUL
|
|
75
|
+
* PERMITTED" - a strict superset of text - for DOM data-family mutations
|
|
76
|
+
* (text/comment/CDATA content, attribute value), where U+0000 is legal data.
|
|
77
|
+
* It never enters the engine (the engine still requires NUL-free text), so it
|
|
78
|
+
* is deliberately absent from this engine lattice.
|
|
79
|
+
*
|
|
62
80
|
* (*) mkr_verified_text_t (defined above) is the one deliberate naming exception:
|
|
63
|
-
* it is a borrowed valid text AND a capability token. Its
|
|
64
|
-
* mkr_verified_text_from_view() at the Ruby boundary
|
|
65
|
-
*
|
|
81
|
+
* it is a borrowed valid text AND a capability token. Its constructors are
|
|
82
|
+
* mkr_verified_text_from_view() at the Ruby boundary and mkr_verified_text_lit()
|
|
83
|
+
* for compile-time literals, so an unvalidated runtime const char* cannot
|
|
84
|
+
* reach the engine's public API. Internally the engine carries the
|
|
66
85
|
* freely-constructible mkr_borrowed_text_t instead.
|
|
67
86
|
*
|
|
68
87
|
* Conversions - the only sanctioned edges. The points that actually VALIDATE
|
data/ext/makiri/core/mkr_utf8.h
CHANGED
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
extern "C" {
|
|
33
33
|
#endif
|
|
34
34
|
|
|
35
|
+
/* src must be non-NULL when len > 0 (it is dereferenced through [0, len)); src
|
|
36
|
+
* may be NULL only when len == 0 (trivially valid). len > 0 with src == NULL is
|
|
37
|
+
* a caller-contract violation, not a fail-closed case - this is a hot path
|
|
38
|
+
* guarded at the call site, not here. */
|
|
35
39
|
bool mkr_utf8_valid(const unsigned char *src, size_t len);
|
|
36
40
|
|
|
37
41
|
/* mkr_utf8_decode1 - decode ONE code point from [p, p+len), strictly: rejects
|
|
@@ -53,7 +57,10 @@ mkr_utf8_decode1_span(const mkr_span_t *s, uint32_t *cp)
|
|
|
53
57
|
/* mkr_utf8_count_chars - count Unicode code points in [ptr, ptr+len): every
|
|
54
58
|
* byte that is NOT a 0x80..0xBF continuation byte starts a new code point.
|
|
55
59
|
* Length-bounded (does not rely on a NUL terminator); ptr may be NULL when
|
|
56
|
-
* len == 0. Used where XPath measures string length / offsets in characters.
|
|
60
|
+
* len == 0. Used where XPath measures string length / offsets in characters.
|
|
61
|
+
* Semantics: on VALID UTF-8 this equals the code-point count (proved in
|
|
62
|
+
* cbmc-utf8-chain); on invalid bytes it is only a lead-byte-class scan (no
|
|
63
|
+
* decoding), so the strong count/boundary guarantees hold for valid text. */
|
|
57
64
|
static inline size_t
|
|
58
65
|
mkr_utf8_count_chars(const char *ptr, size_t len)
|
|
59
66
|
{
|
|
@@ -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
|