makiri 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/conformance.yml +22 -0
- data/.github/workflows/libfuzzer.yml +83 -0
- data/.github/workflows/release.yml +12 -7
- data/.github/workflows/security.yml +88 -3
- data/.github/workflows/valgrind.yml +135 -0
- data/CHANGELOG.md +152 -15
- data/README.md +183 -13
- data/Rakefile +294 -7
- data/ext/makiri/bridge/bridge.h +28 -0
- data/ext/makiri/bridge/ruby_string.c +282 -12
- data/ext/makiri/core/mkr_alloc.c +40 -3
- data/ext/makiri/core/mkr_alloc.h +28 -5
- data/ext/makiri/core/mkr_buf.c +47 -3
- data/ext/makiri/core/mkr_buf.h +112 -3
- data/ext/makiri/core/mkr_core.c +143 -0
- data/ext/makiri/core/mkr_core.h +11 -2
- data/ext/makiri/core/mkr_hash.h +1 -1
- data/ext/makiri/core/mkr_span.h +186 -0
- data/ext/makiri/core/mkr_text.h +8 -8
- data/ext/makiri/core/mkr_utf8.c +101 -0
- data/ext/makiri/core/mkr_utf8.h +88 -0
- data/ext/makiri/extconf.rb +123 -10
- data/ext/makiri/fuzz/Makefile +95 -0
- data/ext/makiri/fuzz/check_fuzzer.cc +4 -0
- data/ext/makiri/fuzz/xml_fuzz.c +24 -0
- data/ext/makiri/fuzz/xpath_fuzz.c +109 -0
- data/ext/makiri/glue/glue.h +55 -11
- data/ext/makiri/glue/ruby_doc.c +129 -59
- data/ext/makiri/glue/ruby_html_css.c +292 -0
- data/ext/makiri/glue/{ruby_mutate.c → ruby_html_mutate.c} +248 -52
- data/ext/makiri/glue/ruby_html_node.c +859 -0
- data/ext/makiri/glue/ruby_html_serialize.c +154 -0
- data/ext/makiri/glue/ruby_node.c +74 -729
- data/ext/makiri/glue/ruby_node_set.c +167 -32
- data/ext/makiri/glue/ruby_xml.c +602 -0
- data/ext/makiri/glue/ruby_xml_node.c +1373 -0
- data/ext/makiri/glue/ruby_xpath.c +63 -30
- data/ext/makiri/glue/ruby_xpath.h +19 -0
- data/ext/makiri/lexbor_compat/compat.h +42 -9
- data/ext/makiri/lexbor_compat/compat_internal.h +1 -1
- data/ext/makiri/lexbor_compat/dom_index.c +2 -2
- data/ext/makiri/lexbor_compat/post_parse.c +100 -10
- data/ext/makiri/lexbor_compat/source_loc.c +15 -13
- data/ext/makiri/lexbor_compat/text_index.c +14 -8
- data/ext/makiri/lexbor_compat/utf8_input.c +19 -33
- data/ext/makiri/makiri.c +184 -6
- data/ext/makiri/makiri.h +43 -2
- data/ext/makiri/xml/mkr_xml.h +125 -0
- data/ext/makiri/xml/mkr_xml_chars.c +195 -0
- data/ext/makiri/xml/mkr_xml_index.c +169 -0
- data/ext/makiri/xml/mkr_xml_index.h +48 -0
- data/ext/makiri/xml/mkr_xml_mutate.c +817 -0
- data/ext/makiri/xml/mkr_xml_mutate.h +139 -0
- data/ext/makiri/xml/mkr_xml_node.c +399 -0
- data/ext/makiri/xml/mkr_xml_node.h +184 -0
- data/ext/makiri/xml/mkr_xml_tree.c +1515 -0
- data/ext/makiri/xpath/mkr_css.c +1023 -0
- data/ext/makiri/xpath/mkr_css.h +65 -0
- data/ext/makiri/xpath/mkr_xpath.c +96 -32
- data/ext/makiri/xpath/mkr_xpath.h +109 -4
- data/ext/makiri/xpath/mkr_xpath_engine_html.c +17 -0
- data/ext/makiri/xpath/mkr_xpath_engine_xml.c +12 -0
- data/ext/makiri/xpath/{mkr_xpath_eval.c → mkr_xpath_eval_body.h} +551 -241
- data/ext/makiri/xpath/{mkr_xpath_funcs.c → mkr_xpath_funcs_body.h} +318 -276
- data/ext/makiri/xpath/mkr_xpath_internal.h +177 -206
- data/ext/makiri/xpath/mkr_xpath_lex.c +95 -125
- data/ext/makiri/xpath/mkr_xpath_node_access_html.h +138 -0
- data/ext/makiri/xpath/mkr_xpath_node_access_xml.h +145 -0
- data/ext/makiri/xpath/mkr_xpath_number.c +109 -0
- data/ext/makiri/xpath/mkr_xpath_parse.c +83 -94
- data/ext/makiri/xpath/mkr_xpath_prelude_html.h +30 -0
- data/ext/makiri/xpath/mkr_xpath_prelude_xml.h +28 -0
- data/ext/makiri/xpath/mkr_xpath_shared.c +609 -0
- data/ext/makiri/xpath/mkr_xpath_value_body.h +801 -0
- data/ext/makiri/xpath/mkr_xpath_xml_selftest.c +76 -0
- data/lib/makiri/{attribute.rb → attr.rb} +7 -3
- data/lib/makiri/cdata_section.rb +19 -0
- data/lib/makiri/comment.rb +10 -0
- data/lib/makiri/compat_aliases.rb +30 -0
- data/lib/makiri/document.rb +9 -73
- data/lib/makiri/document_fragment.rb +14 -9
- data/lib/makiri/element.rb +4 -4
- data/lib/makiri/html/document.rb +106 -0
- data/lib/makiri/html/node_methods.rb +19 -0
- data/lib/makiri/html.rb +12 -0
- data/lib/makiri/node.rb +58 -15
- data/lib/makiri/node_set.rb +8 -0
- data/lib/makiri/processing_instruction.rb +10 -0
- data/lib/makiri/text.rb +1 -1
- data/lib/makiri/version.rb +1 -1
- data/lib/makiri/xml/builder.rb +263 -0
- data/lib/makiri/xml/document.rb +24 -0
- data/lib/makiri/xml/node_methods.rb +84 -0
- data/lib/makiri/xml.rb +10 -0
- data/lib/makiri/xpath_context.rb +1 -1
- data/lib/makiri.rb +24 -5
- data/script/build_native_gem.rb +2 -2
- data/script/check_alloc_failures.rb +266 -0
- data/script/check_c_safety.rb +77 -2
- data/script/check_c_safety_allowlist.yml +102 -0
- data/script/check_leaks.rb +64 -0
- data/script/leaks_harness.rb +64 -0
- data/vendor/lexbor/CMakeLists.txt +6 -0
- data/vendor/lexbor/README.md +12 -0
- data/vendor/lexbor/config.cmake +1 -1
- data/vendor/lexbor/source/lexbor/core/base.h +1 -1
- data/vendor/lexbor/source/lexbor/core/config.cmake +9 -1
- data/vendor/lexbor/source/lexbor/css/selectors/pseudo_state.c +2 -3
- data/vendor/lexbor/source/lexbor/css/selectors/state.c +3 -0
- data/vendor/lexbor/source/lexbor/dom/interfaces/element.c +21 -0
- data/vendor/lexbor/source/lexbor/dom/interfaces/element.h +5 -0
- data/vendor/lexbor/source/lexbor/encoding/decode.c +33 -4
- data/vendor/lexbor/source/lexbor/html/base.h +1 -1
- data/vendor/lexbor/source/lexbor/html/interfaces/select_element.c +4 -0
- data/vendor/lexbor/source/lexbor/html/serialize.c +545 -41
- data/vendor/lexbor/source/lexbor/html/serialize.h +2 -1
- data/vendor/lexbor/source/lexbor/html/tokenizer.h +2 -2
- data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/in_body.c +1 -1
- data/vendor/lexbor/source/lexbor/html/tree.c +6 -6
- data/vendor/lexbor/source/lexbor/selectors/selectors.c +12 -3
- data/vendor/lexbor/source/lexbor/url/base.h +1 -1
- data/vendor/lexbor/source/lexbor/url/url.c +5 -2
- data/vendor/lexbor/source/lexbor/url/url.h +9 -0
- data/vendor/lexbor/version +1 -1
- metadata +53 -9
- data/ext/makiri/glue/ruby_css.c +0 -185
- data/ext/makiri/glue/ruby_serialize.c +0 -92
- data/ext/makiri/xpath/mkr_xpath_value.c +0 -1286
- data/lib/makiri/cdata.rb +0 -6
|
@@ -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
|
+
}
|