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
@@ -0,0 +1,184 @@
1
+ /* mkr_xml_node.h - custom XML DOM node + secure-by-design append-only arena.
2
+ *
3
+ * XML does NOT use lxb_dom (§2.5): a custom node held in a per-document arena,
4
+ * case-preserved, with zero dependency on Lexbor or its internal APIs. The arena
5
+ * is append-only and freed whole with the document; nodes are never individually
6
+ * freed, so Ruby wrappers that alias a node stay valid - including across a
7
+ * mutation, which only detaches (mkr_xml_mutate.c is detach-never-destroy).
8
+ * Ruby-free. Memory-safety structure documented in docs/xml_parser_plan.ja.md
9
+ * §8.1/§8.2 and proven by mkr_xml_node_selftest().
10
+ */
11
+ #ifndef MKR_XML_NODE_H
12
+ #define MKR_XML_NODE_H
13
+
14
+ #include <stdint.h>
15
+ #include <stddef.h>
16
+ #include "../core/mkr_buf.h" /* mkr_spanbuf_t (the arena hand-fill writer) */
17
+
18
+ /* The XML representation's DOM node-type constants - the counterpart of Lexbor's
19
+ * LXB_DOM_NODE_TYPE_* for HTML. The numeric values mirror the DOM/Lexbor
20
+ * encoding exactly (asserted in mkr_xpath_node_access_xml.h) so the monomorphized
21
+ * XPath engine's neutral MKR_NTYPE_* bind to whichever representation it compiles
22
+ * for. The reader produces ELEMENT/ATTRIBUTE/TEXT/CDATA_SECTION/PI/COMMENT/
23
+ * DOCUMENT in the tree, plus a single off-tree DOCUMENT_TYPE (the DOCTYPE
24
+ * metadata, see doc->doctype); the remaining members exist only so the shared
25
+ * engine's node-type contract is complete for both instances. */
26
+ typedef enum {
27
+ MKR_XML_NODE_TYPE_ELEMENT = 1,
28
+ MKR_XML_NODE_TYPE_ATTRIBUTE = 2,
29
+ MKR_XML_NODE_TYPE_TEXT = 3,
30
+ MKR_XML_NODE_TYPE_CDATA_SECTION = 4,
31
+ MKR_XML_NODE_TYPE_ENTITY_REFERENCE = 5, /* never produced (no DTD) */
32
+ MKR_XML_NODE_TYPE_ENTITY = 6, /* never produced (no DTD) */
33
+ MKR_XML_NODE_TYPE_PI = 7,
34
+ MKR_XML_NODE_TYPE_COMMENT = 8,
35
+ MKR_XML_NODE_TYPE_DOCUMENT = 9,
36
+ MKR_XML_NODE_TYPE_DOCUMENT_TYPE = 10, /* the off-tree DOCTYPE metadata node */
37
+ MKR_XML_NODE_TYPE_DOCUMENT_FRAGMENT = 11, /* a detached group of sibling nodes */
38
+ MKR_XML_NODE_TYPE_NOTATION = 12 /* never produced (no DTD) */
39
+ } mkr_xml_node_type_t;
40
+
41
+ /* Field order is chosen for size + locality, not readability:
42
+ * 1. the hot navigation/type fields first (the engine reads type + the tree
43
+ * pointers in its tight walk), kept within the first cache line;
44
+ * 2. the name/value pointers grouped;
45
+ * 3. their lengths grouped at the end.
46
+ * Grouping the uint32 lengths lets adjacent ones pack into shared 8-byte slots
47
+ * instead of each wasting 4 bytes of padding before the next 8-aligned pointer
48
+ * - 144 -> 128 bytes/node (measured), ~160 MB saved at the 10M-node cap. The
49
+ * split is invisible to callers: a length is only ever read via its paired
50
+ * pointer (n->qname / n->qname_len), the field NAMES are unchanged, and the
51
+ * lengths stay uint32 (the deliberate per-slice 4 GiB cap; see the enum note).
52
+ * Names/values are arena-owned byte slices (case-preserved). */
53
+ typedef struct mkr_xml_node {
54
+ mkr_xml_node_type_t type; /* free: the next field is an 8-aligned pointer,
55
+ * so this sits in padding either way (node stays
56
+ * 128 B) - keep the real enum type for safety */
57
+ struct mkr_xml_node *parent, *first_child, *last_child, *prev, *next;
58
+ struct mkr_xml_node *attrs; /* element: head of attribute list (type=ATTRIBUTE) */
59
+ const char *qname; /* element/attr raw "prefix:local" (contiguous; local/prefix slice into it) */
60
+ const char *local;
61
+ const char *prefix;
62
+ const char *ns_uri; /* resolved namespace URI (original case) */
63
+ const char *value; /* text/cdata/comment/pi-data/attr value */
64
+ uint32_t qname_len, local_len, prefix_len, ns_uri_len, value_len;
65
+ uint32_t line, col; /* source position (element only; 0 = none) */
66
+ } mkr_xml_node_t;
67
+
68
+ /* A QName decomposed into its contiguous "prefix:local" parts (the same layout a
69
+ * built node carries): +qname+ is the whole name, +prefix+/+local+ slice into it
70
+ * (prefix points at +qname+; prefix_len 0 = unprefixed). The single value passed
71
+ * through the split/resolve/assign primitives below, so those never take the six
72
+ * fields as loose arguments. */
73
+ typedef struct {
74
+ const char *qname; uint32_t qname_len;
75
+ const char *prefix; uint32_t prefix_len; /* prefix_len 0 = unprefixed */
76
+ const char *local; uint32_t local_len;
77
+ } mkr_xml_qname_t;
78
+
79
+ typedef struct mkr_xml_arena_chunk mkr_xml_arena_chunk_t;
80
+
81
+ /* A document: its append-only arena, the root element (NULL until built), and
82
+ * the per-document budgets (§4). All nodes and name/value bytes live in the
83
+ * arena; mkr_xml_doc_destroy frees it whole. */
84
+ typedef struct mkr_xml_doc {
85
+ mkr_xml_arena_chunk_t *chunks; /* arena chunk list */
86
+ size_t arena_bytes; /* payload bytes cut (budget) */
87
+ size_t max_bytes; /* MKR_XML_MAX_BYTES */
88
+ size_t nodes, max_nodes; /* total node count (attributes included) */
89
+ int oom; /* mkr_xml_status_t once non-zero (sticky) */
90
+ mkr_xml_node_t *root; /* the root element */
91
+ mkr_xml_node_t *doc_node; /* the DOCUMENT node (parent of root); the XPath
92
+ * "/" root and what a Ruby Document wraps */
93
+ mkr_xml_node_t *doctype; /* a DOCUMENT_TYPE node holding the DOCTYPE's
94
+ * metadata, or NULL. Kept OFF the tree (it is
95
+ * not a child of doc_node, so XPath is
96
+ * unaffected) and exposed only via
97
+ * Document#internal_subset. The DTD itself is
98
+ * NOT parsed (no entities/elements). On this
99
+ * node: local/qname = name, prefix = public
100
+ * (external) id, value = system id; a 0-length
101
+ * prefix/value means that id is absent. */
102
+ void *name_index; /* lazily-built element-name index (mkr_xml_index.c),
103
+ * or NULL; dropped on every structural mutation. */
104
+ int has_encoding_decl; /* the parsed source's <?xml?> declaration carried an
105
+ * `encoding` pseudo-attribute. The serializer emits
106
+ * `encoding="UTF-8"` only when this is set (or an
107
+ * explicit encoding: is requested), so a built or
108
+ * declaration-less document round-trips to a bare
109
+ * `<?xml version="1.0"?>`, like Nokogiri. */
110
+ } mkr_xml_doc_t;
111
+ /* The PER-ELEMENT attribute cap (MKR_XML_MAX_ATTRS) is enforced by the tree
112
+ * builder; the arena counts every node - attributes included - toward max_nodes. */
113
+
114
+ mkr_xml_doc_t *mkr_xml_doc_new(void); /* budgets from MKR_XML_MAX_*; NULL on OOM */
115
+ void mkr_xml_doc_destroy(mkr_xml_doc_t *doc); /* whole-arena free */
116
+ size_t mkr_xml_doc_memsize(const mkr_xml_doc_t *doc);
117
+
118
+ /* --- secure arena: typed wrappers only (§8.2) ---
119
+ * The single overflow- and budget-checked alloc choke point is PRIVATE to
120
+ * mkr_xml_node.c. Callers never get a void* to cast: nodes via *_node (zeroed,
121
+ * type-validated), copied name/value bytes via *_bytes (the arena owns them).
122
+ * A caller that must fill a buffer itself uses mkr_xml_arena_spanbuf (below),
123
+ * never a raw pointer. On any failure doc->oom is set (sticky) and the wrapper
124
+ * returns NULL. */
125
+ mkr_xml_node_t *mkr_xml_arena_node(mkr_xml_doc_t *doc, mkr_xml_node_type_t type);
126
+ const char *mkr_xml_arena_bytes(mkr_xml_doc_t *doc, const char *src, uint32_t len);
127
+
128
+ /* Carve +cap+ arena bytes and return a bounded writer (core mkr_spanbuf) over
129
+ * them - the ONLY way to hand-fill arena bytes. The writer owns the cursor and
130
+ * the bounds check, so a write past the cut is impossible by construction (it is
131
+ * refused and latches ok=false, never overruns the next cut). Fill it with the
132
+ * core mkr_spanbuf_putc / _write, then mkr_spanbuf_finish; a NULL finish means
133
+ * the backing alloc failed (doc->oom set) OR a write was refused (doc->oom stays
134
+ * OK -> a broken invariant, i.e. our bug). On alloc failure the returned writer
135
+ * is already not-ok (buf == NULL), so every write is a safe no-op. */
136
+ mkr_spanbuf_t mkr_xml_arena_spanbuf(mkr_xml_doc_t *doc, size_t cap);
137
+
138
+ /* If attribute +a+ is an xmlns declaration, set the declared namespace prefix
139
+ * (NULL/plen 0 for the default xmlns) and its URI as arena slices, and return 1;
140
+ * else 0. The one shared detector for the namespace introspection, the C14N
141
+ * walk, and the mutation namespace resolver (all read these node fields). */
142
+ int mkr_xml_node_xmlns_decl(const mkr_xml_node_t *a, const char **prefix, uint32_t *plen,
143
+ const char **uri, uint32_t *ulen);
144
+
145
+ /* If the raw name [name,len) is an xmlns declaration ("xmlns" or "xmlns:PREFIX"),
146
+ * set the out-params to the declared prefix ("" / 0 for the default xmlns) and
147
+ * return 1; else return 0. prefix/plen may be NULL when only the boolean is
148
+ * wanted. The single byte-level xmlns detector the parser, the namespace
149
+ * resolver, and mkr_xml_node_xmlns_decl all share (the node version just reads
150
+ * the value too). */
151
+ int mkr_xml_xmlns_prefix(const char *name, uint32_t len, const char **prefix, uint32_t *plen);
152
+
153
+ /* Pre-order (document-order) descendant iteration over +root+'s subtree without
154
+ * recursion: start at +root+, then `cur = mkr_xml_preorder_next(root, cur)` until
155
+ * NULL. Visits +root+ first. The one shared form of the iterative DFS the index
156
+ * build and the mutation namespace resolver both need (no scope/state threaded). */
157
+ mkr_xml_node_t *mkr_xml_preorder_next(const mkr_xml_node_t *root, mkr_xml_node_t *cur);
158
+
159
+ /* Validate [name,len) as a well-formed XML 1.0 QName (full NameStartChar/NameChar
160
+ * scan) and split it into +out+ (prefix_len 0 = unprefixed). 0 on success, -1 if
161
+ * malformed. For the mutation path, whose input is not pre-scanned; the tree
162
+ * builder keeps its own lighter split (the name is already scan_name'd). */
163
+ int mkr_xml_qname_split(const char *name, uint32_t len, mkr_xml_qname_t *out);
164
+
165
+ /* Copy +qn+'s name into +doc+'s arena as one contiguous slice and point
166
+ * node->qname/local/prefix into it (ns_uri is the caller's concern). 0 on
167
+ * success, -1 on arena OOM (node left untouched, so the caller stays fail-closed).
168
+ * The single arena-copy-and-assign both the tree builder and the mutators use. */
169
+ int mkr_xml_qname_assign(mkr_xml_doc_t *doc, mkr_xml_node_t *node, const mkr_xml_qname_t *qn);
170
+
171
+ /* The QName parts a built node already carries, as a mkr_xml_qname_t value. */
172
+ static inline mkr_xml_qname_t
173
+ mkr_xml_qname_of(const mkr_xml_node_t *n)
174
+ {
175
+ mkr_xml_qname_t q = { n->qname, n->qname_len, n->prefix, n->prefix_len, n->local, n->local_len };
176
+ return q;
177
+ }
178
+
179
+ /* Self-test of the arena's secure-by-design properties (overflow, budgets,
180
+ * alignment, zero-init, copy-on-store, whole-free). Returns 0 on success or the
181
+ * 1-based index of the first failing check. Run from Makiri.__c_selftest. */
182
+ int mkr_xml_node_selftest(void);
183
+
184
+ #endif /* MKR_XML_NODE_H */