makiri 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/conformance.yml +22 -0
  3. data/.github/workflows/libfuzzer.yml +83 -0
  4. data/.github/workflows/release.yml +12 -7
  5. data/.github/workflows/security.yml +88 -3
  6. data/.github/workflows/valgrind.yml +135 -0
  7. data/CHANGELOG.md +152 -15
  8. data/README.md +183 -13
  9. data/Rakefile +294 -7
  10. data/ext/makiri/bridge/bridge.h +28 -0
  11. data/ext/makiri/bridge/ruby_string.c +282 -12
  12. data/ext/makiri/core/mkr_alloc.c +40 -3
  13. data/ext/makiri/core/mkr_alloc.h +28 -5
  14. data/ext/makiri/core/mkr_buf.c +47 -3
  15. data/ext/makiri/core/mkr_buf.h +112 -3
  16. data/ext/makiri/core/mkr_core.c +143 -0
  17. data/ext/makiri/core/mkr_core.h +11 -2
  18. data/ext/makiri/core/mkr_hash.h +1 -1
  19. data/ext/makiri/core/mkr_span.h +186 -0
  20. data/ext/makiri/core/mkr_text.h +8 -8
  21. data/ext/makiri/core/mkr_utf8.c +101 -0
  22. data/ext/makiri/core/mkr_utf8.h +88 -0
  23. data/ext/makiri/extconf.rb +123 -10
  24. data/ext/makiri/fuzz/Makefile +95 -0
  25. data/ext/makiri/fuzz/check_fuzzer.cc +4 -0
  26. data/ext/makiri/fuzz/xml_fuzz.c +24 -0
  27. data/ext/makiri/fuzz/xpath_fuzz.c +109 -0
  28. data/ext/makiri/glue/glue.h +55 -11
  29. data/ext/makiri/glue/ruby_doc.c +129 -59
  30. data/ext/makiri/glue/ruby_html_css.c +292 -0
  31. data/ext/makiri/glue/{ruby_mutate.c → ruby_html_mutate.c} +248 -52
  32. data/ext/makiri/glue/ruby_html_node.c +859 -0
  33. data/ext/makiri/glue/ruby_html_serialize.c +154 -0
  34. data/ext/makiri/glue/ruby_node.c +74 -729
  35. data/ext/makiri/glue/ruby_node_set.c +167 -32
  36. data/ext/makiri/glue/ruby_xml.c +602 -0
  37. data/ext/makiri/glue/ruby_xml_node.c +1373 -0
  38. data/ext/makiri/glue/ruby_xpath.c +63 -30
  39. data/ext/makiri/glue/ruby_xpath.h +19 -0
  40. data/ext/makiri/lexbor_compat/compat.h +42 -9
  41. data/ext/makiri/lexbor_compat/compat_internal.h +1 -1
  42. data/ext/makiri/lexbor_compat/dom_index.c +2 -2
  43. data/ext/makiri/lexbor_compat/post_parse.c +100 -10
  44. data/ext/makiri/lexbor_compat/source_loc.c +15 -13
  45. data/ext/makiri/lexbor_compat/text_index.c +14 -8
  46. data/ext/makiri/lexbor_compat/utf8_input.c +19 -33
  47. data/ext/makiri/makiri.c +184 -6
  48. data/ext/makiri/makiri.h +43 -2
  49. data/ext/makiri/xml/mkr_xml.h +125 -0
  50. data/ext/makiri/xml/mkr_xml_chars.c +195 -0
  51. data/ext/makiri/xml/mkr_xml_index.c +169 -0
  52. data/ext/makiri/xml/mkr_xml_index.h +48 -0
  53. data/ext/makiri/xml/mkr_xml_mutate.c +817 -0
  54. data/ext/makiri/xml/mkr_xml_mutate.h +139 -0
  55. data/ext/makiri/xml/mkr_xml_node.c +399 -0
  56. data/ext/makiri/xml/mkr_xml_node.h +184 -0
  57. data/ext/makiri/xml/mkr_xml_tree.c +1515 -0
  58. data/ext/makiri/xpath/mkr_css.c +1023 -0
  59. data/ext/makiri/xpath/mkr_css.h +65 -0
  60. data/ext/makiri/xpath/mkr_xpath.c +96 -32
  61. data/ext/makiri/xpath/mkr_xpath.h +109 -4
  62. data/ext/makiri/xpath/mkr_xpath_engine_html.c +17 -0
  63. data/ext/makiri/xpath/mkr_xpath_engine_xml.c +12 -0
  64. data/ext/makiri/xpath/{mkr_xpath_eval.c → mkr_xpath_eval_body.h} +551 -241
  65. data/ext/makiri/xpath/{mkr_xpath_funcs.c → mkr_xpath_funcs_body.h} +318 -276
  66. data/ext/makiri/xpath/mkr_xpath_internal.h +177 -206
  67. data/ext/makiri/xpath/mkr_xpath_lex.c +95 -125
  68. data/ext/makiri/xpath/mkr_xpath_node_access_html.h +138 -0
  69. data/ext/makiri/xpath/mkr_xpath_node_access_xml.h +145 -0
  70. data/ext/makiri/xpath/mkr_xpath_number.c +109 -0
  71. data/ext/makiri/xpath/mkr_xpath_parse.c +83 -94
  72. data/ext/makiri/xpath/mkr_xpath_prelude_html.h +30 -0
  73. data/ext/makiri/xpath/mkr_xpath_prelude_xml.h +28 -0
  74. data/ext/makiri/xpath/mkr_xpath_shared.c +609 -0
  75. data/ext/makiri/xpath/mkr_xpath_value_body.h +801 -0
  76. data/ext/makiri/xpath/mkr_xpath_xml_selftest.c +76 -0
  77. data/lib/makiri/{attribute.rb → attr.rb} +7 -3
  78. data/lib/makiri/cdata_section.rb +19 -0
  79. data/lib/makiri/comment.rb +10 -0
  80. data/lib/makiri/compat_aliases.rb +30 -0
  81. data/lib/makiri/document.rb +9 -73
  82. data/lib/makiri/document_fragment.rb +14 -9
  83. data/lib/makiri/element.rb +4 -4
  84. data/lib/makiri/html/document.rb +106 -0
  85. data/lib/makiri/html/node_methods.rb +19 -0
  86. data/lib/makiri/html.rb +12 -0
  87. data/lib/makiri/node.rb +58 -15
  88. data/lib/makiri/node_set.rb +8 -0
  89. data/lib/makiri/processing_instruction.rb +10 -0
  90. data/lib/makiri/text.rb +1 -1
  91. data/lib/makiri/version.rb +1 -1
  92. data/lib/makiri/xml/builder.rb +263 -0
  93. data/lib/makiri/xml/document.rb +24 -0
  94. data/lib/makiri/xml/node_methods.rb +84 -0
  95. data/lib/makiri/xml.rb +10 -0
  96. data/lib/makiri/xpath_context.rb +1 -1
  97. data/lib/makiri.rb +24 -5
  98. data/script/build_native_gem.rb +2 -2
  99. data/script/check_alloc_failures.rb +266 -0
  100. data/script/check_c_safety.rb +77 -2
  101. data/script/check_c_safety_allowlist.yml +102 -0
  102. data/script/check_leaks.rb +64 -0
  103. data/script/leaks_harness.rb +64 -0
  104. data/vendor/lexbor/CMakeLists.txt +6 -0
  105. data/vendor/lexbor/README.md +12 -0
  106. data/vendor/lexbor/config.cmake +1 -1
  107. data/vendor/lexbor/source/lexbor/core/base.h +1 -1
  108. data/vendor/lexbor/source/lexbor/core/config.cmake +9 -1
  109. data/vendor/lexbor/source/lexbor/css/selectors/pseudo_state.c +2 -3
  110. data/vendor/lexbor/source/lexbor/css/selectors/state.c +3 -0
  111. data/vendor/lexbor/source/lexbor/dom/interfaces/element.c +21 -0
  112. data/vendor/lexbor/source/lexbor/dom/interfaces/element.h +5 -0
  113. data/vendor/lexbor/source/lexbor/encoding/decode.c +33 -4
  114. data/vendor/lexbor/source/lexbor/html/base.h +1 -1
  115. data/vendor/lexbor/source/lexbor/html/interfaces/select_element.c +4 -0
  116. data/vendor/lexbor/source/lexbor/html/serialize.c +545 -41
  117. data/vendor/lexbor/source/lexbor/html/serialize.h +2 -1
  118. data/vendor/lexbor/source/lexbor/html/tokenizer.h +2 -2
  119. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/in_body.c +1 -1
  120. data/vendor/lexbor/source/lexbor/html/tree.c +6 -6
  121. data/vendor/lexbor/source/lexbor/selectors/selectors.c +12 -3
  122. data/vendor/lexbor/source/lexbor/url/base.h +1 -1
  123. data/vendor/lexbor/source/lexbor/url/url.c +5 -2
  124. data/vendor/lexbor/source/lexbor/url/url.h +9 -0
  125. data/vendor/lexbor/version +1 -1
  126. metadata +53 -9
  127. data/ext/makiri/glue/ruby_css.c +0 -185
  128. data/ext/makiri/glue/ruby_serialize.c +0 -92
  129. data/ext/makiri/xpath/mkr_xpath_value.c +0 -1286
  130. data/lib/makiri/cdata.rb +0 -6
