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,817 @@
1
+ /* mkr_xml_mutate.c - Ruby-free XML tree mutation primitives.
2
+ *
3
+ * See mkr_xml_mutate.h for the contract. Phase 1 (in-place edits: rename,
4
+ * attribute set/remove, content set, detach) and Phase 2 (building: node
5
+ * factories, deep-copy import, and insertion that resolves the inserted subtree's
6
+ * namespaces against its new context). Every primitive validates first and
7
+ * allocates all arena bytes - and, for insertion, resolves namespaces - BEFORE
8
+ * mutating any node link, so a failure (bad name, bad chars, unbound prefix,
9
+ * cycle, hierarchy, OOM) leaves the tree untouched, even for a move. The
10
+ * namespace resolver mirrors the parser's §7 rules (mkr_xml_tree.c) so a mutated
11
+ * node's ns_uri is identical to what a re-parse would compute.
12
+ */
13
+ #include "mkr_xml_mutate.h"
14
+ #include "mkr_xml.h" /* mkr_xml_is_name_start/char, mkr_xml_utf8_decode, mkr_xml_validate_chars */
15
+ #include "../core/mkr_core.h" /* mkr_grow_reserve (the iterative import stack) */
16
+
17
+ #include <stdlib.h> /* free (import stack) */
18
+ #include <string.h>
19
+
20
+ #define XML_NS_URI "http://www.w3.org/XML/1998/namespace"
21
+ #define XMLNS_NS_URI "http://www.w3.org/2000/xmlns/"
22
+ #define LIT_LEN(s) ((uint32_t)(sizeof(s) - 1))
23
+
24
+ /* Nearest in-scope binding for +prefix+ (pl 0 = the default namespace) at or
25
+ * above +node+, walking the real tree (no scope dictionary is threaded). */
26
+ static int
27
+ resolve_in_scope(const mkr_xml_node_t *node, const char *prefix, uint32_t pl,
28
+ const char **uri, uint32_t *ulen)
29
+ {
30
+ for (const mkr_xml_node_t *e = node; e != NULL; e = e->parent) {
31
+ if (e->type != MKR_XML_NODE_TYPE_ELEMENT) continue;
32
+ for (mkr_xml_node_t *a = e->attrs; a != NULL; a = a->next) {
33
+ const char *p, *u; uint32_t apl, ul;
34
+ if (mkr_xml_node_xmlns_decl(a, &p, &apl, &u, &ul)
35
+ && mkr_bytes_eq(p, apl, prefix, pl)) {
36
+ *uri = u; *ulen = ul; return 1;
37
+ }
38
+ }
39
+ }
40
+ return 0;
41
+ }
42
+
43
+ /* True if +node+ is part of the live document tree (its topmost ancestor is the
44
+ * document node), as opposed to a detached fragment still being assembled. An
45
+ * unbound prefix is a hard error only once connected (so the live tree stays
46
+ * serializable to well-formed XML); on a detached node it is deferred - left
47
+ * unresolved until the fragment is inserted, when resolution runs again. */
48
+ static int
49
+ is_connected(const mkr_xml_node_t *node)
50
+ {
51
+ const mkr_xml_node_t *top = node;
52
+ while (top->parent != NULL) top = top->parent;
53
+ return top->type == MKR_XML_NODE_TYPE_DOCUMENT;
54
+ }
55
+
56
+ /* Resolve the namespace for QName +qn+ applied at +scope+ (the element whose
57
+ * in-scope declarations apply). +is_attr+ selects attribute rules (xmlns*
58
+ * declarations live in the xmlns namespace, an unprefixed attribute is in no
59
+ * namespace). +connected+ decides what an unbound prefix means: an error in the
60
+ * live tree, deferred (unresolved) when detached. Mirrors mkr_xml_tree.c §7. */
61
+ static mkr_xml_mut_status_t
62
+ resolve_ns(const mkr_xml_node_t *scope, const mkr_xml_qname_t *qn, int is_attr,
63
+ int connected, const char **uri, uint32_t *ulen)
64
+ {
65
+ *uri = NULL; *ulen = 0;
66
+ if (is_attr && mkr_xml_xmlns_prefix(qn->qname, qn->qname_len, NULL, NULL)) {
67
+ *uri = XMLNS_NS_URI; *ulen = LIT_LEN(XMLNS_NS_URI); /* an xmlns declaration */
68
+ return MKR_XML_MUT_OK;
69
+ }
70
+ if (qn->prefix_len == 0) {
71
+ if (is_attr) return MKR_XML_MUT_OK; /* unprefixed attribute -> no namespace */
72
+ const char *u; uint32_t ul; /* unprefixed element -> the default ns */
73
+ if (resolve_in_scope(scope, "", 0, &u, &ul) && ul > 0) { *uri = u; *ulen = ul; }
74
+ return MKR_XML_MUT_OK;
75
+ }
76
+ if (mkr_bytes_eq(qn->prefix, qn->prefix_len, "xml", 3)) { /* the predefined xml: prefix */
77
+ *uri = XML_NS_URI; *ulen = LIT_LEN(XML_NS_URI);
78
+ return MKR_XML_MUT_OK;
79
+ }
80
+ if (mkr_bytes_eq(qn->prefix, qn->prefix_len, "xmlns", 5)) return MKR_XML_MUT_BAD_NAME; /* xmlns: as a normal name */
81
+ const char *u; uint32_t ul;
82
+ if (!resolve_in_scope(scope, qn->prefix, qn->prefix_len, &u, &ul) || ul == 0) {
83
+ return connected ? MKR_XML_MUT_UNBOUND_NS : MKR_XML_MUT_OK; /* defer when detached */
84
+ }
85
+ *uri = u; *ulen = ul;
86
+ return MKR_XML_MUT_OK;
87
+ }
88
+
89
+ /* Copy +qn+ into the arena and point the node's name fields at it (shared
90
+ * mkr_xml_qname_assign), mapping its OOM to the mutation status. */
91
+ static mkr_xml_mut_status_t
92
+ assign_qname(mkr_xml_doc_t *doc, mkr_xml_node_t *node, const mkr_xml_qname_t *qn)
93
+ {
94
+ return mkr_xml_qname_assign(doc, node, qn) == 0 ? MKR_XML_MUT_OK : MKR_XML_MUT_OOM;
95
+ }
96
+
97
+ void
98
+ mkr_xml_detach(mkr_xml_node_t *node)
99
+ {
100
+ mkr_xml_node_t *parent = node->parent;
101
+ if (parent == NULL) return;
102
+ if (node->type == MKR_XML_NODE_TYPE_ATTRIBUTE) {
103
+ mkr_xml_node_t *prev = NULL;
104
+ for (mkr_xml_node_t *a = parent->attrs; a != NULL; prev = a, a = a->next) {
105
+ if (a == node) {
106
+ if (prev) prev->next = a->next; else parent->attrs = a->next;
107
+ break;
108
+ }
109
+ }
110
+ node->next = NULL; node->parent = NULL;
111
+ return;
112
+ }
113
+ if (node->prev) node->prev->next = node->next; else parent->first_child = node->next;
114
+ if (node->next) node->next->prev = node->prev; else parent->last_child = node->prev;
115
+ node->parent = node->prev = node->next = NULL;
116
+ }
117
+
118
+ mkr_xml_mut_status_t
119
+ mkr_xml_rename(mkr_xml_doc_t *doc, mkr_xml_node_t *node, const char *name, uint32_t nlen)
120
+ {
121
+ if (node->type != MKR_XML_NODE_TYPE_ELEMENT && node->type != MKR_XML_NODE_TYPE_ATTRIBUTE)
122
+ return MKR_XML_MUT_TYPE;
123
+ mkr_xml_qname_t qn;
124
+ if (mkr_xml_qname_split(name, nlen, &qn) != 0) return MKR_XML_MUT_BAD_NAME;
125
+
126
+ int is_attr = (node->type == MKR_XML_NODE_TYPE_ATTRIBUTE);
127
+ const mkr_xml_node_t *scope = is_attr ? node->parent : node;
128
+ const char *uri; uint32_t ulen;
129
+ int connected = scope != NULL && is_connected(scope);
130
+ mkr_xml_mut_status_t st = resolve_ns(scope, &qn, is_attr, connected, &uri, &ulen);
131
+ if (st != MKR_XML_MUT_OK) return st;
132
+
133
+ /* Copy the new qname BEFORE writing ns_uri, so an OOM leaves node intact. */
134
+ st = assign_qname(doc, node, &qn);
135
+ if (st != MKR_XML_MUT_OK) return st;
136
+ node->ns_uri = uri; node->ns_uri_len = ulen;
137
+ return MKR_XML_MUT_OK;
138
+ }
139
+
140
+ mkr_xml_mut_status_t
141
+ mkr_xml_set_attribute(mkr_xml_doc_t *doc, mkr_xml_node_t *el, const char *name, uint32_t nlen,
142
+ const char *val, uint32_t vlen, mkr_xml_node_t **out)
143
+ {
144
+ if (out) *out = NULL;
145
+ if (el->type != MKR_XML_NODE_TYPE_ELEMENT) return MKR_XML_MUT_TYPE;
146
+ mkr_xml_qname_t qn;
147
+ if (mkr_xml_qname_split(name, nlen, &qn) != 0) return MKR_XML_MUT_BAD_NAME;
148
+ /* A prefixed namespace declaration (xmlns:foo="") must not bind a prefix to
149
+ * the empty namespace: XML Namespaces 1.0 forbids it and it serializes to
150
+ * invalid XML. (Undeclaring the DEFAULT namespace, xmlns="", is allowed.) */
151
+ if (vlen == 0 && qn.prefix_len == 5 && mkr_bytes_eq(qn.prefix, qn.prefix_len, "xmlns", 5))
152
+ return MKR_XML_MUT_BAD_NS_DECL;
153
+ if (vlen > 0 && mkr_xml_validate_chars(val, vlen) != 0) return MKR_XML_MUT_BAD_CHARS;
154
+
155
+ const char *uri; uint32_t ulen;
156
+ mkr_xml_mut_status_t st = resolve_ns(el, &qn, 1, is_connected(el), &uri, &ulen);
157
+ if (st != MKR_XML_MUT_OK) return st;
158
+
159
+ /* An existing attribute with the same raw QName -> replace its value. */
160
+ for (mkr_xml_node_t *a = el->attrs; a != NULL; a = a->next) {
161
+ if (mkr_bytes_eq(a->qname, a->qname_len, name, nlen)) {
162
+ const char *nv = mkr_xml_arena_bytes(doc, val, vlen);
163
+ if (vlen > 0 && nv == NULL) return MKR_XML_MUT_OOM;
164
+ a->value = nv ? nv : ""; a->value_len = vlen;
165
+ a->ns_uri = uri; a->ns_uri_len = ulen;
166
+ if (out) *out = a;
167
+ return MKR_XML_MUT_OK;
168
+ }
169
+ }
170
+
171
+ /* New attribute - allocate node + qname + value before linking. */
172
+ mkr_xml_node_t *attr = mkr_xml_arena_node(doc, MKR_XML_NODE_TYPE_ATTRIBUTE);
173
+ if (attr == NULL) return MKR_XML_MUT_OOM;
174
+ st = assign_qname(doc, attr, &qn);
175
+ if (st != MKR_XML_MUT_OK) return st; /* abandoned in the arena; freed with doc */
176
+ const char *nv = mkr_xml_arena_bytes(doc, val, vlen);
177
+ if (vlen > 0 && nv == NULL) return MKR_XML_MUT_OOM;
178
+ attr->value = nv ? nv : ""; attr->value_len = vlen;
179
+ attr->ns_uri = uri; attr->ns_uri_len = ulen;
180
+ attr->parent = el;
181
+
182
+ if (el->attrs == NULL) {
183
+ el->attrs = attr;
184
+ } else {
185
+ mkr_xml_node_t *t = el->attrs; /* O(attrs) tail find; bounded per element */
186
+ while (t->next != NULL) t = t->next;
187
+ t->next = attr;
188
+ }
189
+ if (out) *out = attr;
190
+ return MKR_XML_MUT_OK;
191
+ }
192
+
193
+ int
194
+ mkr_xml_remove_attribute(mkr_xml_node_t *el, const char *name, uint32_t nlen)
195
+ {
196
+ if (el->type != MKR_XML_NODE_TYPE_ELEMENT) return 0;
197
+ mkr_xml_node_t *prev = NULL;
198
+ for (mkr_xml_node_t *a = el->attrs; a != NULL; prev = a, a = a->next) {
199
+ if (mkr_bytes_eq(a->qname, a->qname_len, name, nlen)) {
200
+ if (prev) prev->next = a->next; else el->attrs = a->next;
201
+ a->next = NULL; a->parent = NULL;
202
+ return 1;
203
+ }
204
+ }
205
+ return 0;
206
+ }
207
+
208
+ /* Reject a forbidden character SEQUENCE for a leaf node's serialized value:
209
+ * "--" (or a trailing "-") in a comment, "]]>" in a CDATA section, "?>" in a PI
210
+ * - each would break the node's terminator and is not well-formed XML. Plain
211
+ * XML-Char validity is checked separately. Shared by every path that fills a
212
+ * leaf value (the factories and #content=), so none can bypass it. */
213
+ static mkr_xml_mut_status_t
214
+ mkr_xml_check_value_seq(uint8_t type, const char *text, uint32_t tlen)
215
+ {
216
+ switch (type) {
217
+ case MKR_XML_NODE_TYPE_COMMENT:
218
+ if (tlen > 0 && text[tlen - 1] == '-') return MKR_XML_MUT_BAD_CHARS;
219
+ for (uint32_t i = 1; i < tlen; i++) {
220
+ if (text[i] == '-' && text[i - 1] == '-') return MKR_XML_MUT_BAD_CHARS;
221
+ }
222
+ break;
223
+ case MKR_XML_NODE_TYPE_CDATA_SECTION:
224
+ for (uint32_t i = 2; i < tlen; i++) {
225
+ if (text[i] == '>' && text[i - 1] == ']' && text[i - 2] == ']') return MKR_XML_MUT_BAD_CHARS;
226
+ }
227
+ break;
228
+ case MKR_XML_NODE_TYPE_PI:
229
+ for (uint32_t i = 1; i < tlen; i++) {
230
+ if (text[i] == '>' && text[i - 1] == '?') return MKR_XML_MUT_BAD_CHARS;
231
+ }
232
+ break;
233
+ default:
234
+ break;
235
+ }
236
+ return MKR_XML_MUT_OK;
237
+ }
238
+
239
+ mkr_xml_mut_status_t
240
+ mkr_xml_set_content(mkr_xml_doc_t *doc, mkr_xml_node_t *node, const char *text, uint32_t tlen)
241
+ {
242
+ if (tlen > 0 && mkr_xml_validate_chars(text, tlen) != 0) return MKR_XML_MUT_BAD_CHARS;
243
+
244
+ switch (node->type) {
245
+ case MKR_XML_NODE_TYPE_TEXT:
246
+ case MKR_XML_NODE_TYPE_CDATA_SECTION:
247
+ case MKR_XML_NODE_TYPE_COMMENT:
248
+ case MKR_XML_NODE_TYPE_PI: {
249
+ mkr_xml_mut_status_t seq = mkr_xml_check_value_seq(node->type, text, tlen);
250
+ if (seq != MKR_XML_MUT_OK) return seq;
251
+ const char *nv = mkr_xml_arena_bytes(doc, text, tlen);
252
+ if (tlen > 0 && nv == NULL) return MKR_XML_MUT_OOM;
253
+ node->value = nv ? nv : ""; node->value_len = tlen;
254
+ return MKR_XML_MUT_OK;
255
+ }
256
+ case MKR_XML_NODE_TYPE_ELEMENT: {
257
+ /* Build the replacement TEXT node FIRST, so an OOM leaves the children
258
+ * intact (no half-applied content). */
259
+ mkr_xml_node_t *t = NULL;
260
+ if (tlen > 0) {
261
+ const char *nv = mkr_xml_arena_bytes(doc, text, tlen);
262
+ if (nv == NULL) return MKR_XML_MUT_OOM;
263
+ t = mkr_xml_arena_node(doc, MKR_XML_NODE_TYPE_TEXT);
264
+ if (t == NULL) return MKR_XML_MUT_OOM;
265
+ t->value = nv; t->value_len = tlen;
266
+ }
267
+ for (mkr_xml_node_t *c = node->first_child; c != NULL;) {
268
+ mkr_xml_node_t *nx = c->next;
269
+ c->parent = c->prev = c->next = NULL; /* detach each former child */
270
+ c = nx;
271
+ }
272
+ node->first_child = node->last_child = t; /* t may be NULL (empty element) */
273
+ if (t != NULL) t->parent = node;
274
+ return MKR_XML_MUT_OK;
275
+ }
276
+ default:
277
+ return MKR_XML_MUT_TYPE;
278
+ }
279
+ }
280
+
281
+ /* ============================ Phase 2: building ============================ */
282
+
283
+ /* True if [s,len) is the reserved PI target "xml" in any case (§2.6). */
284
+ static int
285
+ is_reserved_pi_target(const char *s, uint32_t len)
286
+ {
287
+ return len == 3 && (s[0] | 0x20) == 'x' && (s[1] | 0x20) == 'm' && (s[2] | 0x20) == 'l';
288
+ }
289
+
290
+ mkr_xml_mut_status_t
291
+ mkr_xml_new_element(mkr_xml_doc_t *doc, const char *name, uint32_t nlen, mkr_xml_node_t **out)
292
+ {
293
+ *out = NULL;
294
+ mkr_xml_qname_t qn;
295
+ if (mkr_xml_qname_split(name, nlen, &qn) != 0) return MKR_XML_MUT_BAD_NAME;
296
+ if (mkr_bytes_eq(qn.prefix, qn.prefix_len, "xmlns", 5)) return MKR_XML_MUT_BAD_NAME; /* xmlns: is not an element prefix */
297
+ mkr_xml_node_t *el = mkr_xml_arena_node(doc, MKR_XML_NODE_TYPE_ELEMENT);
298
+ if (el == NULL) return MKR_XML_MUT_OOM;
299
+ mkr_xml_mut_status_t st = assign_qname(doc, el, &qn);
300
+ if (st != MKR_XML_MUT_OK) return st;
301
+ *out = el; /* ns_uri stays unresolved until insertion */
302
+ return MKR_XML_MUT_OK;
303
+ }
304
+
305
+ mkr_xml_mut_status_t
306
+ mkr_xml_new_chardata(mkr_xml_doc_t *doc, uint8_t type, const char *text, uint32_t tlen,
307
+ mkr_xml_node_t **out)
308
+ {
309
+ *out = NULL;
310
+ if (type != MKR_XML_NODE_TYPE_TEXT && type != MKR_XML_NODE_TYPE_CDATA_SECTION
311
+ && type != MKR_XML_NODE_TYPE_COMMENT) {
312
+ return MKR_XML_MUT_TYPE;
313
+ }
314
+ if (tlen > 0 && mkr_xml_validate_chars(text, tlen) != 0) return MKR_XML_MUT_BAD_CHARS;
315
+ mkr_xml_mut_status_t seq = mkr_xml_check_value_seq(type, text, tlen);
316
+ if (seq != MKR_XML_MUT_OK) return seq;
317
+ mkr_xml_node_t *n = mkr_xml_arena_node(doc, type);
318
+ if (n == NULL) return MKR_XML_MUT_OOM;
319
+ const char *v = mkr_xml_arena_bytes(doc, text, tlen);
320
+ if (tlen > 0 && v == NULL) return MKR_XML_MUT_OOM;
321
+ n->value = v ? v : ""; n->value_len = tlen;
322
+ *out = n;
323
+ return MKR_XML_MUT_OK;
324
+ }
325
+
326
+ mkr_xml_mut_status_t
327
+ mkr_xml_new_pi(mkr_xml_doc_t *doc, const char *target, uint32_t tlen,
328
+ const char *data, uint32_t dlen, mkr_xml_node_t **out)
329
+ {
330
+ *out = NULL;
331
+ /* PITarget is an NCName (a QName with no colon) and not "xml" in any case. */
332
+ mkr_xml_qname_t qn;
333
+ if (mkr_xml_qname_split(target, tlen, &qn) != 0 || qn.prefix_len != 0) return MKR_XML_MUT_BAD_NAME;
334
+ if (is_reserved_pi_target(target, tlen)) return MKR_XML_MUT_BAD_NAME;
335
+ if (dlen > 0 && mkr_xml_validate_chars(data, dlen) != 0) return MKR_XML_MUT_BAD_CHARS;
336
+ mkr_xml_mut_status_t seq = mkr_xml_check_value_seq(MKR_XML_NODE_TYPE_PI, data, dlen);
337
+ if (seq != MKR_XML_MUT_OK) return seq;
338
+ mkr_xml_node_t *pi = mkr_xml_arena_node(doc, MKR_XML_NODE_TYPE_PI);
339
+ if (pi == NULL) return MKR_XML_MUT_OOM;
340
+ const char *t = mkr_xml_arena_bytes(doc, target, tlen);
341
+ if (tlen > 0 && t == NULL) return MKR_XML_MUT_OOM;
342
+ const char *d = mkr_xml_arena_bytes(doc, data, dlen);
343
+ if (dlen > 0 && d == NULL) return MKR_XML_MUT_OOM;
344
+ pi->local = t; pi->local_len = tlen; /* the PI target (Node#name) */
345
+ pi->value = d ? d : ""; pi->value_len = dlen;
346
+ *out = pi;
347
+ return MKR_XML_MUT_OK;
348
+ }
349
+
350
+ /* Resolve the namespace of every element (and prefixed attribute) in +root+'s
351
+ * subtree against the in-scope declarations reachable from each node - which,
352
+ * because the caller has temporarily pointed root->parent at the prospective
353
+ * context, includes that context and its ancestors. Order-independent (it reads
354
+ * xmlns attribute values, never resolved ns_uri). Iterative pre-order, no
355
+ * recursion. Fails closed on an unbound prefix, having changed no link. */
356
+ static mkr_xml_mut_status_t
357
+ resolve_node_ns(mkr_xml_node_t *e, int connected)
358
+ {
359
+ const char *uri; uint32_t ulen;
360
+ mkr_xml_qname_t eq = mkr_xml_qname_of(e);
361
+ mkr_xml_mut_status_t st = resolve_ns(e, &eq, 0, connected, &uri, &ulen);
362
+ if (st != MKR_XML_MUT_OK) return st;
363
+ e->ns_uri = uri; e->ns_uri_len = ulen;
364
+ for (mkr_xml_node_t *a = e->attrs; a != NULL; a = a->next) {
365
+ mkr_xml_qname_t aq = mkr_xml_qname_of(a);
366
+ st = resolve_ns(e, &aq, 1, connected, &uri, &ulen);
367
+ if (st != MKR_XML_MUT_OK) return st;
368
+ a->ns_uri = uri; a->ns_uri_len = ulen;
369
+ }
370
+ return MKR_XML_MUT_OK;
371
+ }
372
+
373
+ static mkr_xml_mut_status_t
374
+ resolve_subtree(mkr_xml_node_t *root, int connected)
375
+ {
376
+ for (mkr_xml_node_t *cur = root; cur != NULL; cur = mkr_xml_preorder_next(root, cur)) {
377
+ if (cur->type == MKR_XML_NODE_TYPE_ELEMENT) {
378
+ mkr_xml_mut_status_t st = resolve_node_ns(cur, connected);
379
+ if (st != MKR_XML_MUT_OK) return st;
380
+ }
381
+ }
382
+ return MKR_XML_MUT_OK;
383
+ }
384
+
385
+ /* Resolve +node+'s subtree as if it were a child of +context+, WITHOUT linking it
386
+ * (so a resolution failure leaves the tree untouched): temporarily borrow
387
+ * node->parent for the ancestor walk, then restore it. An unbound prefix errors
388
+ * only when +context+ is part of the live document (is_connected via the borrowed
389
+ * parent); inserting into a still-detached fragment defers resolution. */
390
+ static mkr_xml_mut_status_t
391
+ resolve_into(mkr_xml_node_t *node, mkr_xml_node_t *context)
392
+ {
393
+ mkr_xml_node_t *saved = node->parent;
394
+ node->parent = context;
395
+ mkr_xml_mut_status_t st = resolve_subtree(node, is_connected(node));
396
+ node->parent = saved;
397
+ return st;
398
+ }
399
+
400
+ /* A single arena copy of +src+ (its own fields + attributes, NOT its children).
401
+ * When +with_ns+ is false the resolved namespace URI is left unset (the import
402
+ * path re-resolves it at the insertion site); when true it is copied too, for
403
+ * clone_node, whose detached result must keep its namespace (it is never
404
+ * re-resolved). Returns NULL on OOM. */
405
+ static mkr_xml_node_t *
406
+ copy_one(mkr_xml_doc_t *doc, const mkr_xml_node_t *src, bool with_ns)
407
+ {
408
+ mkr_xml_node_t *n = mkr_xml_arena_node(doc, src->type);
409
+ if (n == NULL) return NULL;
410
+ if (src->qname != NULL && src->qname_len > 0) { /* element / attribute QName */
411
+ mkr_xml_qname_t qn = mkr_xml_qname_of(src);
412
+ if (assign_qname(doc, n, &qn) != MKR_XML_MUT_OK) return NULL;
413
+ } else if (src->local != NULL && src->local_len > 0) { /* PI target */
414
+ const char *t = mkr_xml_arena_bytes(doc, src->local, src->local_len);
415
+ if (t == NULL) return NULL;
416
+ n->local = t; n->local_len = src->local_len;
417
+ }
418
+ if (src->value_len > 0) {
419
+ const char *v = mkr_xml_arena_bytes(doc, src->value, src->value_len);
420
+ if (v == NULL) return NULL;
421
+ n->value = v; n->value_len = src->value_len;
422
+ } else if (src->value != NULL) {
423
+ n->value = "";
424
+ }
425
+ if (with_ns && src->ns_uri != NULL && src->ns_uri_len > 0) {
426
+ const char *u = mkr_xml_arena_bytes(doc, src->ns_uri, src->ns_uri_len);
427
+ if (u == NULL) return NULL;
428
+ n->ns_uri = u; n->ns_uri_len = src->ns_uri_len;
429
+ }
430
+ /* copy attributes (each an arena node), preserving order */
431
+ mkr_xml_node_t *tail = NULL;
432
+ for (mkr_xml_node_t *a = src->attrs; a != NULL; a = a->next) {
433
+ mkr_xml_node_t *ca = copy_one(doc, a, with_ns); /* an attribute has no children/attrs */
434
+ if (ca == NULL) return NULL;
435
+ ca->parent = n;
436
+ if (tail) tail->next = ca; else n->attrs = ca;
437
+ tail = ca;
438
+ }
439
+ return n;
440
+ }
441
+
442
+ /* Deep copy of +src+'s subtree into +doc+'s arena. +with_ns+ is threaded to
443
+ * copy_one: false for import (the insertion site re-resolves namespaces), true
444
+ * for clone_node (the detached result keeps its resolved namespaces). */
445
+ static mkr_xml_mut_status_t
446
+ deep_copy(mkr_xml_doc_t *doc, const mkr_xml_node_t *src, bool with_ns,
447
+ mkr_xml_node_t **out)
448
+ {
449
+ *out = NULL;
450
+ mkr_xml_node_t *root = copy_one(doc, src, with_ns);
451
+ if (root == NULL) return MKR_XML_MUT_OOM;
452
+
453
+ /* Iterative pre-order: an explicit heap stack of (src, dst) pairs copies each
454
+ * src node's children under its dst copy. No C recursion -> no stack DoS on a
455
+ * deep constructed tree. */
456
+ typedef struct { const mkr_xml_node_t *s; mkr_xml_node_t *d; } frame_t;
457
+ frame_t *stack = NULL; size_t cap = 0, top = 0;
458
+ mkr_xml_mut_status_t st = MKR_XML_MUT_OK;
459
+
460
+ if (mkr_grow_reserve((void **)&stack, &cap, 1, sizeof(*stack)) != MKR_OK) {
461
+ return MKR_XML_MUT_OOM;
462
+ }
463
+ stack[top].s = src; stack[top].d = root; top++;
464
+ while (top > 0) {
465
+ frame_t f = stack[--top];
466
+ mkr_xml_node_t *dtail = NULL;
467
+ for (const mkr_xml_node_t *sc = f.s->first_child; sc != NULL; sc = sc->next) {
468
+ mkr_xml_node_t *dc = copy_one(doc, sc, with_ns);
469
+ if (dc == NULL) { st = MKR_XML_MUT_OOM; goto done; }
470
+ dc->parent = f.d;
471
+ if (dtail) { dtail->next = dc; dc->prev = dtail; }
472
+ else f.d->first_child = dc;
473
+ f.d->last_child = dc;
474
+ dtail = dc;
475
+ if (sc->first_child != NULL) {
476
+ if (mkr_grow_reserve((void **)&stack, &cap, top + 1, sizeof(*stack)) != MKR_OK) {
477
+ st = MKR_XML_MUT_OOM; goto done;
478
+ }
479
+ stack[top].s = sc; stack[top].d = dc; top++;
480
+ }
481
+ }
482
+ }
483
+ done:
484
+ free(stack);
485
+ if (st != MKR_XML_MUT_OK) return st; /* partial copy abandoned in the arena */
486
+ *out = root;
487
+ return MKR_XML_MUT_OK;
488
+ }
489
+
490
+ mkr_xml_mut_status_t
491
+ mkr_xml_import_subtree(mkr_xml_doc_t *doc, const mkr_xml_node_t *src, mkr_xml_node_t **out)
492
+ {
493
+ return deep_copy(doc, src, false, out); /* namespaces re-resolved at the insertion site */
494
+ }
495
+
496
+ mkr_xml_mut_status_t
497
+ mkr_xml_clone_node(mkr_xml_doc_t *doc, const mkr_xml_node_t *src, bool deep,
498
+ mkr_xml_node_t **out)
499
+ {
500
+ if (deep) {
501
+ return deep_copy(doc, src, true, out);
502
+ }
503
+ *out = copy_one(doc, src, true); /* shallow: own fields + attributes, no children */
504
+ return (*out == NULL) ? MKR_XML_MUT_OOM : MKR_XML_MUT_OK;
505
+ }
506
+
507
+ /* ---- insertion ---- */
508
+
509
+ /* +node+ may be placed in the tree at all (not an attribute / document / doctype,
510
+ * which are never element-level children). */
511
+ static int
512
+ is_insertable(const mkr_xml_node_t *node)
513
+ {
514
+ switch (node->type) {
515
+ case MKR_XML_NODE_TYPE_ELEMENT:
516
+ case MKR_XML_NODE_TYPE_TEXT:
517
+ case MKR_XML_NODE_TYPE_CDATA_SECTION:
518
+ case MKR_XML_NODE_TYPE_COMMENT:
519
+ case MKR_XML_NODE_TYPE_PI:
520
+ return 1;
521
+ default:
522
+ return 0;
523
+ }
524
+ }
525
+
526
+ /* +node+ must not be an inclusive ancestor of +container+ (else a cycle). */
527
+ static int
528
+ would_cycle(const mkr_xml_node_t *container, const mkr_xml_node_t *node)
529
+ {
530
+ for (const mkr_xml_node_t *p = container; p != NULL; p = p->parent) {
531
+ if (p == node) return 1;
532
+ }
533
+ return 0;
534
+ }
535
+
536
+ /* The XML single-root rule: the document node holds at most one ELEMENT child.
537
+ * True if linking +node+ under document +container+ is allowed (i.e. node is not
538
+ * an element, or there is no existing element child other than +exclude+, which
539
+ * is the node being replaced). */
540
+ static int
541
+ doc_root_ok(const mkr_xml_node_t *container, const mkr_xml_node_t *node,
542
+ const mkr_xml_node_t *exclude)
543
+ {
544
+ if (container->type != MKR_XML_NODE_TYPE_DOCUMENT) return 1;
545
+ if (node->type != MKR_XML_NODE_TYPE_ELEMENT) return 1; /* comments/PIs are fine at doc level */
546
+ for (const mkr_xml_node_t *c = container->first_child; c != NULL; c = c->next) {
547
+ if (c != exclude && c != node && c->type == MKR_XML_NODE_TYPE_ELEMENT) return 0;
548
+ }
549
+ return 1;
550
+ }
551
+
552
+ /* After a structural change whose container is the document node, point doc->root
553
+ * at the (single) element child, or NULL. */
554
+ static void
555
+ sync_doc_root(mkr_xml_doc_t *doc, const mkr_xml_node_t *container)
556
+ {
557
+ if (container != doc->doc_node) return;
558
+ doc->root = NULL;
559
+ for (mkr_xml_node_t *c = doc->doc_node->first_child; c != NULL; c = c->next) {
560
+ if (c->type == MKR_XML_NODE_TYPE_ELEMENT) { doc->root = c; break; }
561
+ }
562
+ }
563
+
564
+ /* Shared validation + namespace resolution for an insertion of +node+ under
565
+ * +container+ (the future parent), replacing +exclude+ (or NULL). Performs NO
566
+ * structural change; on success the caller commits the link. */
567
+ static mkr_xml_mut_status_t
568
+ prepare_insert(mkr_xml_node_t *container, mkr_xml_node_t *node, const mkr_xml_node_t *exclude)
569
+ {
570
+ if (!is_insertable(node)) return MKR_XML_MUT_HIERARCHY;
571
+ if (container->type != MKR_XML_NODE_TYPE_ELEMENT
572
+ && container->type != MKR_XML_NODE_TYPE_DOCUMENT) {
573
+ return MKR_XML_MUT_HIERARCHY; /* only elements / the document hold children */
574
+ }
575
+ if (would_cycle(container, node)) return MKR_XML_MUT_CYCLE;
576
+ if (!doc_root_ok(container, node, exclude)) return MKR_XML_MUT_HIERARCHY;
577
+ return resolve_into(node, container); /* fail-closed: no link changed yet */
578
+ }
579
+
580
+ mkr_xml_mut_status_t
581
+ mkr_xml_insert_child(mkr_xml_doc_t *doc, mkr_xml_node_t *parent, mkr_xml_node_t *node)
582
+ {
583
+ mkr_xml_mut_status_t st = prepare_insert(parent, node, NULL);
584
+ if (st != MKR_XML_MUT_OK) return st;
585
+ mkr_xml_detach(node); /* move semantics */
586
+ node->parent = parent;
587
+ node->prev = parent->last_child;
588
+ node->next = NULL;
589
+ if (parent->last_child) parent->last_child->next = node; else parent->first_child = node;
590
+ parent->last_child = node;
591
+ sync_doc_root(doc, parent);
592
+ return MKR_XML_MUT_OK;
593
+ }
594
+
595
+ mkr_xml_mut_status_t
596
+ mkr_xml_insert_before(mkr_xml_doc_t *doc, mkr_xml_node_t *ref, mkr_xml_node_t *node)
597
+ {
598
+ if (node == ref) return MKR_XML_MUT_OK; /* inserting a node before itself is a no-op */
599
+ mkr_xml_node_t *container = ref->parent;
600
+ if (container == NULL) return MKR_XML_MUT_HIERARCHY; /* a parentless ref has no sibling slot */
601
+ mkr_xml_mut_status_t st = prepare_insert(container, node, NULL);
602
+ if (st != MKR_XML_MUT_OK) return st;
603
+ mkr_xml_detach(node);
604
+ node->parent = container;
605
+ node->next = ref;
606
+ node->prev = ref->prev;
607
+ if (ref->prev) ref->prev->next = node; else container->first_child = node;
608
+ ref->prev = node;
609
+ sync_doc_root(doc, container);
610
+ return MKR_XML_MUT_OK;
611
+ }
612
+
613
+ mkr_xml_mut_status_t
614
+ mkr_xml_insert_after(mkr_xml_doc_t *doc, mkr_xml_node_t *ref, mkr_xml_node_t *node)
615
+ {
616
+ if (node == ref) return MKR_XML_MUT_OK; /* inserting a node after itself is a no-op */
617
+ mkr_xml_node_t *container = ref->parent;
618
+ if (container == NULL) return MKR_XML_MUT_HIERARCHY;
619
+ mkr_xml_mut_status_t st = prepare_insert(container, node, NULL);
620
+ if (st != MKR_XML_MUT_OK) return st;
621
+ mkr_xml_detach(node);
622
+ node->parent = container;
623
+ node->prev = ref;
624
+ node->next = ref->next;
625
+ if (ref->next) ref->next->prev = node; else container->last_child = node;
626
+ ref->next = node;
627
+ sync_doc_root(doc, container);
628
+ return MKR_XML_MUT_OK;
629
+ }
630
+
631
+ mkr_xml_mut_status_t
632
+ mkr_xml_replace_node(mkr_xml_doc_t *doc, mkr_xml_node_t *ref, mkr_xml_node_t *node)
633
+ {
634
+ mkr_xml_node_t *container = ref->parent;
635
+ if (container == NULL) return MKR_XML_MUT_HIERARCHY; /* a parentless / document ref */
636
+ if (node == ref) return MKR_XML_MUT_OK;
637
+ mkr_xml_mut_status_t st = prepare_insert(container, node, ref);
638
+ if (st != MKR_XML_MUT_OK) return st;
639
+ mkr_xml_detach(node);
640
+ node->parent = container;
641
+ node->prev = ref->prev;
642
+ node->next = ref->next;
643
+ if (ref->prev) ref->prev->next = node; else container->first_child = node;
644
+ if (ref->next) ref->next->prev = node; else container->last_child = node;
645
+ ref->parent = ref->prev = ref->next = NULL; /* detach the replaced node */
646
+ sync_doc_root(doc, container);
647
+ return MKR_XML_MUT_OK;
648
+ }
649
+
650
+ /* ---- self-test (Makiri.__c_selftest) ---- */
651
+ int
652
+ mkr_xml_mutate_selftest(void)
653
+ {
654
+ mkr_xml_doc_t *doc = mkr_xml_doc_new();
655
+ if (doc == NULL) return 1;
656
+ int rc = 0;
657
+ mkr_xml_qname_t qn;
658
+
659
+ /* 1. QName validation + split (the shared mkr_xml_qname_split) */
660
+ if (mkr_xml_qname_split("a:b", 3, &qn) != 0 || qn.prefix_len != 1 || qn.local_len != 1) { rc = 2; goto done; }
661
+ if (mkr_xml_qname_split("1bad", 4, &qn) == 0) { rc = 3; goto done; } /* bad NameStartChar */
662
+ if (mkr_xml_qname_split("a:b:c", 5, &qn) == 0) { rc = 4; goto done; } /* two colons */
663
+ if (mkr_xml_qname_split(":x", 2, &qn) == 0) { rc = 5; goto done; } /* empty prefix */
664
+ if (mkr_xml_qname_split("x:", 2, &qn) == 0) { rc = 6; goto done; } /* empty local */
665
+
666
+ /* 2. build a root element, exercise attribute set/replace */
667
+ mkr_xml_node_t *r = mkr_xml_arena_node(doc, MKR_XML_NODE_TYPE_ELEMENT);
668
+ if (r == NULL) { rc = 7; goto done; }
669
+ mkr_xml_qname_t rq = { "r", 1, "r", 0, "r", 1 };
670
+ if (assign_qname(doc, r, &rq) != MKR_XML_MUT_OK) { rc = 8; goto done; }
671
+
672
+ mkr_xml_node_t *at = NULL;
673
+ if (mkr_xml_set_attribute(doc, r, "id", 2, "x", 1, &at) != MKR_XML_MUT_OK
674
+ || at == NULL || at->value_len != 1 || at->value[0] != 'x' || r->attrs != at) { rc = 9; goto done; }
675
+ if (mkr_xml_set_attribute(doc, r, "id", 2, "yy", 2, &at) != MKR_XML_MUT_OK
676
+ || at->value_len != 2 || r->attrs->next != NULL) { rc = 10; goto done; } /* replaced, still one */
677
+
678
+ /* 3. fail-closed: non-XML-Char value. An unbound prefix on a DETACHED element
679
+ * defers (left unresolved), not an error - the live-tree error is exercised
680
+ * in the Phase 2 checks below. */
681
+ if (mkr_xml_set_attribute(doc, r, "k", 1, "\x01", 1, NULL) != MKR_XML_MUT_BAD_CHARS) { rc = 11; goto done; }
682
+
683
+ /* 3b. fail-closed: forbidden value SEQUENCES (would serialize to invalid XML),
684
+ * enforced uniformly by the factories AND by set_content (no bypass). */
685
+ mkr_xml_node_t *chk = NULL;
686
+ if (mkr_xml_new_chardata(doc, MKR_XML_NODE_TYPE_COMMENT, "a--b", 4, &chk) != MKR_XML_MUT_BAD_CHARS) { rc = 111; goto done; }
687
+ if (mkr_xml_new_chardata(doc, MKR_XML_NODE_TYPE_COMMENT, "x-", 2, &chk) != MKR_XML_MUT_BAD_CHARS) { rc = 112; goto done; } /* trailing - */
688
+ if (mkr_xml_new_chardata(doc, MKR_XML_NODE_TYPE_CDATA_SECTION, "a]]>b", 5, &chk) != MKR_XML_MUT_BAD_CHARS) { rc = 113; goto done; }
689
+ if (mkr_xml_new_chardata(doc, MKR_XML_NODE_TYPE_COMMENT, "a-b", 3, &chk) != MKR_XML_MUT_OK || chk == NULL) { rc = 114; goto done; } /* single - fine */
690
+ if (mkr_xml_set_content(doc, chk, "x--y", 4) != MKR_XML_MUT_BAD_CHARS) { rc = 115; goto done; } /* set_content guarded too */
691
+ /* fail-closed: a prefixed namespace declaration may not bind to "" (default xmlns="" IS allowed). */
692
+ if (mkr_xml_set_attribute(doc, r, "xmlns:q", 7, "", 0, NULL) != MKR_XML_MUT_BAD_NS_DECL) { rc = 116; goto done; }
693
+ if (mkr_xml_set_attribute(doc, r, "xmlns", 5, "", 0, NULL) != MKR_XML_MUT_OK) { rc = 117; goto done; }
694
+
695
+ mkr_xml_node_t *det = NULL;
696
+ if (mkr_xml_new_element(doc, "det", 3, &det) != MKR_XML_MUT_OK
697
+ || mkr_xml_set_attribute(doc, det, "p:k", 3, "v", 1, &at) != MKR_XML_MUT_OK
698
+ || at->ns_uri_len != 0) { rc = 12; goto done; }
699
+
700
+ /* 4. the predefined xml: prefix resolves with no declaration */
701
+ if (mkr_xml_set_attribute(doc, r, "xml:lang", 8, "en", 2, &at) != MKR_XML_MUT_OK
702
+ || !mkr_bytes_eq(at->ns_uri, at->ns_uri_len, XML_NS_URI, LIT_LEN(XML_NS_URI))) { rc = 13; goto done; }
703
+
704
+ /* 5. adding an xmlns:* declaration lands in the xmlns namespace and then binds
705
+ * a previously-unbound prefix */
706
+ if (mkr_xml_set_attribute(doc, r, "xmlns:p", 7, "urn:p", 5, &at) != MKR_XML_MUT_OK
707
+ || !mkr_bytes_eq(at->ns_uri, at->ns_uri_len, XMLNS_NS_URI, LIT_LEN(XMLNS_NS_URI))) { rc = 14; goto done; }
708
+ if (mkr_xml_set_attribute(doc, r, "p:k", 3, "v", 1, &at) != MKR_XML_MUT_OK
709
+ || !mkr_bytes_eq(at->ns_uri, at->ns_uri_len, "urn:p", 5)) { rc = 15; goto done; }
710
+
711
+ /* 6. remove by name (idempotent) */
712
+ if (mkr_xml_remove_attribute(r, "id", 2) != 1) { rc = 16; goto done; }
713
+ if (mkr_xml_remove_attribute(r, "id", 2) != 0) { rc = 17; goto done; }
714
+
715
+ /* 7. rename the element + re-resolve namespace */
716
+ if (mkr_xml_rename(doc, r, "q", 1) != MKR_XML_MUT_OK
717
+ || r->qname_len != 1 || r->qname[0] != 'q' || r->ns_uri_len != 0) { rc = 18; goto done; }
718
+
719
+ /* 8. content: replace children with text, then empty it */
720
+ mkr_xml_node_t *c1 = mkr_xml_arena_node(doc, MKR_XML_NODE_TYPE_ELEMENT);
721
+ if (c1 == NULL) { rc = 19; goto done; }
722
+ c1->parent = r; r->first_child = r->last_child = c1;
723
+ if (mkr_xml_set_content(doc, r, "hi", 2) != MKR_XML_MUT_OK) { rc = 20; goto done; }
724
+ if (r->first_child == NULL || r->first_child->type != MKR_XML_NODE_TYPE_TEXT
725
+ || r->first_child->value_len != 2 || r->last_child != r->first_child
726
+ || c1->parent != NULL) { rc = 21; goto done; }
727
+ if (mkr_xml_set_content(doc, r, "", 0) != MKR_XML_MUT_OK || r->first_child != NULL
728
+ || r->last_child != NULL) { rc = 22; goto done; }
729
+
730
+ /* 9. detach a middle/edge tree child, sibling links stay consistent */
731
+ mkr_xml_node_t *a1 = mkr_xml_arena_node(doc, MKR_XML_NODE_TYPE_ELEMENT);
732
+ mkr_xml_node_t *a2 = mkr_xml_arena_node(doc, MKR_XML_NODE_TYPE_ELEMENT);
733
+ if (a1 == NULL || a2 == NULL) { rc = 23; goto done; }
734
+ a1->parent = r; a2->parent = r; a1->next = a2; a2->prev = a1;
735
+ r->first_child = a1; r->last_child = a2;
736
+ mkr_xml_detach(a1);
737
+ if (r->first_child != a2 || a2->prev != NULL || a1->parent != NULL) { rc = 24; goto done; }
738
+ mkr_xml_detach(a2);
739
+ if (r->first_child != NULL || r->last_child != NULL || a2->parent != NULL) { rc = 25; goto done; }
740
+
741
+ /* 10. Phase 2 - a live document node + a connected root carrying xmlns:p */
742
+ mkr_xml_node_t *docn = mkr_xml_arena_node(doc, MKR_XML_NODE_TYPE_DOCUMENT);
743
+ if (docn == NULL) { rc = 26; goto done; }
744
+ doc->doc_node = docn;
745
+ mkr_xml_node_t *pr = NULL, *ne = NULL, *tx = NULL;
746
+ if (mkr_xml_new_element(doc, "pr", 2, &pr) != MKR_XML_MUT_OK || pr == NULL
747
+ || pr->parent != NULL || pr->ns_uri_len != 0) { rc = 27; goto done; }
748
+ if (mkr_xml_set_attribute(doc, pr, "xmlns:p", 7, "urn:p", 5, NULL) != MKR_XML_MUT_OK) { rc = 28; goto done; }
749
+ if (mkr_xml_insert_child(doc, docn, pr) != MKR_XML_MUT_OK || doc->root != pr
750
+ || !is_connected(pr)) { rc = 29; goto done; }
751
+ if (mkr_xml_new_chardata(doc, MKR_XML_NODE_TYPE_TEXT, "hi", 2, &tx) != MKR_XML_MUT_OK
752
+ || tx->value_len != 2) { rc = 30; goto done; }
753
+
754
+ /* 11. insert_child links + resolves the inserted subtree's namespace (p bound on pr) */
755
+ if (mkr_xml_new_element(doc, "p:c", 3, &ne) != MKR_XML_MUT_OK) { rc = 31; goto done; }
756
+ if (mkr_xml_insert_child(doc, pr, ne) != MKR_XML_MUT_OK
757
+ || pr->first_child != ne || ne->parent != pr
758
+ || !mkr_bytes_eq(ne->ns_uri, ne->ns_uri_len, "urn:p", 5)) { rc = 32; goto done; }
759
+ if (mkr_xml_insert_child(doc, ne, tx) != MKR_XML_MUT_OK || ne->first_child != tx) { rc = 33; goto done; }
760
+
761
+ /* 12. an unbound prefix is a hard error in the live tree, leaving it unchanged */
762
+ mkr_xml_node_t *ub = NULL;
763
+ if (mkr_xml_new_element(doc, "z:c", 3, &ub) != MKR_XML_MUT_OK) { rc = 34; goto done; }
764
+ if (mkr_xml_insert_child(doc, pr, ub) != MKR_XML_MUT_UNBOUND_NS
765
+ || ub->parent != NULL || pr->last_child != ne) { rc = 35; goto done; }
766
+
767
+ /* 13. deferred resolution: build a detached fragment whose prefix is unbound
768
+ * there, then connect it - resolution succeeds against the live context */
769
+ mkr_xml_node_t *wrap = NULL, *inner = NULL;
770
+ if (mkr_xml_new_element(doc, "p:wrap", 6, &wrap) != MKR_XML_MUT_OK
771
+ || mkr_xml_new_element(doc, "p:inner", 7, &inner) != MKR_XML_MUT_OK) { rc = 36; goto done; }
772
+ if (mkr_xml_insert_child(doc, wrap, inner) != MKR_XML_MUT_OK
773
+ || inner->ns_uri_len != 0) { rc = 37; goto done; } /* deferred while detached */
774
+ if (mkr_xml_insert_child(doc, pr, wrap) != MKR_XML_MUT_OK
775
+ || !mkr_bytes_eq(wrap->ns_uri, wrap->ns_uri_len, "urn:p", 5)
776
+ || !mkr_bytes_eq(inner->ns_uri, inner->ns_uri_len, "urn:p", 5)) { rc = 38; goto done; }
777
+
778
+ /* 14. cycle rejection: a node cannot be inserted under its own descendant */
779
+ if (mkr_xml_insert_child(doc, ne, pr) != MKR_XML_MUT_CYCLE) { rc = 39; goto done; }
780
+
781
+ /* 15. sibling order: before / after place correctly */
782
+ mkr_xml_node_t *b1 = NULL, *b2 = NULL;
783
+ if (mkr_xml_new_element(doc, "b1", 2, &b1) != MKR_XML_MUT_OK
784
+ || mkr_xml_new_element(doc, "b2", 2, &b2) != MKR_XML_MUT_OK) { rc = 40; goto done; }
785
+ if (mkr_xml_insert_before(doc, ne, b1) != MKR_XML_MUT_OK || pr->first_child != b1
786
+ || b1->next != ne) { rc = 41; goto done; }
787
+ if (mkr_xml_insert_after(doc, ne, b2) != MKR_XML_MUT_OK || ne->next != b2) { rc = 42; goto done; }
788
+ /* inserting a node before/after ITSELF is a no-op (must not form a self-cycle) */
789
+ if (mkr_xml_insert_before(doc, ne, ne) != MKR_XML_MUT_OK || ne->next == ne || ne->prev == ne
790
+ || mkr_xml_insert_after(doc, ne, ne) != MKR_XML_MUT_OK || ne->next == ne) { rc = 99; goto done; }
791
+
792
+ /* 16. replace swaps a node in place (detach-never-destroy of the old node) */
793
+ mkr_xml_node_t *rep = NULL;
794
+ if (mkr_xml_new_element(doc, "rep", 3, &rep) != MKR_XML_MUT_OK) { rc = 43; goto done; }
795
+ if (mkr_xml_replace_node(doc, ne, rep) != MKR_XML_MUT_OK
796
+ || ne->parent != NULL || rep->parent != pr || b1->next != rep) { rc = 44; goto done; }
797
+
798
+ /* 17. cross-document import: a deep, independent copy in another arena */
799
+ mkr_xml_doc_t *doc2 = mkr_xml_doc_new();
800
+ if (doc2 == NULL) { rc = 45; goto done; }
801
+ mkr_xml_node_t *imp = NULL;
802
+ int irc = mkr_xml_import_subtree(doc2, pr, &imp);
803
+ if (irc != MKR_XML_MUT_OK || imp == NULL || imp == pr
804
+ || !mkr_bytes_eq(imp->qname, imp->qname_len, "pr", 2)
805
+ || imp->first_child == NULL || imp->first_child == pr->first_child) { rc = 46; }
806
+ mkr_xml_doc_destroy(doc2);
807
+ if (rc != 0) goto done;
808
+
809
+ /* 18. single-root rule at the document level (pr is already the root) */
810
+ mkr_xml_node_t *root2 = NULL;
811
+ if (mkr_xml_new_element(doc, "root2", 5, &root2) != MKR_XML_MUT_OK) { rc = 47; goto done; }
812
+ if (mkr_xml_insert_child(doc, docn, root2) != MKR_XML_MUT_HIERARCHY) { rc = 48; goto done; }
813
+
814
+ done:
815
+ mkr_xml_doc_destroy(doc);
816
+ return rc;
817
+ }