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
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
#include <lexbor/encoding/encoding.h>
|
|
5
5
|
|
|
6
|
+
#include <string.h> /* memcpy for the word-at-a-time ASCII scan */
|
|
7
|
+
|
|
6
8
|
/* ------------------------------------------------------------------ */
|
|
7
9
|
/* UTF-8 input sanitisation (browser-compatible HTML decoding) */
|
|
8
10
|
/* ------------------------------------------------------------------ */
|
|
@@ -14,36 +16,12 @@
|
|
|
14
16
|
* via mkr_utf8_sanitize (declared in compat.h).
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
|
-
/*
|
|
18
|
-
* (
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
static bool
|
|
24
|
-
mkr_utf8_valid(const lxb_char_t *src, size_t len)
|
|
25
|
-
{
|
|
26
|
-
const lxb_encoding_data_t *u8 = lxb_encoding_data(LXB_ENCODING_UTF_8);
|
|
27
|
-
lxb_encoding_decode_t dec;
|
|
28
|
-
lxb_codepoint_t cp[1024];
|
|
29
|
-
const lxb_char_t *data = src, *end = src + len;
|
|
30
|
-
lxb_status_t st;
|
|
31
|
-
|
|
32
|
-
if (lxb_encoding_decode_init(&dec, u8, cp,
|
|
33
|
-
sizeof(cp) / sizeof(cp[0])) != LXB_STATUS_OK) {
|
|
34
|
-
return false; /* treat as invalid -> caller transcodes (fail safe) */
|
|
35
|
-
}
|
|
36
|
-
/* No replace_set: an invalid sequence aborts with LXB_STATUS_ERROR. */
|
|
37
|
-
do {
|
|
38
|
-
st = u8->decode(&dec, &data, end);
|
|
39
|
-
lxb_encoding_decode_buf_used_set(&dec, 0); /* discard code points */
|
|
40
|
-
} while (st == LXB_STATUS_SMALL_BUFFER);
|
|
41
|
-
|
|
42
|
-
if (st != LXB_STATUS_OK) {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
return lxb_encoding_decode_finish(&dec) == LXB_STATUS_OK;
|
|
46
|
-
}
|
|
19
|
+
/* UTF-8 validation: the shared, allocation-free validator lives in core
|
|
20
|
+
* (core/mkr_utf8.c, via mkr_core.h) so this fast path and the Ruby bridge's
|
|
21
|
+
* strict input gate (mkr_verify_text) run ONE implementation. Its contract
|
|
22
|
+
* here is unchanged: it returns true *only* for input that
|
|
23
|
+
* mkr_utf8_replace_invalid would leave byte-identical, so "valid" can safely
|
|
24
|
+
* skip the transcode. (lxb_char_t is unsigned char, so the call is direct.) */
|
|
47
25
|
|
|
48
26
|
/* Transcode UTF-8 -> UTF-8 replacing every invalid sequence with U+FFFD
|
|
49
27
|
* (WHATWG byte-stream decoding), into a freshly malloc'd, NUL-terminated
|
|
@@ -73,9 +51,17 @@ mkr_utf8_replace_invalid(const lxb_char_t *src, size_t len, size_t *out_len)
|
|
|
73
51
|
(void) lxb_encoding_encode_replace_set(&enc, LXB_ENCODING_REPLACEMENT_BYTES,
|
|
74
52
|
LXB_ENCODING_REPLACEMENT_SIZE);
|
|
75
53
|
|
|
76
|
-
/*
|
|
77
|
-
*
|
|
78
|
-
|
|
54
|
+
/* The output is at most 3x the input: WHATWG byte-stream decoding replaces
|
|
55
|
+
* each invalid byte with U+FFFD (3 bytes) and passes valid bytes through 1:1.
|
|
56
|
+
* Cap the buffer at exactly that bound - tight and tied to the actual input,
|
|
57
|
+
* so a large document still parses but nothing runs away - rather than a
|
|
58
|
+
* blanket ceiling. (len > 0 here: mkr_utf8_sanitize returns early on len == 0.)
|
|
59
|
+
* It grows overflow-safe and fails closed (NULL) on OOM or the cap. */
|
|
60
|
+
size_t cap;
|
|
61
|
+
if (!mkr_size_mul(len, 3, &cap)) {
|
|
62
|
+
cap = MKR_BUF_HARD_MAX; /* unreachable for any real in-memory string */
|
|
63
|
+
}
|
|
64
|
+
mkr_buf_init(&buf, cap);
|
|
79
65
|
|
|
80
66
|
#define MKR_FLUSH() \
|
|
81
67
|
do { \
|
data/ext/makiri/makiri.c
CHANGED
|
@@ -1,25 +1,54 @@
|
|
|
1
1
|
#include "makiri.h"
|
|
2
2
|
#include "core/mkr_core.h"
|
|
3
|
+
#include "bridge/bridge.h"
|
|
4
|
+
#include "xml/mkr_xml.h"
|
|
5
|
+
#include "xml/mkr_xml_mutate.h"
|
|
3
6
|
|
|
4
7
|
VALUE mkr_mMakiri;
|
|
5
8
|
VALUE mkr_cNode;
|
|
6
9
|
VALUE mkr_cDocument;
|
|
7
10
|
VALUE mkr_cElement;
|
|
8
|
-
VALUE
|
|
11
|
+
VALUE mkr_cAttr;
|
|
9
12
|
VALUE mkr_cText;
|
|
10
13
|
VALUE mkr_cComment;
|
|
11
|
-
VALUE
|
|
14
|
+
VALUE mkr_cCDATASection;
|
|
12
15
|
VALUE mkr_cProcessingInstruction;
|
|
13
16
|
VALUE mkr_cDocumentType;
|
|
14
17
|
VALUE mkr_cDocumentFragment;
|
|
18
|
+
VALUE mkr_mHTML;
|
|
19
|
+
VALUE mkr_mHtmlNodeMethods;
|
|
20
|
+
VALUE mkr_cHtmlNode;
|
|
21
|
+
VALUE mkr_cHtmlDocument;
|
|
22
|
+
VALUE mkr_cHtmlElement;
|
|
23
|
+
VALUE mkr_cHtmlAttr;
|
|
24
|
+
VALUE mkr_cHtmlText;
|
|
25
|
+
VALUE mkr_cHtmlComment;
|
|
26
|
+
VALUE mkr_cHtmlCDATASection;
|
|
27
|
+
VALUE mkr_cHtmlProcessingInstruction;
|
|
28
|
+
VALUE mkr_cHtmlDocumentType;
|
|
29
|
+
VALUE mkr_cHtmlDocumentFragment;
|
|
30
|
+
VALUE mkr_mXmlNodeMethods;
|
|
31
|
+
VALUE mkr_cXmlNode;
|
|
32
|
+
VALUE mkr_cXmlDocument;
|
|
33
|
+
VALUE mkr_cXmlElement;
|
|
34
|
+
VALUE mkr_cXmlAttr;
|
|
35
|
+
VALUE mkr_cXmlText;
|
|
36
|
+
VALUE mkr_cXmlComment;
|
|
37
|
+
VALUE mkr_cXmlCDATASection;
|
|
38
|
+
VALUE mkr_cXmlProcessingInstruction;
|
|
39
|
+
VALUE mkr_cXmlDocumentType;
|
|
40
|
+
VALUE mkr_cXmlDocumentFragment;
|
|
15
41
|
VALUE mkr_cNodeSet;
|
|
16
42
|
VALUE mkr_cXPathContext;
|
|
17
43
|
VALUE mkr_mXPath;
|
|
18
44
|
VALUE mkr_mCSS;
|
|
45
|
+
VALUE mkr_mXML;
|
|
19
46
|
VALUE mkr_eError;
|
|
20
47
|
VALUE mkr_eXPathSyntaxError;
|
|
21
48
|
VALUE mkr_eXPathLimitExceeded;
|
|
22
49
|
VALUE mkr_eCSSSyntaxError;
|
|
50
|
+
VALUE mkr_eXmlSyntaxError;
|
|
51
|
+
VALUE mkr_eXmlLimitExceeded;
|
|
23
52
|
|
|
24
53
|
/* Makiri.__c_selftest -> true, or raises if the safe-core primitives
|
|
25
54
|
* (mkr_core.c) fail their internal edge-case checks. Test hook only. */
|
|
@@ -31,9 +60,78 @@ mkr_c_selftest(VALUE self)
|
|
|
31
60
|
if (rc != 0) {
|
|
32
61
|
rb_raise(mkr_eError, "mkr_core_selftest failed at check %d", rc);
|
|
33
62
|
}
|
|
63
|
+
int xc = mkr_xml_node_selftest();
|
|
64
|
+
if (xc != 0) {
|
|
65
|
+
rb_raise(mkr_eError, "mkr_xml_node_selftest failed at check %d", xc);
|
|
66
|
+
}
|
|
67
|
+
int pc = mkr_xml_parse_selftest();
|
|
68
|
+
if (pc != 0) {
|
|
69
|
+
rb_raise(mkr_eError, "mkr_xml_parse_selftest failed at check %d", pc);
|
|
70
|
+
}
|
|
71
|
+
int qc = mkr_xml_xpath_selftest();
|
|
72
|
+
if (qc != 0) {
|
|
73
|
+
rb_raise(mkr_eError, "mkr_xml_xpath_selftest failed at check %d", qc);
|
|
74
|
+
}
|
|
75
|
+
int mc = mkr_xml_mutate_selftest();
|
|
76
|
+
if (mc != 0) {
|
|
77
|
+
rb_raise(mkr_eError, "mkr_xml_mutate_selftest failed at check %d", mc);
|
|
78
|
+
}
|
|
34
79
|
return Qtrue;
|
|
35
80
|
}
|
|
36
81
|
|
|
82
|
+
/* Makiri.__alloc_inject(n) / __alloc_inject_calls / __alloc_inject? - the OOM
|
|
83
|
+
* sweep harness's controls (script/check_alloc_failures.rb, `rake oom`): arm
|
|
84
|
+
* "the nth core allocation fails once", and read how many core allocations a
|
|
85
|
+
* workload attempted. Compiled to a real hook only under MKR_ALLOC_INJECT
|
|
86
|
+
* (extconf: MAKIRI_ALLOC_INJECT=1); in a normal build __alloc_inject? is
|
|
87
|
+
* false and the others raise, so the harness fails loudly on the wrong build
|
|
88
|
+
* instead of sweeping nothing. Test hooks only. */
|
|
89
|
+
static VALUE
|
|
90
|
+
mkr_s_alloc_inject_p(VALUE self)
|
|
91
|
+
{
|
|
92
|
+
(void)self;
|
|
93
|
+
#ifdef MKR_ALLOC_INJECT
|
|
94
|
+
return Qtrue;
|
|
95
|
+
#else
|
|
96
|
+
return Qfalse;
|
|
97
|
+
#endif
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
static VALUE
|
|
101
|
+
mkr_s_alloc_inject(VALUE self, VALUE nth)
|
|
102
|
+
{
|
|
103
|
+
(void)self;
|
|
104
|
+
#ifdef MKR_ALLOC_INJECT
|
|
105
|
+
mkr_alloc_inject_arm((long long)NUM2LL(nth));
|
|
106
|
+
return Qnil;
|
|
107
|
+
#else
|
|
108
|
+
(void)nth;
|
|
109
|
+
rb_raise(rb_eNotImpError, "rebuild with MAKIRI_ALLOC_INJECT=1 (rake oom does this)");
|
|
110
|
+
#endif
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static VALUE
|
|
114
|
+
mkr_s_alloc_inject_calls(VALUE self)
|
|
115
|
+
{
|
|
116
|
+
(void)self;
|
|
117
|
+
#ifdef MKR_ALLOC_INJECT
|
|
118
|
+
return ULL2NUM(mkr_alloc_inject_calls());
|
|
119
|
+
#else
|
|
120
|
+
rb_raise(rb_eNotImpError, "rebuild with MAKIRI_ALLOC_INJECT=1 (rake oom does this)");
|
|
121
|
+
#endif
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Makiri::XML.__decode(str) -> validated, UTF-8-tagged String, or raises
|
|
125
|
+
* Makiri::XML::SyntaxError. Internal test hook exercising the strict input
|
|
126
|
+
* decode (§2.1) on its own, until the full Makiri::XML(...) parse pipeline
|
|
127
|
+
* (tokenizer + tree builder) lands and subsumes it. */
|
|
128
|
+
static VALUE
|
|
129
|
+
mkr_xml_s_decode(VALUE self, VALUE str)
|
|
130
|
+
{
|
|
131
|
+
(void)self;
|
|
132
|
+
return mkr_xml_decode_input(rb_String(str), 0); /* decode-only: no arena, no budget */
|
|
133
|
+
}
|
|
134
|
+
|
|
37
135
|
RUBY_FUNC_EXPORTED void
|
|
38
136
|
Init_makiri(void)
|
|
39
137
|
{
|
|
@@ -41,10 +139,10 @@ Init_makiri(void)
|
|
|
41
139
|
mkr_cNode = rb_define_class_under(mkr_mMakiri, "Node", rb_cObject);
|
|
42
140
|
mkr_cDocument = rb_define_class_under(mkr_mMakiri, "Document", mkr_cNode);
|
|
43
141
|
mkr_cElement = rb_define_class_under(mkr_mMakiri, "Element", mkr_cNode);
|
|
44
|
-
|
|
142
|
+
mkr_cAttr = rb_define_class_under(mkr_mMakiri, "Attr", mkr_cNode);
|
|
45
143
|
mkr_cText = rb_define_class_under(mkr_mMakiri, "Text", mkr_cNode);
|
|
46
144
|
mkr_cComment = rb_define_class_under(mkr_mMakiri, "Comment", mkr_cNode);
|
|
47
|
-
|
|
145
|
+
mkr_cCDATASection = rb_define_class_under(mkr_mMakiri, "CDATASection", mkr_cNode);
|
|
48
146
|
mkr_cProcessingInstruction =
|
|
49
147
|
rb_define_class_under(mkr_mMakiri, "ProcessingInstruction", mkr_cNode);
|
|
50
148
|
mkr_cDocumentType =
|
|
@@ -56,11 +154,85 @@ Init_makiri(void)
|
|
|
56
154
|
|
|
57
155
|
mkr_mXPath = rb_define_module_under(mkr_mMakiri, "XPath");
|
|
58
156
|
mkr_mCSS = rb_define_module_under(mkr_mMakiri, "CSS");
|
|
157
|
+
mkr_mXML = rb_define_module_under(mkr_mMakiri, "XML");
|
|
158
|
+
|
|
159
|
+
/* Per-kind hierarchy (§12): the classes above are abstract bases; concrete
|
|
160
|
+
* HTML nodes are Makiri::HTML::* leaves. The reader/query methods (which read
|
|
161
|
+
* lxb_dom) live on the mkr_mHtmlNodeMethods behavior module and are included into
|
|
162
|
+
* every leaf, so XML nodes (added in step 2) never inherit an HTML reader. */
|
|
163
|
+
mkr_mHTML = rb_define_module_under(mkr_mMakiri, "HTML");
|
|
164
|
+
mkr_mHtmlNodeMethods = rb_define_module_under(mkr_mHTML, "NodeMethods");
|
|
165
|
+
/* The concrete generic HTML node - the wrap fallback for an uncommon DOM node
|
|
166
|
+
* type with no more specific leaf. Carries the readers but is not an Element. */
|
|
167
|
+
mkr_cHtmlNode = rb_define_class_under(mkr_mHTML, "Node", mkr_cNode);
|
|
168
|
+
mkr_cHtmlDocument = rb_define_class_under(mkr_mHTML, "Document", mkr_cDocument);
|
|
169
|
+
mkr_cHtmlElement = rb_define_class_under(mkr_mHTML, "Element", mkr_cElement);
|
|
170
|
+
mkr_cHtmlAttr = rb_define_class_under(mkr_mHTML, "Attr", mkr_cAttr);
|
|
171
|
+
mkr_cHtmlText = rb_define_class_under(mkr_mHTML, "Text", mkr_cText);
|
|
172
|
+
mkr_cHtmlComment = rb_define_class_under(mkr_mHTML, "Comment", mkr_cComment);
|
|
173
|
+
mkr_cHtmlCDATASection = rb_define_class_under(mkr_mHTML, "CDATASection", mkr_cCDATASection);
|
|
174
|
+
mkr_cHtmlProcessingInstruction =
|
|
175
|
+
rb_define_class_under(mkr_mHTML, "ProcessingInstruction", mkr_cProcessingInstruction);
|
|
176
|
+
mkr_cHtmlDocumentType =
|
|
177
|
+
rb_define_class_under(mkr_mHTML, "DocumentType", mkr_cDocumentType);
|
|
178
|
+
mkr_cHtmlDocumentFragment =
|
|
179
|
+
rb_define_class_under(mkr_mHTML, "DocumentFragment", mkr_cDocumentFragment);
|
|
180
|
+
|
|
181
|
+
/* Every HTML leaf - the generic node and the typed leaves - gets the shared
|
|
182
|
+
* lxb_dom reader/query methods. */
|
|
183
|
+
VALUE html_leaves[] = {
|
|
184
|
+
mkr_cHtmlNode, mkr_cHtmlDocument, mkr_cHtmlElement, mkr_cHtmlAttr,
|
|
185
|
+
mkr_cHtmlText, mkr_cHtmlComment, mkr_cHtmlCDATASection,
|
|
186
|
+
mkr_cHtmlProcessingInstruction, mkr_cHtmlDocumentType,
|
|
187
|
+
mkr_cHtmlDocumentFragment,
|
|
188
|
+
};
|
|
189
|
+
for (size_t i = 0; i < sizeof(html_leaves) / sizeof(html_leaves[0]); i++) {
|
|
190
|
+
rb_include_module(html_leaves[i], mkr_mHtmlNodeMethods);
|
|
191
|
+
rb_undef_alloc_func(html_leaves[i]); /* created only from C, never .new */
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* Makiri::XML leaves (the custom-node counterparts). XML::Document itself is
|
|
195
|
+
* defined in mkr_init_xml (it backs a mkr_parsed_t, not a node). */
|
|
196
|
+
mkr_mXmlNodeMethods = rb_define_module_under(mkr_mXML, "NodeMethods");
|
|
197
|
+
mkr_cXmlNode = rb_define_class_under(mkr_mXML, "Node", mkr_cNode);
|
|
198
|
+
mkr_cXmlElement = rb_define_class_under(mkr_mXML, "Element", mkr_cElement);
|
|
199
|
+
mkr_cXmlAttr = rb_define_class_under(mkr_mXML, "Attr", mkr_cAttr);
|
|
200
|
+
mkr_cXmlText = rb_define_class_under(mkr_mXML, "Text", mkr_cText);
|
|
201
|
+
mkr_cXmlComment = rb_define_class_under(mkr_mXML, "Comment", mkr_cComment);
|
|
202
|
+
mkr_cXmlCDATASection = rb_define_class_under(mkr_mXML, "CDATASection", mkr_cCDATASection);
|
|
203
|
+
mkr_cXmlProcessingInstruction =
|
|
204
|
+
rb_define_class_under(mkr_mXML, "ProcessingInstruction", mkr_cProcessingInstruction);
|
|
205
|
+
/* Makiri::XML::DocumentType - the off-tree DOCTYPE metadata node
|
|
206
|
+
* (doc->doctype), reachable only via Document#internal_subset (XPath has no
|
|
207
|
+
* doctype node, as in Nokogiri/libxml2). The canonical name is the WHATWG DOM
|
|
208
|
+
* interface name DocumentType (with a Makiri::XML::DTD alias for Nokogiri
|
|
209
|
+
* compatibility, defined in Ruby); it descends from the shared
|
|
210
|
+
* Makiri::DocumentType base (like HTML::DocumentType) - not Makiri::XML::Node -
|
|
211
|
+
* so is_a?(Makiri::DocumentType) holds for both HTML and XML doctypes. It is
|
|
212
|
+
* still an XML node leaf (the reader/identity methods come from the included
|
|
213
|
+
* mkr_mXmlNodeMethods below), with its own name/external_id/system_id readers. */
|
|
214
|
+
mkr_cXmlDocumentType = rb_define_class_under(mkr_mXML, "DocumentType", mkr_cDocumentType);
|
|
215
|
+
/* Makiri::XML::DocumentFragment - a detached group of sibling nodes built by
|
|
216
|
+
* XML::DocumentFragment.parse / XML::Document#fragment (an XML node leaf, like
|
|
217
|
+
* its HTML counterpart). */
|
|
218
|
+
mkr_cXmlDocumentFragment =
|
|
219
|
+
rb_define_class_under(mkr_mXML, "DocumentFragment", mkr_cDocumentFragment);
|
|
220
|
+
VALUE xml_leaves[] = {
|
|
221
|
+
mkr_cXmlNode, mkr_cXmlElement, mkr_cXmlAttr, mkr_cXmlText,
|
|
222
|
+
mkr_cXmlComment, mkr_cXmlCDATASection, mkr_cXmlProcessingInstruction, mkr_cXmlDocumentType,
|
|
223
|
+
mkr_cXmlDocumentFragment,
|
|
224
|
+
};
|
|
225
|
+
for (size_t i = 0; i < sizeof(xml_leaves) / sizeof(xml_leaves[0]); i++) {
|
|
226
|
+
rb_include_module(xml_leaves[i], mkr_mXmlNodeMethods);
|
|
227
|
+
rb_undef_alloc_func(xml_leaves[i]);
|
|
228
|
+
}
|
|
59
229
|
|
|
60
230
|
mkr_eError = rb_define_class_under(mkr_mMakiri, "Error", rb_eStandardError);
|
|
61
231
|
mkr_eXPathSyntaxError = rb_define_class_under(mkr_mXPath, "SyntaxError", mkr_eError);
|
|
62
232
|
mkr_eXPathLimitExceeded = rb_define_class_under(mkr_mXPath, "LimitExceeded", mkr_eXPathSyntaxError);
|
|
63
233
|
mkr_eCSSSyntaxError = rb_define_class_under(mkr_mCSS, "SyntaxError", mkr_eError);
|
|
234
|
+
mkr_eXmlSyntaxError = rb_define_class_under(mkr_mXML, "SyntaxError", mkr_eError);
|
|
235
|
+
mkr_eXmlLimitExceeded = rb_define_class_under(mkr_mXML, "LimitExceeded", mkr_eError);
|
|
64
236
|
|
|
65
237
|
/* Instances of these classes are created only from C via
|
|
66
238
|
* TypedData_Make_Struct (document parse / tree traversal / query
|
|
@@ -70,10 +242,10 @@ Init_makiri(void)
|
|
|
70
242
|
rb_undef_alloc_func(mkr_cNode);
|
|
71
243
|
rb_undef_alloc_func(mkr_cDocument);
|
|
72
244
|
rb_undef_alloc_func(mkr_cElement);
|
|
73
|
-
rb_undef_alloc_func(
|
|
245
|
+
rb_undef_alloc_func(mkr_cAttr);
|
|
74
246
|
rb_undef_alloc_func(mkr_cText);
|
|
75
247
|
rb_undef_alloc_func(mkr_cComment);
|
|
76
|
-
rb_undef_alloc_func(
|
|
248
|
+
rb_undef_alloc_func(mkr_cCDATASection);
|
|
77
249
|
rb_undef_alloc_func(mkr_cProcessingInstruction);
|
|
78
250
|
rb_undef_alloc_func(mkr_cDocumentType);
|
|
79
251
|
rb_undef_alloc_func(mkr_cDocumentFragment);
|
|
@@ -89,6 +261,12 @@ Init_makiri(void)
|
|
|
89
261
|
mkr_init_css();
|
|
90
262
|
mkr_init_serialize();
|
|
91
263
|
mkr_init_mutate();
|
|
264
|
+
mkr_init_xml();
|
|
265
|
+
mkr_init_xml_node();
|
|
92
266
|
|
|
93
267
|
rb_define_singleton_method(mkr_mMakiri, "__c_selftest", mkr_c_selftest, 0);
|
|
268
|
+
rb_define_singleton_method(mkr_mMakiri, "__alloc_inject?", mkr_s_alloc_inject_p, 0);
|
|
269
|
+
rb_define_singleton_method(mkr_mMakiri, "__alloc_inject", mkr_s_alloc_inject, 1);
|
|
270
|
+
rb_define_singleton_method(mkr_mMakiri, "__alloc_inject_calls", mkr_s_alloc_inject_calls, 0);
|
|
271
|
+
rb_define_singleton_method(mkr_mXML, "__decode", mkr_xml_s_decode, 1);
|
|
94
272
|
}
|
data/ext/makiri/makiri.h
CHANGED
|
@@ -15,24 +15,64 @@ extern "C" {
|
|
|
15
15
|
|
|
16
16
|
/* Ruby module + class refs, populated by Init_makiri. */
|
|
17
17
|
extern VALUE mkr_mMakiri;
|
|
18
|
+
/* Abstract bases (§12): Node and the per-type bases. Concrete nodes are the
|
|
19
|
+
* per-kind leaves below; `is_a?(Makiri::Element)` etc. stays true for both. */
|
|
18
20
|
extern VALUE mkr_cNode;
|
|
19
21
|
extern VALUE mkr_cDocument;
|
|
20
22
|
extern VALUE mkr_cElement;
|
|
21
|
-
extern VALUE
|
|
23
|
+
extern VALUE mkr_cAttr;
|
|
22
24
|
extern VALUE mkr_cText;
|
|
23
25
|
extern VALUE mkr_cComment;
|
|
24
|
-
extern VALUE
|
|
26
|
+
extern VALUE mkr_cCDATASection;
|
|
25
27
|
extern VALUE mkr_cProcessingInstruction;
|
|
26
28
|
extern VALUE mkr_cDocumentType;
|
|
27
29
|
extern VALUE mkr_cDocumentFragment;
|
|
30
|
+
|
|
31
|
+
/* Makiri::HTML module + leaves. mkr_mHtmlNodeMethods (Makiri::HTML::NodeMethods)
|
|
32
|
+
* is a behavior module holding the lxb_dom-backed reader/query methods, included
|
|
33
|
+
* into every HTML leaf (so an XML node never inherits an HTML reader).
|
|
34
|
+
* mkr_cHtmlNode (Makiri::HTML::Node) is the concrete generic HTML node - the
|
|
35
|
+
* wrap fallback for an uncommon DOM node type that has no more specific leaf
|
|
36
|
+
* (it carries the readers but is NOT classified as an Element). */
|
|
37
|
+
extern VALUE mkr_mHTML;
|
|
38
|
+
extern VALUE mkr_mHtmlNodeMethods;
|
|
39
|
+
extern VALUE mkr_cHtmlNode;
|
|
40
|
+
extern VALUE mkr_cHtmlDocument;
|
|
41
|
+
extern VALUE mkr_cHtmlElement;
|
|
42
|
+
extern VALUE mkr_cHtmlAttr;
|
|
43
|
+
extern VALUE mkr_cHtmlText;
|
|
44
|
+
extern VALUE mkr_cHtmlComment;
|
|
45
|
+
extern VALUE mkr_cHtmlCDATASection;
|
|
46
|
+
extern VALUE mkr_cHtmlProcessingInstruction;
|
|
47
|
+
extern VALUE mkr_cHtmlDocumentType;
|
|
48
|
+
extern VALUE mkr_cHtmlDocumentFragment;
|
|
49
|
+
|
|
50
|
+
/* Makiri::XML leaves + the XML reader behavior module (the custom-node
|
|
51
|
+
* counterpart of mkr_mHtmlNodeMethods). */
|
|
52
|
+
extern VALUE mkr_mXmlNodeMethods;
|
|
53
|
+
extern VALUE mkr_cXmlNode;
|
|
54
|
+
extern VALUE mkr_cXmlDocument;
|
|
55
|
+
extern VALUE mkr_cXmlElement;
|
|
56
|
+
extern VALUE mkr_cXmlAttr;
|
|
57
|
+
extern VALUE mkr_cXmlText;
|
|
58
|
+
extern VALUE mkr_cXmlComment;
|
|
59
|
+
extern VALUE mkr_cXmlCDATASection;
|
|
60
|
+
extern VALUE mkr_cXmlProcessingInstruction;
|
|
61
|
+
extern VALUE mkr_cXmlDocumentType;
|
|
62
|
+
extern VALUE mkr_cXmlDocumentFragment;
|
|
63
|
+
|
|
64
|
+
void mkr_init_xml_node(void);
|
|
28
65
|
extern VALUE mkr_cNodeSet;
|
|
29
66
|
extern VALUE mkr_cXPathContext;
|
|
30
67
|
extern VALUE mkr_mXPath;
|
|
31
68
|
extern VALUE mkr_mCSS;
|
|
69
|
+
extern VALUE mkr_mXML;
|
|
32
70
|
extern VALUE mkr_eError;
|
|
33
71
|
extern VALUE mkr_eXPathSyntaxError;
|
|
34
72
|
extern VALUE mkr_eXPathLimitExceeded;
|
|
35
73
|
extern VALUE mkr_eCSSSyntaxError;
|
|
74
|
+
extern VALUE mkr_eXmlSyntaxError;
|
|
75
|
+
extern VALUE mkr_eXmlLimitExceeded;
|
|
36
76
|
|
|
37
77
|
/* Forward-declared sub-init entry points. Each sub-component owns its
|
|
38
78
|
* own Init_* and registers methods on the class refs above. */
|
|
@@ -43,6 +83,7 @@ void mkr_init_xpath(void);
|
|
|
43
83
|
void mkr_init_css(void);
|
|
44
84
|
void mkr_init_serialize(void);
|
|
45
85
|
void mkr_init_mutate(void);
|
|
86
|
+
void mkr_init_xml(void);
|
|
46
87
|
|
|
47
88
|
#ifdef __cplusplus
|
|
48
89
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/* mkr_xml.h - public boundary of the native XML reader (Phase 1, read-only).
|
|
2
|
+
*
|
|
3
|
+
* Ruby-free. The Ruby surface (Makiri::XML::Document.parse / Makiri::XML(...)) lives in
|
|
4
|
+
* glue/ruby_xml.c, which decodes input (bridge/mkr_xml_decode_input), releases
|
|
5
|
+
* the GVL, and calls mkr_xml_parse here. See docs/xml_parser_plan.ja.md.
|
|
6
|
+
*/
|
|
7
|
+
#ifndef MKR_XML_H
|
|
8
|
+
#define MKR_XML_H
|
|
9
|
+
|
|
10
|
+
#include <stddef.h>
|
|
11
|
+
#include "mkr_xml_node.h"
|
|
12
|
+
|
|
13
|
+
/* Fail-closed status codes (mapped to Ruby exceptions at the glue boundary). */
|
|
14
|
+
typedef enum {
|
|
15
|
+
MKR_XML_OK = 0,
|
|
16
|
+
MKR_XML_ERR_SYNTAX, /* well-formedness violation -> Makiri::XML::SyntaxError */
|
|
17
|
+
MKR_XML_ERR_LIMIT, /* budget exceeded -> Makiri::XML::LimitExceeded */
|
|
18
|
+
MKR_XML_ERR_OOM, /* allocation failure -> Makiri::Error */
|
|
19
|
+
MKR_XML_ERR_INTERNAL, /* impossible-state guard tripped (programming error) -> Makiri::Error */
|
|
20
|
+
MKR_XML_ERR_VERSION /* well-formed but a version != 1.0 (XML 1.1) -> Makiri::XML::SyntaxError */
|
|
21
|
+
} mkr_xml_status_t;
|
|
22
|
+
|
|
23
|
+
/* Per-document budgets (§4). Enforced in the arena / tree builder; exceeding any
|
|
24
|
+
* of them is fail-closed (no partial/truncated document - raise instead). */
|
|
25
|
+
#define MKR_XML_MAX_DEPTH 1024u /* element nesting (no stack DoS) */
|
|
26
|
+
#define MKR_XML_MAX_NODES (10u * 1000u * 1000u) /* total DOM nodes - a SECONDARY guard; the
|
|
27
|
+
* byte budget below is the primary memory
|
|
28
|
+
* ceiling (at ~128 B/node it caps node count
|
|
29
|
+
* well under 10M, so it trips first). */
|
|
30
|
+
#define MKR_XML_MAX_ATTRS 4096u /* attributes per element */
|
|
31
|
+
#define MKR_XML_MAX_NS 4096u /* namespace bindings IN SCOPE at once - the
|
|
32
|
+
* scope-stack depth, popped on element close.
|
|
33
|
+
* Bounds concurrent bindings + the binds array,
|
|
34
|
+
* NOT document-wide distinct declarations (those
|
|
35
|
+
* are each an attribute, bounded by the byte /
|
|
36
|
+
* attr budgets). */
|
|
37
|
+
#ifndef MKR_XML_MAX_BYTES
|
|
38
|
+
/* Arena payload cap: the single arena choke (arena_alloc) counts BOTH node
|
|
39
|
+
* structs and copied name/value bytes against this, so it is the master memory
|
|
40
|
+
* ceiling - it subsumes the node-count limit and bounds tiny-element
|
|
41
|
+
* amplification (a `<a/>` flood is ~23x input→arena, measured) to ~11 MB of
|
|
42
|
+
* hostile input. Enforced before each allocation, and the parse entry rejects
|
|
43
|
+
* any input longer than this up front (so the line-ending scratch is bounded
|
|
44
|
+
* too). 256 MiB comfortably fits every standard document (a 50 MB sitemap is
|
|
45
|
+
* ~82 MB of arena; RSS/Atom/SOAP far less) and only rejects documents past
|
|
46
|
+
* ~2M elements. A caller that needs more passes max_bytes to mkr_xml_parse_ex
|
|
47
|
+
* (per-document override); -DMKR_XML_MAX_BYTES changes the default. */
|
|
48
|
+
#define MKR_XML_MAX_BYTES ((size_t)256u * 1024u * 1024u) /* 256 MiB */
|
|
49
|
+
#endif
|
|
50
|
+
|
|
51
|
+
/* Per-parse budget overrides (the runtime counterpart of the MKR_XML_MAX_*
|
|
52
|
+
* compile-time defaults). A 0 field means "use the compile-time default", so a
|
|
53
|
+
* zeroed struct (or a NULL pointer to mkr_xml_parse_ex) reproduces mkr_xml_parse
|
|
54
|
+
* exactly. Only max_bytes is honoured today; the other budgets (max_nodes,
|
|
55
|
+
* max_depth, max_attrs, max_ns) are intended to join this struct as they become
|
|
56
|
+
* runtime-configurable, without changing the entry-point signature. */
|
|
57
|
+
typedef struct {
|
|
58
|
+
size_t max_bytes; /* arena payload cap; 0 = MKR_XML_MAX_BYTES */
|
|
59
|
+
} mkr_xml_limits_t;
|
|
60
|
+
|
|
61
|
+
/* Parse +len+ bytes of already-decoded, validated UTF-8 (§2.1 guarantees the
|
|
62
|
+
* input is valid UTF-8, XML-Char-only, NUL-free) into a document. Ruby-free, so
|
|
63
|
+
* the caller runs it with the GVL released. Returns NULL and sets *status on
|
|
64
|
+
* failure; otherwise an owned mkr_xml_doc_t the caller must hand to GC
|
|
65
|
+
* (mkr_xml_doc_destroy on free).
|
|
66
|
+
*
|
|
67
|
+
* mkr_xml_parse uses the compile-time default budgets; mkr_xml_parse_ex applies
|
|
68
|
+
* the non-zero fields of +limits+ over them first (a NULL +limits+ is identical
|
|
69
|
+
* to mkr_xml_parse). */
|
|
70
|
+
mkr_xml_doc_t *mkr_xml_parse(const char *src, size_t len, mkr_xml_status_t *status);
|
|
71
|
+
mkr_xml_doc_t *mkr_xml_parse_ex(const char *src, size_t len,
|
|
72
|
+
const mkr_xml_limits_t *limits, mkr_xml_status_t *status);
|
|
73
|
+
|
|
74
|
+
/* Parse +src+ as a FRAGMENT (0+ top-level nodes: char data, multiple elements,
|
|
75
|
+
* comments, PIs, CDATA - no XML declaration, DOCTYPE, or single-root rule) into a
|
|
76
|
+
* fresh DOCUMENT_FRAGMENT node in +doc+'s arena, returned on success. When
|
|
77
|
+
* +inherit_doc_ns+ is set, +doc+'s root in-scope namespace declarations seed the
|
|
78
|
+
* scope so a prefixed/default-namespaced name resolves against the document
|
|
79
|
+
* (Document#fragment); otherwise the fragment is self-contained
|
|
80
|
+
* (DocumentFragment.parse, typically into a fresh doc). Bytes count against
|
|
81
|
+
* +doc+'s budget. NULL + *status on error; the partial fragment is left detached
|
|
82
|
+
* in the arena (never freed). The input must already be validated UTF-8. */
|
|
83
|
+
mkr_xml_node_t *mkr_xml_parse_fragment(mkr_xml_doc_t *doc, const char *src, size_t len,
|
|
84
|
+
int inherit_doc_ns, mkr_xml_status_t *status);
|
|
85
|
+
|
|
86
|
+
/* Structural self-test of the tokenizer + tree builder (run from
|
|
87
|
+
* Makiri.__c_selftest). Returns 0 on success or the 1-based failing check. */
|
|
88
|
+
int mkr_xml_parse_selftest(void);
|
|
89
|
+
|
|
90
|
+
/* Self-test of the monomorphized XML XPath engine instance (xpath/
|
|
91
|
+
* mkr_xpath_xml_selftest.c): runs queries through the _xml engine over a small
|
|
92
|
+
* document. Returns 0 or the 1-based failing check. */
|
|
93
|
+
int mkr_xml_xpath_selftest(void);
|
|
94
|
+
|
|
95
|
+
/* --- character data: XML Char + entity/char-reference expansion (§9.1/§9.2) --- */
|
|
96
|
+
int mkr_xml_is_char(uint32_t c);
|
|
97
|
+
|
|
98
|
+
/* Validate that [src, src+len) is entirely XML 1.0 Char with NO reference
|
|
99
|
+
* recognition (comment/CDATA/PI content, where '&'/'<' are literal). 0 / -1. */
|
|
100
|
+
int mkr_xml_validate_chars(const char *src, uint32_t len);
|
|
101
|
+
|
|
102
|
+
/* XML 1.0 §2.3 NameStartChar / NameChar (§9.2b). One-codepoint decoding is the
|
|
103
|
+
* core mkr_utf8_decode1 / mkr_utf8_decode1_span (strict, bounds-checked). */
|
|
104
|
+
int mkr_xml_is_name_start(uint32_t c);
|
|
105
|
+
int mkr_xml_is_name_char(uint32_t c);
|
|
106
|
+
|
|
107
|
+
/* Expansion context. ATTR adds XML 1.0 §3.3.3 attribute-value normalization:
|
|
108
|
+
* a *literal* white-space byte (TAB/LF/CR) becomes a space, while a white-space
|
|
109
|
+
* character produced by a numeric reference (	/ / ) is preserved. TEXT
|
|
110
|
+
* copies content verbatim (no whitespace folding). */
|
|
111
|
+
typedef enum {
|
|
112
|
+
MKR_XML_EXPAND_TEXT = 0,
|
|
113
|
+
MKR_XML_EXPAND_ATTR = 1
|
|
114
|
+
} mkr_xml_expand_mode_t;
|
|
115
|
+
|
|
116
|
+
/* Expand the 5 predefined entities + numeric char refs in [src, src+len),
|
|
117
|
+
* validating XML Char (and, in ATTR mode, normalizing literal whitespace); writes
|
|
118
|
+
* the (never-longer) result into the arena. Returns the arena slice and sets
|
|
119
|
+
* *out_len, or NULL on undefined entity / bad reference / non-XML-Char (sets
|
|
120
|
+
* *status) or arena OOM/LIMIT. */
|
|
121
|
+
const char *mkr_xml_expand(mkr_xml_doc_t *doc, const char *src, uint32_t len,
|
|
122
|
+
mkr_xml_expand_mode_t mode,
|
|
123
|
+
uint32_t *out_len, mkr_xml_status_t *status);
|
|
124
|
+
|
|
125
|
+
#endif /* MKR_XML_H */
|