@@ -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
- /* Is `src` (len bytes) well-formed UTF-8? Uses Lexbor's spec UTF-8 decoder
18
- * (which rejects bad continuation bytes, overlong forms, surrogates and
19
- * out-of-range code points) with NO replacement, so the first error makes it
20
- * return false. An incomplete trailing sequence (caught by decode_finish) is
21
- * also invalid. NUL bytes are valid UTF-8 here and are left for the HTML
22
- * tokenizer to handle per the spec (drop in text, U+FFFD in foreign content). */
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
- /* HTML parse input is not byte-capped; mkr_buf grows overflow-safe and fails
77
- * closed (NULL) on OOM. */
78
- mkr_buf_init(&buf, 0);
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 mkr_cAttribute;
11
+ VALUE mkr_cAttr;
9
12
  VALUE mkr_cText;
10
13
  VALUE mkr_cComment;
11
- VALUE mkr_cCData;
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
- mkr_cAttribute = rb_define_class_under(mkr_mMakiri, "Attribute", mkr_cNode);
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
- mkr_cCData = rb_define_class_under(mkr_mMakiri, "CData", mkr_cNode);
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(mkr_cAttribute);
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(mkr_cCData);
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 mkr_cAttribute;
23
+ extern VALUE mkr_cAttr;
22
24
  extern VALUE mkr_cText;
23
25
  extern VALUE mkr_cComment;
24
- extern VALUE mkr_cCData;
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 (&#9;/&#10;/&#13;) 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 */