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,1515 @@
|
|
|
1
|
+
/* mkr_xml_tree.c - XML tokenizer + tree builder (§14 steps 5-9).
|
|
2
|
+
*
|
|
3
|
+
* Ruby-free. Parses a decoded, validated-UTF-8 byte buffer (§2.1) into the
|
|
4
|
+
* custom node arena: elements, attributes, character data, namespaces (§7),
|
|
5
|
+
* entity/char-reference expansion (§9.1), comments / CDATA sections / processing
|
|
6
|
+
* instructions / the XML declaration (§9). A '<!DOCTYPE' is RECOGNIZED but its
|
|
7
|
+
* DTD is not processed (§9.4 alternative): the name + external id are kept (in an
|
|
8
|
+
* off-tree DOCUMENT_TYPE node, for Document#internal_subset) and the rest is
|
|
9
|
+
* skipped to its true '>'. NOTHING in the DTD is registered - no entity is
|
|
10
|
+
* defined (a DTD-defined &name; stays an undefined-entity error) and no external
|
|
11
|
+
* subset is fetched (zero I/O), so XXE / billion-laughs remain structurally
|
|
12
|
+
* impossible. Comments and PIs in the prolog/epilog (outside the root) are
|
|
13
|
+
* retained as children of the DOCUMENT node (the XPath data model; like
|
|
14
|
+
* Nokogiri); inter-construct whitespace there is not a text node. Strict
|
|
15
|
+
* NameStartChar/NameChar (§9.2b) and duplicate-attribute rejection (§9.3) apply.
|
|
16
|
+
*
|
|
17
|
+
* Budgets (§4): element depth (MKR_XML_MAX_DEPTH) and node/attr counts (enforced
|
|
18
|
+
* in the arena) are fail-closed - a violation aborts with no partial document.
|
|
19
|
+
*/
|
|
20
|
+
#include "mkr_xml.h"
|
|
21
|
+
#include "mkr_xml_node.h"
|
|
22
|
+
#include "../core/mkr_core.h"
|
|
23
|
+
|
|
24
|
+
#include <stdarg.h>
|
|
25
|
+
#include <stdio.h>
|
|
26
|
+
#include <stdlib.h>
|
|
27
|
+
#include <string.h>
|
|
28
|
+
|
|
29
|
+
/* A namespace binding in scope: prefix ("" for the default ns) -> URI. The
|
|
30
|
+
* prefix slice borrows the input (valid for the whole parse); the URI is
|
|
31
|
+
* arena-owned (nodes share it via ns_uri). */
|
|
32
|
+
typedef struct {
|
|
33
|
+
const char *pfx; uint32_t pfx_len;
|
|
34
|
+
const char *uri; uint32_t uri_len; /* uri_len 0 = "no namespace" (xmlns="") */
|
|
35
|
+
} ns_binding_t;
|
|
36
|
+
|
|
37
|
+
/* One attribute collected from a start tag before namespaces are resolved
|
|
38
|
+
* (xmlns declarations on the same element must be processed first). */
|
|
39
|
+
typedef struct {
|
|
40
|
+
const char *name; uint32_t name_len; /* raw QName slice (input) */
|
|
41
|
+
const char *val; uint32_t val_len; /* raw value slice (input, pre-expansion) */
|
|
42
|
+
} raw_attr_t;
|
|
43
|
+
|
|
44
|
+
typedef struct {
|
|
45
|
+
mkr_span_t in; /* bounded input reader (cursor + end; every
|
|
46
|
+
* read is bounds-checked by construction) */
|
|
47
|
+
const char *start; /* buffer origin (the only place the XML
|
|
48
|
+
* declaration is valid; compared, never read) */
|
|
49
|
+
uint32_t line, col; /* 1-based; col in bytes (§5) */
|
|
50
|
+
mkr_xml_doc_t *doc;
|
|
51
|
+
mkr_xml_node_t *fragment; /* NULL = parse a whole document; else parse a
|
|
52
|
+
* fragment INTO this DOCUMENT_FRAGMENT node:
|
|
53
|
+
* 0+ top-level nodes (char data, multiple
|
|
54
|
+
* elements; no XML decl / DOCTYPE / single-root
|
|
55
|
+
* rule) attach to it instead of the doc node */
|
|
56
|
+
mkr_xml_status_t status;
|
|
57
|
+
ns_binding_t *binds; size_t nbind, bind_cap; /* namespace scope stack */
|
|
58
|
+
raw_attr_t *ratt; size_t nratt, ratt_cap; /* reusable per-tag attr buffer */
|
|
59
|
+
mkr_xml_node_t **stack; size_t depth, scap; /* open-element stack */
|
|
60
|
+
size_t *frame; size_t fcap; /* parallel: ns-binding count when
|
|
61
|
+
* each open element was entered */
|
|
62
|
+
int saw_doctype; /* at most one DOCTYPE, prolog only */
|
|
63
|
+
} mkr_xml_parser_t;
|
|
64
|
+
|
|
65
|
+
#define XML_NS_URI "http://www.w3.org/XML/1998/namespace"
|
|
66
|
+
#define XMLNS_NS_URI "http://www.w3.org/2000/xmlns/"
|
|
67
|
+
#define LIT_LEN(s) ((uint32_t)(sizeof(s) - 1))
|
|
68
|
+
|
|
69
|
+
/* small leaf predicates defined later but used by earlier handlers */
|
|
70
|
+
static int is_space_byte(int c);
|
|
71
|
+
static int lit_ahead(const mkr_xml_parser_t *P, const char *lit, size_t n);
|
|
72
|
+
|
|
73
|
+
static void
|
|
74
|
+
set_syntax(mkr_xml_parser_t *P)
|
|
75
|
+
{
|
|
76
|
+
if (P->status == MKR_XML_OK) P->status = MKR_XML_ERR_SYNTAX;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Propagate an arena failure (node/attr/byte budget or OOM) recorded on the doc. */
|
|
80
|
+
static int
|
|
81
|
+
propagate_oom(mkr_xml_parser_t *P)
|
|
82
|
+
{
|
|
83
|
+
if (P->doc->oom != MKR_XML_OK) { P->status = P->doc->oom; return 1; }
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static void
|
|
88
|
+
advance(mkr_xml_parser_t *P)
|
|
89
|
+
{
|
|
90
|
+
int c = mkr_span_take(&P->in);
|
|
91
|
+
if (c < 0) return;
|
|
92
|
+
if (c == '\n') { P->line++; P->col = 1; }
|
|
93
|
+
else { P->col++; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Advance n bytes, keeping line/col correct (the consumed span may contain LF:
|
|
97
|
+
* comment/CDATA/PI content). Input is line-ending-normalized, so only LF appears. */
|
|
98
|
+
static void
|
|
99
|
+
advance_n(mkr_xml_parser_t *P, size_t n)
|
|
100
|
+
{
|
|
101
|
+
while (n-- > 0 && mkr_span_left(&P->in) > 0) advance(P);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static void
|
|
105
|
+
skip_ws(mkr_xml_parser_t *P)
|
|
106
|
+
{
|
|
107
|
+
for (;;) {
|
|
108
|
+
int c = mkr_span_peek(&P->in);
|
|
109
|
+
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') advance(P);
|
|
110
|
+
else break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Scan a Name into [*out, *out+*len) (a slice of the input), codepoint by
|
|
115
|
+
* codepoint against the full XML 1.0 §2.3 NameStartChar / NameChar sets (§9.2b):
|
|
116
|
+
* the first codepoint must be a NameStartChar, the rest NameChar. 0 on success. */
|
|
117
|
+
static int
|
|
118
|
+
scan_name(mkr_xml_parser_t *P, const char **out, uint32_t *len)
|
|
119
|
+
{
|
|
120
|
+
const char *start = mkr_span_mark(&P->in);
|
|
121
|
+
uint32_t cp;
|
|
122
|
+
int bl = mkr_utf8_decode1_span(&P->in, &cp);
|
|
123
|
+
if (bl == 0 || !mkr_xml_is_name_start(cp)) { set_syntax(P); return -1; }
|
|
124
|
+
advance_n(P, (size_t)bl);
|
|
125
|
+
for (;;) {
|
|
126
|
+
bl = mkr_utf8_decode1_span(&P->in, &cp);
|
|
127
|
+
if (bl == 0 || !mkr_xml_is_name_char(cp)) break;
|
|
128
|
+
advance_n(P, (size_t)bl);
|
|
129
|
+
}
|
|
130
|
+
size_t n = mkr_span_since(&P->in, start);
|
|
131
|
+
if (n > UINT32_MAX) { P->status = MKR_XML_ERR_LIMIT; return -1; } /* fail closed, never truncate */
|
|
132
|
+
*out = start;
|
|
133
|
+
*len = (uint32_t)n;
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static void
|
|
138
|
+
append_child(mkr_xml_node_t *parent, mkr_xml_node_t *child)
|
|
139
|
+
{
|
|
140
|
+
child->parent = parent;
|
|
141
|
+
if (parent->last_child) { parent->last_child->next = child; child->prev = parent->last_child; }
|
|
142
|
+
else parent->first_child = child;
|
|
143
|
+
parent->last_child = child;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* Append a character-data node (TEXT or CDATA) whose value is the arena slice
|
|
147
|
+
* [val, val+len). Adjacent character data of the SAME type is coalesced into the
|
|
148
|
+
* preceding sibling (as libxml2's xmlAddChild does, and per the XPath data
|
|
149
|
+
* model's "as much character data as possible is grouped" rule) - so
|
|
150
|
+
* <![CDATA[a]]><![CDATA[b]]> is one CDATA node, not two. Different types stay
|
|
151
|
+
* separate (text vs CDATA remain distinct DOM nodes). 0 / -1 (P->status set). */
|
|
152
|
+
static int
|
|
153
|
+
append_chardata(mkr_xml_parser_t *P, mkr_xml_node_t *parent, uint8_t type,
|
|
154
|
+
const char *val, uint32_t len)
|
|
155
|
+
{
|
|
156
|
+
mkr_xml_node_t *last = parent->last_child;
|
|
157
|
+
if (last != NULL && last->type == type) {
|
|
158
|
+
size_t total = (size_t)last->value_len + len;
|
|
159
|
+
if (total > UINT32_MAX) { P->status = MKR_XML_ERR_LIMIT; return -1; }
|
|
160
|
+
/* concatenate via the bounded writer: total == the two parts exactly, so
|
|
161
|
+
* it never overflows; the writer just removes the raw-pointer arithmetic. */
|
|
162
|
+
mkr_spanbuf_t b = mkr_xml_arena_spanbuf(P->doc, total);
|
|
163
|
+
mkr_spanbuf_write(&b, last->value, last->value_len);
|
|
164
|
+
mkr_spanbuf_write(&b, val, len);
|
|
165
|
+
const char *buf = mkr_spanbuf_finish(&b);
|
|
166
|
+
if (buf == NULL) { propagate_oom(P); return -1; } /* alloc failure */
|
|
167
|
+
last->value = buf; last->value_len = (uint32_t)b.pos;
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
mkr_xml_node_t *n = mkr_xml_arena_node(P->doc, type);
|
|
171
|
+
if (n == NULL) { propagate_oom(P); return -1; }
|
|
172
|
+
n->value = val; n->value_len = len;
|
|
173
|
+
append_child(parent, n);
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
/* Copy a slice into the arena; on failure record the arena status. NULL return
|
|
179
|
+
* with len>0 means budget/OOM (P->status set); len==0 yields "". */
|
|
180
|
+
static const char *
|
|
181
|
+
own(mkr_xml_parser_t *P, const char *src, uint32_t len)
|
|
182
|
+
{
|
|
183
|
+
const char *p = mkr_xml_arena_bytes(P->doc, src, len);
|
|
184
|
+
if (p == NULL && len > 0) propagate_oom(P);
|
|
185
|
+
return p;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* Store an element/attribute's raw "prefix:local" QName as ONE arena copy, with
|
|
189
|
+
* local and prefix slicing into it. Keeping the qualified name contiguous lets
|
|
190
|
+
* Node#name / the XPath name() borrow it without rebuilding it. +loc+/+pl+ come
|
|
191
|
+
* from split_qname (loc points into +name+). 0 on success, -1 on OOM. Thin
|
|
192
|
+
* wrapper over the shared mkr_xml_qname_assign (one arena-copy rule with the
|
|
193
|
+
* mutators), propagating an OOM to P->status. */
|
|
194
|
+
static int
|
|
195
|
+
set_node_qname(mkr_xml_parser_t *P, mkr_xml_node_t *node, const char *name,
|
|
196
|
+
uint32_t nl, const char *loc, uint32_t ll, uint32_t pl)
|
|
197
|
+
{
|
|
198
|
+
mkr_xml_qname_t qn = { name, nl, name, pl, loc, ll };
|
|
199
|
+
if (mkr_xml_qname_assign(P->doc, node, &qn) != 0) { propagate_oom(P); return -1; }
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
static int
|
|
204
|
+
slice_eq(const char *a, uint32_t al, const char *b, uint32_t bl)
|
|
205
|
+
{
|
|
206
|
+
return mkr_bytes_eq(a, al, b, bl);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Split a QName into prefix:local. 0 on success (no-colon names get prefix_len
|
|
210
|
+
* 0); -1 if malformed (>1 colon, or an empty prefix/local). */
|
|
211
|
+
static int
|
|
212
|
+
split_qname(const char *name, uint32_t len, const char **pfx, uint32_t *pfx_len,
|
|
213
|
+
const char **loc, uint32_t *loc_len)
|
|
214
|
+
{
|
|
215
|
+
mkr_span_t s = mkr_span(name, len);
|
|
216
|
+
size_t colon_at;
|
|
217
|
+
if (!mkr_span_find(&s, ':', &colon_at)) {
|
|
218
|
+
*pfx = name; *pfx_len = 0; *loc = name; *loc_len = len; return 0;
|
|
219
|
+
}
|
|
220
|
+
uint32_t pl = (uint32_t)colon_at;
|
|
221
|
+
const char *ls = name + colon_at + 1;
|
|
222
|
+
uint32_t ll = len - pl - 1;
|
|
223
|
+
if (pl == 0 || ll == 0) return -1; /* ":x" or "x:" */
|
|
224
|
+
mkr_span_t lsp = mkr_span(ls, ll);
|
|
225
|
+
size_t second;
|
|
226
|
+
if (mkr_span_find(&lsp, ':', &second)) return -1; /* second colon */
|
|
227
|
+
/* §9.2b: prefix and local must each be an NCName. scan_name already proved the
|
|
228
|
+
* whole QName is NameStartChar + NameChar* and split removed the only colon, so
|
|
229
|
+
* the one remaining check is that the local part starts with a NameStartChar
|
|
230
|
+
* (the prefix starts the QName, so it already does). Rejects e.g. "a:1b". */
|
|
231
|
+
uint32_t cp;
|
|
232
|
+
if (mkr_utf8_decode1_span(&lsp, &cp) == 0 || !mkr_xml_is_name_start(cp)) return -1;
|
|
233
|
+
*pfx = name; *pfx_len = pl; *loc = ls; *loc_len = ll;
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Resolve a prefix to its in-scope namespace URI, or NULL when unbound. The
|
|
238
|
+
* predefined 'xml' prefix always resolves; for the empty prefix this returns the
|
|
239
|
+
* default-namespace URI (possibly "" with *uri_len 0 = undeclared) or NULL. */
|
|
240
|
+
static const char *
|
|
241
|
+
ns_lookup(mkr_xml_parser_t *P, const char *pfx, uint32_t pfx_len, uint32_t *uri_len)
|
|
242
|
+
{
|
|
243
|
+
if (slice_eq(pfx, pfx_len, "xml", 3)) { *uri_len = LIT_LEN(XML_NS_URI); return XML_NS_URI; }
|
|
244
|
+
for (size_t i = P->nbind; i > 0; i--) {
|
|
245
|
+
ns_binding_t *b = &P->binds[i - 1];
|
|
246
|
+
if (slice_eq(b->pfx, b->pfx_len, pfx, pfx_len)) { *uri_len = b->uri_len; return b->uri; }
|
|
247
|
+
}
|
|
248
|
+
return NULL;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
static int
|
|
252
|
+
push_binding(mkr_xml_parser_t *P, const char *pfx, uint32_t pfx_len,
|
|
253
|
+
const char *uri, uint32_t uri_len)
|
|
254
|
+
{
|
|
255
|
+
if (P->nbind + 1 > MKR_XML_MAX_NS) { P->status = MKR_XML_ERR_LIMIT; return -1; }
|
|
256
|
+
if (mkr_grow_reserve((void **)&P->binds, &P->bind_cap, P->nbind + 1, sizeof(*P->binds)) != MKR_OK) {
|
|
257
|
+
P->status = MKR_XML_ERR_OOM; return -1;
|
|
258
|
+
}
|
|
259
|
+
P->binds[P->nbind].pfx = pfx; P->binds[P->nbind].pfx_len = pfx_len;
|
|
260
|
+
P->binds[P->nbind].uri = uri; P->binds[P->nbind].uri_len = uri_len;
|
|
261
|
+
P->nbind++;
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* Parse a start tag's attributes and close, resolving namespaces (§7). Collects
|
|
266
|
+
* the attributes, processes xmlns declarations into scope bindings, resolves the
|
|
267
|
+
* element's own namespace, then creates the attribute nodes (xmlns declarations
|
|
268
|
+
* kept as DOM attributes, §7.2). On '>' sets *pushed; on '/>' leaves a leaf.
|
|
269
|
+
* The element's prefix/local are already set by the caller; bindings pushed here
|
|
270
|
+
* are popped by the caller when the element closes. Returns 0 / -1. */
|
|
271
|
+
static int
|
|
272
|
+
parse_element_body(mkr_xml_parser_t *P, mkr_xml_node_t *el, int *pushed)
|
|
273
|
+
{
|
|
274
|
+
*pushed = 0;
|
|
275
|
+
P->nratt = 0;
|
|
276
|
+
|
|
277
|
+
/* Phase 1: scan all attributes + the tag close into the raw buffer. */
|
|
278
|
+
for (;;) {
|
|
279
|
+
skip_ws(P);
|
|
280
|
+
int c = mkr_span_peek(&P->in);
|
|
281
|
+
if (c < 0) { set_syntax(P); return -1; } /* unterminated tag */
|
|
282
|
+
if (c == '>') { advance(P); *pushed = 1; break; }
|
|
283
|
+
if (c == '/') {
|
|
284
|
+
advance(P);
|
|
285
|
+
if (mkr_span_peek(&P->in) != '>') { set_syntax(P); return -1; }
|
|
286
|
+
advance(P); break; /* self-closing */
|
|
287
|
+
}
|
|
288
|
+
const char *an; uint32_t alen;
|
|
289
|
+
if (scan_name(P, &an, &alen) != 0) return -1;
|
|
290
|
+
skip_ws(P);
|
|
291
|
+
if (mkr_span_peek(&P->in) != '=') { set_syntax(P); return -1; }
|
|
292
|
+
advance(P); skip_ws(P);
|
|
293
|
+
int q = mkr_span_peek(&P->in);
|
|
294
|
+
if (q != '"' && q != '\'') { set_syntax(P); return -1; }
|
|
295
|
+
advance(P);
|
|
296
|
+
const char *vs = mkr_span_mark(&P->in);
|
|
297
|
+
for (;;) {
|
|
298
|
+
int vc = mkr_span_peek(&P->in);
|
|
299
|
+
if (vc < 0 || vc == q) break;
|
|
300
|
+
if (vc == '<') { set_syntax(P); return -1; } /* raw '<' in AttValue */
|
|
301
|
+
advance(P);
|
|
302
|
+
}
|
|
303
|
+
if (mkr_span_left(&P->in) == 0) { set_syntax(P); return -1; } /* unterminated value */
|
|
304
|
+
size_t vraw = mkr_span_since(&P->in, vs);
|
|
305
|
+
if (vraw > UINT32_MAX) { P->status = MKR_XML_ERR_LIMIT; return -1; } /* fail closed */
|
|
306
|
+
uint32_t vlen = (uint32_t)vraw;
|
|
307
|
+
advance(P); /* closing quote */
|
|
308
|
+
/* §3.1: attributes are S-separated - after a value the next byte must be
|
|
309
|
+
* whitespace or the tag close ('>' / '/'), never another name. Rejects
|
|
310
|
+
* `<a x="1"y="2"/>`. */
|
|
311
|
+
int nx = mkr_span_peek(&P->in);
|
|
312
|
+
if (nx >= 0 && nx != '>' && nx != '/' && !is_space_byte(nx)) {
|
|
313
|
+
set_syntax(P); return -1;
|
|
314
|
+
}
|
|
315
|
+
if (P->nratt + 1 > MKR_XML_MAX_ATTRS) { P->status = MKR_XML_ERR_LIMIT; return -1; }
|
|
316
|
+
if (mkr_grow_reserve((void **)&P->ratt, &P->ratt_cap, P->nratt + 1, sizeof(*P->ratt)) != MKR_OK) {
|
|
317
|
+
P->status = MKR_XML_ERR_OOM; return -1;
|
|
318
|
+
}
|
|
319
|
+
P->ratt[P->nratt].name = an; P->ratt[P->nratt].name_len = alen;
|
|
320
|
+
P->ratt[P->nratt].val = vs; P->ratt[P->nratt].val_len = vlen;
|
|
321
|
+
P->nratt++;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/* Phase 2: xmlns declarations -> namespace bindings (§7.1 reserved rules). */
|
|
325
|
+
for (size_t i = 0; i < P->nratt; i++) {
|
|
326
|
+
raw_attr_t *r = &P->ratt[i];
|
|
327
|
+
const char *bpfx; uint32_t bpl;
|
|
328
|
+
if (!mkr_xml_xmlns_prefix(r->name, r->name_len, &bpfx, &bpl)) continue;
|
|
329
|
+
if (slice_eq(bpfx, bpl, "xmlns", 5)) { set_syntax(P); return -1; } /* xmlns:xmlns reserved */
|
|
330
|
+
|
|
331
|
+
uint32_t ulen;
|
|
332
|
+
const char *uri = mkr_xml_expand(P->doc, r->val, r->val_len,
|
|
333
|
+
MKR_XML_EXPAND_ATTR, &ulen, &P->status);
|
|
334
|
+
if (uri == NULL) return -1;
|
|
335
|
+
if (slice_eq(bpfx, bpl, "xml", 3)) {
|
|
336
|
+
if (!slice_eq(uri, ulen, XML_NS_URI, LIT_LEN(XML_NS_URI))) { set_syntax(P); return -1; }
|
|
337
|
+
} else if (slice_eq(uri, ulen, XML_NS_URI, LIT_LEN(XML_NS_URI))
|
|
338
|
+
|| slice_eq(uri, ulen, XMLNS_NS_URI, LIT_LEN(XMLNS_NS_URI))) {
|
|
339
|
+
set_syntax(P); return -1; /* reserved URI bound to another prefix */
|
|
340
|
+
}
|
|
341
|
+
if (bpl > 0 && ulen == 0) { set_syntax(P); return -1; } /* xmlns:p="" (XML 1.0) */
|
|
342
|
+
if (push_binding(P, bpfx, bpl, uri, ulen) != 0) return -1;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* Phase 3: resolve the element's own namespace. */
|
|
346
|
+
if (el->prefix_len > 0) {
|
|
347
|
+
if (slice_eq(el->prefix, el->prefix_len, "xmlns", 5)) { set_syntax(P); return -1; }
|
|
348
|
+
uint32_t ulen;
|
|
349
|
+
const char *uri = ns_lookup(P, el->prefix, el->prefix_len, &ulen);
|
|
350
|
+
if (uri == NULL) { set_syntax(P); return -1; } /* unbound prefix */
|
|
351
|
+
el->ns_uri = uri; el->ns_uri_len = ulen;
|
|
352
|
+
} else {
|
|
353
|
+
uint32_t ulen;
|
|
354
|
+
const char *uri = ns_lookup(P, "", 0, &ulen);
|
|
355
|
+
if (uri != NULL && ulen > 0) { el->ns_uri = uri; el->ns_uri_len = ulen; }
|
|
356
|
+
/* else: no default namespace -> the element is in no namespace */
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* Phase 4: create the attribute nodes (xmlns declarations kept, §7.2). The
|
|
360
|
+
* list is built with an explicit tail so each append is O(1) - walking to the
|
|
361
|
+
* tail per attribute would be O(n^2) over a MKR_XML_MAX_ATTRS-bounded set. */
|
|
362
|
+
mkr_xml_node_t *attr_tail = NULL;
|
|
363
|
+
for (size_t i = 0; i < P->nratt; i++) {
|
|
364
|
+
raw_attr_t *r = &P->ratt[i];
|
|
365
|
+
const char *pfx, *loc; uint32_t pl, ll;
|
|
366
|
+
if (split_qname(r->name, r->name_len, &pfx, &pl, &loc, &ll) != 0) { set_syntax(P); return -1; }
|
|
367
|
+
|
|
368
|
+
mkr_xml_node_t *attr = mkr_xml_arena_node(P->doc, MKR_XML_NODE_TYPE_ATTRIBUTE);
|
|
369
|
+
if (attr == NULL) { propagate_oom(P); return -1; }
|
|
370
|
+
(void)pfx;
|
|
371
|
+
if (set_node_qname(P, attr, r->name, r->name_len, loc, ll, pl) != 0) return -1;
|
|
372
|
+
|
|
373
|
+
if (mkr_xml_xmlns_prefix(r->name, r->name_len, NULL, NULL)) {
|
|
374
|
+
attr->ns_uri = XMLNS_NS_URI; attr->ns_uri_len = LIT_LEN(XMLNS_NS_URI);
|
|
375
|
+
} else if (pl > 0) { /* prefixed attribute */
|
|
376
|
+
uint32_t ulen;
|
|
377
|
+
const char *uri = ns_lookup(P, pfx, pl, &ulen);
|
|
378
|
+
if (uri == NULL) { set_syntax(P); return -1; } /* unbound prefix */
|
|
379
|
+
attr->ns_uri = uri; attr->ns_uri_len = ulen;
|
|
380
|
+
}
|
|
381
|
+
/* else: unprefixed attribute -> no namespace (the default ns never applies) */
|
|
382
|
+
|
|
383
|
+
attr->value = mkr_xml_expand(P->doc, r->val, r->val_len,
|
|
384
|
+
MKR_XML_EXPAND_ATTR, &attr->value_len, &P->status);
|
|
385
|
+
if (attr->value == NULL) return -1;
|
|
386
|
+
attr->parent = el; /* O(1) tail append */
|
|
387
|
+
if (attr_tail) attr_tail->next = attr; else el->attrs = attr;
|
|
388
|
+
attr_tail = attr;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/* §9.3: no two attributes may share the same (namespace URI, local name) -
|
|
392
|
+
* compared AFTER resolution, so "a:x" and "b:x" bound to the same URI collide
|
|
393
|
+
* while "xmlns:a"/"xmlns:b" (same xmlns URI, different local) do not. O(n^2)
|
|
394
|
+
* over a per-element set bounded by MKR_XML_MAX_ATTRS. */
|
|
395
|
+
for (mkr_xml_node_t *a = el->attrs; a != NULL; a = a->next) {
|
|
396
|
+
for (mkr_xml_node_t *b = a->next; b != NULL; b = b->next) {
|
|
397
|
+
if (slice_eq(a->local, a->local_len, b->local, b->local_len)
|
|
398
|
+
&& slice_eq(a->ns_uri, a->ns_uri_len, b->ns_uri, b->ns_uri_len)) {
|
|
399
|
+
set_syntax(P); return -1; /* duplicate attribute */
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return 0;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* True if the remaining input begins with the literal [lit, lit+n). */
|
|
407
|
+
static int
|
|
408
|
+
lit_ahead(const mkr_xml_parser_t *P, const char *lit, size_t n)
|
|
409
|
+
{
|
|
410
|
+
return mkr_span_starts(&P->in, lit, n);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* Takes the int-or-minus-one a span peek returns (-1 / end-of-input is never a
|
|
414
|
+
* space, so a "peek is space" test needs no separate end check). */
|
|
415
|
+
static int
|
|
416
|
+
is_space_byte(int c)
|
|
417
|
+
{
|
|
418
|
+
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* '<!--' comment (P->p at '!'). XML 1.0: the content is XML Char and may not
|
|
422
|
+
* contain '--' except as part of the closing '-->'. Creates a COMMENT node under
|
|
423
|
+
* +parent+ - its open element, or the DOCUMENT node in the prolog/epilog (XPath
|
|
424
|
+
* data model; like Nokogiri). */
|
|
425
|
+
static int
|
|
426
|
+
parse_comment(mkr_xml_parser_t *P, mkr_xml_node_t *parent)
|
|
427
|
+
{
|
|
428
|
+
advance_n(P, 3); /* consume "!--" */
|
|
429
|
+
const char *cstart = mkr_span_mark(&P->in);
|
|
430
|
+
mkr_span_t s = P->in; /* scan copy: looks ahead, consumes nothing */
|
|
431
|
+
for (;;) {
|
|
432
|
+
size_t at;
|
|
433
|
+
if (!mkr_span_find(&s, '-', &at)) { set_syntax(P); return -1; } /* unterminated */
|
|
434
|
+
mkr_span_skip(&s, at); /* cursor on the '-' */
|
|
435
|
+
if (mkr_span_at(&s, 1) == '-') {
|
|
436
|
+
if (mkr_span_at(&s, 2) == '>') break; /* cursor on the closing "-->" */
|
|
437
|
+
set_syntax(P); return -1; /* '--' not part of '-->' */
|
|
438
|
+
}
|
|
439
|
+
mkr_span_skip(&s, 1);
|
|
440
|
+
}
|
|
441
|
+
size_t craw = mkr_span_since(&s, cstart);
|
|
442
|
+
if (craw > UINT32_MAX) { P->status = MKR_XML_ERR_LIMIT; return -1; }
|
|
443
|
+
uint32_t clen = (uint32_t)craw;
|
|
444
|
+
if (mkr_xml_validate_chars(cstart, clen) != 0) { set_syntax(P); return -1; }
|
|
445
|
+
if (parent != NULL) {
|
|
446
|
+
mkr_xml_node_t *c = mkr_xml_arena_node(P->doc, MKR_XML_NODE_TYPE_COMMENT);
|
|
447
|
+
if (c == NULL) { propagate_oom(P); return -1; }
|
|
448
|
+
c->value = own(P, cstart, clen); c->value_len = clen;
|
|
449
|
+
if (clen > 0 && c->value == NULL) return -1;
|
|
450
|
+
append_child(parent, c);
|
|
451
|
+
}
|
|
452
|
+
advance_n(P, craw + 3); /* content + "-->" */
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/* '<![CDATA[' section (P->p at '!'). Raw character data (no reference recognition)
|
|
457
|
+
* up to ']]>'. Character data is only well-formed inside an element. */
|
|
458
|
+
static int
|
|
459
|
+
parse_cdata(mkr_xml_parser_t *P, mkr_xml_node_t *parent)
|
|
460
|
+
{
|
|
461
|
+
if (P->depth == 0 && P->fragment == NULL) { set_syntax(P); return -1; } /* CDATA outside the root */
|
|
462
|
+
advance_n(P, 8); /* consume "![CDATA[" */
|
|
463
|
+
const char *cstart = mkr_span_mark(&P->in);
|
|
464
|
+
mkr_span_t s = P->in; /* scan copy */
|
|
465
|
+
for (;;) {
|
|
466
|
+
size_t at;
|
|
467
|
+
if (!mkr_span_find(&s, ']', &at)) { set_syntax(P); return -1; } /* unterminated */
|
|
468
|
+
mkr_span_skip(&s, at); /* cursor on the ']' */
|
|
469
|
+
if (mkr_span_at(&s, 1) == ']' && mkr_span_at(&s, 2) == '>') break; /* cursor on "]]>" */
|
|
470
|
+
mkr_span_skip(&s, 1);
|
|
471
|
+
}
|
|
472
|
+
size_t craw = mkr_span_since(&s, cstart);
|
|
473
|
+
if (craw > UINT32_MAX) { P->status = MKR_XML_ERR_LIMIT; return -1; }
|
|
474
|
+
uint32_t clen = (uint32_t)craw;
|
|
475
|
+
if (mkr_xml_validate_chars(cstart, clen) != 0) { set_syntax(P); return -1; }
|
|
476
|
+
const char *cval = own(P, cstart, clen);
|
|
477
|
+
if (clen > 0 && cval == NULL) return -1;
|
|
478
|
+
if (append_chardata(P, parent, MKR_XML_NODE_TYPE_CDATA_SECTION, cval, clen) != 0) return -1;
|
|
479
|
+
advance_n(P, craw + 3); /* content + "]]>" */
|
|
480
|
+
return 0;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/* ---- XML declaration (§2.8 / §4.3.1) strict pseudo-attribute grammar ---- */
|
|
484
|
+
|
|
485
|
+
/* match the literal keyword [kw, kw+n) at the cursor and consume it; 0/1. */
|
|
486
|
+
static int
|
|
487
|
+
eat_keyword(mkr_xml_parser_t *P, const char *kw, size_t n)
|
|
488
|
+
{
|
|
489
|
+
if (!mkr_span_starts(&P->in, kw, n)) return 0;
|
|
490
|
+
advance_n(P, n);
|
|
491
|
+
return 1;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/* Eq ::= S? '=' S? - 0 on success, -1 if '=' is missing. */
|
|
495
|
+
static int
|
|
496
|
+
decl_eq(mkr_xml_parser_t *P)
|
|
497
|
+
{
|
|
498
|
+
skip_ws(P);
|
|
499
|
+
if (mkr_span_peek(&P->in) != '=') { set_syntax(P); return -1; }
|
|
500
|
+
advance(P);
|
|
501
|
+
skip_ws(P);
|
|
502
|
+
return 0;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/* Read a quoted pseudo-attribute value into [*out, *out+*out_len). 0 on success,
|
|
506
|
+
* -1 on a missing/mismatched quote. */
|
|
507
|
+
static int
|
|
508
|
+
decl_value_get(mkr_xml_parser_t *P, const char **out, uint32_t *out_len)
|
|
509
|
+
{
|
|
510
|
+
int qch = mkr_span_peek(&P->in);
|
|
511
|
+
if (qch != '"' && qch != '\'') { set_syntax(P); return -1; }
|
|
512
|
+
advance(P);
|
|
513
|
+
const char *vs = mkr_span_mark(&P->in);
|
|
514
|
+
while (mkr_span_peek(&P->in) >= 0 && mkr_span_peek(&P->in) != qch) advance(P);
|
|
515
|
+
if (mkr_span_left(&P->in) == 0) { set_syntax(P); return -1; } /* unterminated / mismatched quote */
|
|
516
|
+
size_t n = mkr_span_since(&P->in, vs);
|
|
517
|
+
if (n > UINT32_MAX) { P->status = MKR_XML_ERR_LIMIT; return -1; } /* fail closed */
|
|
518
|
+
*out = vs; *out_len = (uint32_t)n;
|
|
519
|
+
advance(P); /* closing quote */
|
|
520
|
+
return 0;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/* A quoted pseudo-attribute value, validated whole by +ok+. 0 on success. */
|
|
524
|
+
static int
|
|
525
|
+
decl_value(mkr_xml_parser_t *P, int (*ok)(const char *, uint32_t))
|
|
526
|
+
{
|
|
527
|
+
const char *vs; uint32_t vl;
|
|
528
|
+
if (decl_value_get(P, &vs, &vl) != 0) return -1;
|
|
529
|
+
if (!ok(vs, vl)) { set_syntax(P); return -1; }
|
|
530
|
+
return 0;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
static int is_version_num(const char *s, uint32_t n) { /* '1.' [0-9]+ */
|
|
534
|
+
mkr_span_t v = mkr_span(s, n);
|
|
535
|
+
if (n < 3 || !mkr_span_starts(&v, "1.", 2)) return 0;
|
|
536
|
+
for (uint32_t i = 2; i < n; i++) {
|
|
537
|
+
int c = mkr_span_at(&v, i);
|
|
538
|
+
if (c < '0' || c > '9') return 0;
|
|
539
|
+
}
|
|
540
|
+
return 1;
|
|
541
|
+
}
|
|
542
|
+
static int is_enc_name(const char *s, uint32_t n) { /* [A-Za-z] ([A-Za-z0-9._] | '-')* */
|
|
543
|
+
mkr_span_t v = mkr_span(s, n);
|
|
544
|
+
int c0 = mkr_span_peek(&v);
|
|
545
|
+
if (!((c0 >= 'A' && c0 <= 'Z') || (c0 >= 'a' && c0 <= 'z'))) return 0;
|
|
546
|
+
for (uint32_t i = 1; i < n; i++) {
|
|
547
|
+
int c = mkr_span_at(&v, i);
|
|
548
|
+
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')
|
|
549
|
+
|| c == '.' || c == '_' || c == '-')) return 0;
|
|
550
|
+
}
|
|
551
|
+
return 1;
|
|
552
|
+
}
|
|
553
|
+
static int is_yes_no(const char *s, uint32_t n) {
|
|
554
|
+
return mkr_bytes_eq(s, n, "yes", 3) || mkr_bytes_eq(s, n, "no", 2);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/* '<?xml' already consumed (cursor just past "xml"). Validate
|
|
558
|
+
* S 'version' Eq VersionNum (S 'encoding' Eq EncName)? (S 'standalone' Eq ('yes'|'no'))? S? '?>'
|
|
559
|
+
* - pseudo-attributes are lowercase, in this order, each at most once, S-separated.
|
|
560
|
+
* The declaration is not retained as a node. 0 on success / -1 (P->status set). */
|
|
561
|
+
static int
|
|
562
|
+
parse_xml_decl_body(mkr_xml_parser_t *P)
|
|
563
|
+
{
|
|
564
|
+
if (!is_space_byte(mkr_span_peek(&P->in))) { set_syntax(P); return -1; } /* S */
|
|
565
|
+
skip_ws(P);
|
|
566
|
+
if (!eat_keyword(P, "version", 7)) { set_syntax(P); return -1; }
|
|
567
|
+
if (decl_eq(P) != 0) return -1;
|
|
568
|
+
const char *ver; uint32_t ver_len;
|
|
569
|
+
if (decl_value_get(P, &ver, &ver_len) != 0) return -1;
|
|
570
|
+
if (!is_version_num(ver, ver_len)) { set_syntax(P); return -1; } /* bad VersionNum syntax */
|
|
571
|
+
if (!mkr_bytes_eq(ver, ver_len, "1.0", 3)) {
|
|
572
|
+
/* well-formed, but a version Makiri does not implement (XML 1.1 / 1.x):
|
|
573
|
+
* fail closed rather than silently parse it under XML 1.0 rules. */
|
|
574
|
+
P->status = MKR_XML_ERR_VERSION; return -1;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
int saw_enc = 0, saw_sd = 0;
|
|
578
|
+
for (;;) {
|
|
579
|
+
int had_s = 0;
|
|
580
|
+
while (is_space_byte(mkr_span_peek(&P->in))) { advance(P); had_s = 1; }
|
|
581
|
+
if (lit_ahead(P, "?>", 2)) { advance_n(P, 2); return 0; } /* end of declaration */
|
|
582
|
+
if (!had_s) { set_syntax(P); return -1; } /* attrs need an S separator */
|
|
583
|
+
if (!saw_enc && !saw_sd && eat_keyword(P, "encoding", 8)) {
|
|
584
|
+
saw_enc = 1; /* encoding precedes standalone */
|
|
585
|
+
P->doc->has_encoding_decl = 1; /* serializer emits encoding="UTF-8" only then */
|
|
586
|
+
if (decl_eq(P) != 0 || decl_value(P, is_enc_name) != 0) return -1;
|
|
587
|
+
} else if (!saw_sd && eat_keyword(P, "standalone", 10)) {
|
|
588
|
+
saw_sd = 1;
|
|
589
|
+
if (decl_eq(P) != 0 || decl_value(P, is_yes_no) != 0) return -1;
|
|
590
|
+
} else {
|
|
591
|
+
set_syntax(P); return -1; /* unknown / duplicate / out-of-order pseudo-attribute */
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/* '<?' processing instruction (P->p at '?'). The '<?xml ...?>' declaration (exact
|
|
597
|
+
* lowercase target, document start only) is validated strictly by
|
|
598
|
+
* parse_xml_decl_body and not retained; a target matching "xml" in any OTHER case
|
|
599
|
+
* is a reserved PITarget (§2.6) and rejected. Every other PI is retained as a
|
|
600
|
+
* node - under its open element, or under the DOCUMENT node in the
|
|
601
|
+
* prolog/epilog (XPath data model; like Nokogiri). */
|
|
602
|
+
static int
|
|
603
|
+
parse_pi(mkr_xml_parser_t *P, mkr_xml_node_t *parent, int at_doc_start)
|
|
604
|
+
{
|
|
605
|
+
advance_n(P, 1); /* consume '?' */
|
|
606
|
+
const char *tgt; uint32_t tl;
|
|
607
|
+
if (scan_name(P, &tgt, &tl) != 0) return -1; /* PITarget (a Name) */
|
|
608
|
+
/* Namespaces in XML §3: a PITarget is an NCName, so a colon is not-wf under
|
|
609
|
+
* namespace processing (which Makiri always does, like Nokogiri::XML). */
|
|
610
|
+
mkr_span_t tsp = mkr_span(tgt, tl);
|
|
611
|
+
size_t colon_at;
|
|
612
|
+
if (mkr_span_find(&tsp, ':', &colon_at)) { set_syntax(P); return -1; }
|
|
613
|
+
int ci_xml = (tl == 3 && (mkr_span_at(&tsp, 0) | 0x20) == 'x'
|
|
614
|
+
&& (mkr_span_at(&tsp, 1) | 0x20) == 'm'
|
|
615
|
+
&& (mkr_span_at(&tsp, 2) | 0x20) == 'l');
|
|
616
|
+
int is_decl = (tl == 3 && mkr_span_starts(&tsp, "xml", 3));
|
|
617
|
+
if (is_decl) {
|
|
618
|
+
if (!at_doc_start || P->depth != 0) { set_syntax(P); return -1; } /* decl: doc start only */
|
|
619
|
+
return parse_xml_decl_body(P);
|
|
620
|
+
}
|
|
621
|
+
if (ci_xml) { set_syntax(P); return -1; } /* reserved target ("XML"/"xmL"/...) */
|
|
622
|
+
|
|
623
|
+
int empty_data = lit_ahead(P, "?>", 2);
|
|
624
|
+
if (!empty_data) {
|
|
625
|
+
if (!is_space_byte(mkr_span_peek(&P->in))) { set_syntax(P); return -1; } /* need S */
|
|
626
|
+
skip_ws(P);
|
|
627
|
+
}
|
|
628
|
+
const char *dstart = mkr_span_mark(&P->in);
|
|
629
|
+
mkr_span_t s = P->in; /* scan copy */
|
|
630
|
+
for (;;) {
|
|
631
|
+
size_t at;
|
|
632
|
+
if (!mkr_span_find(&s, '?', &at)) { set_syntax(P); return -1; } /* unterminated */
|
|
633
|
+
mkr_span_skip(&s, at); /* cursor on the '?' */
|
|
634
|
+
if (mkr_span_at(&s, 1) == '>') break; /* cursor on the closing "?>" */
|
|
635
|
+
mkr_span_skip(&s, 1);
|
|
636
|
+
}
|
|
637
|
+
size_t draw = mkr_span_since(&s, dstart);
|
|
638
|
+
if (draw > UINT32_MAX) { P->status = MKR_XML_ERR_LIMIT; return -1; }
|
|
639
|
+
uint32_t dlen = (uint32_t)draw;
|
|
640
|
+
if (mkr_xml_validate_chars(dstart, dlen) != 0) { set_syntax(P); return -1; }
|
|
641
|
+
|
|
642
|
+
if (parent != NULL) {
|
|
643
|
+
mkr_xml_node_t *pi = mkr_xml_arena_node(P->doc, MKR_XML_NODE_TYPE_PI);
|
|
644
|
+
if (pi == NULL) { propagate_oom(P); return -1; }
|
|
645
|
+
pi->local = own(P, tgt, tl); pi->local_len = tl;
|
|
646
|
+
pi->value = own(P, dstart, dlen); pi->value_len = dlen;
|
|
647
|
+
if (pi->local == NULL || (dlen > 0 && pi->value == NULL)) return -1;
|
|
648
|
+
append_child(parent, pi);
|
|
649
|
+
}
|
|
650
|
+
advance_n(P, draw + 2); /* data + "?>" */
|
|
651
|
+
return 0;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/* ---- per-construct handlers (the tokenizer dispatch calls one per token) ----
|
|
655
|
+
* Each takes only the parser (which now carries the open-element stack), returns
|
|
656
|
+
* 0 on success / -1 on a well-formedness or budget failure (P->status set), and
|
|
657
|
+
* leaves P->p just past the construct. */
|
|
658
|
+
|
|
659
|
+
/* The parent a node created at the cursor attaches to: the innermost open
|
|
660
|
+
* element, or - at the document level (prolog/epilog) - the DOCUMENT node, so a
|
|
661
|
+
* top-level comment / PI becomes its child (XPath data model; like Nokogiri).
|
|
662
|
+
* The document node exists from the start of the parse. */
|
|
663
|
+
static inline mkr_xml_node_t *
|
|
664
|
+
cur_parent(const mkr_xml_parser_t *P)
|
|
665
|
+
{
|
|
666
|
+
if (P->depth) return P->stack[P->depth - 1];
|
|
667
|
+
return P->fragment ? P->fragment : P->doc->doc_node;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/* End tag '</name S? >' (P->p at '/'): must match the innermost open element's
|
|
671
|
+
* raw QName; on match pops it and restores that element's namespace scope. */
|
|
672
|
+
static int
|
|
673
|
+
parse_end_tag(mkr_xml_parser_t *P)
|
|
674
|
+
{
|
|
675
|
+
advance(P); /* '/' */
|
|
676
|
+
const char *nm; uint32_t nl;
|
|
677
|
+
if (scan_name(P, &nm, &nl) != 0) return -1;
|
|
678
|
+
skip_ws(P);
|
|
679
|
+
if (mkr_span_peek(&P->in) != '>') { set_syntax(P); return -1; }
|
|
680
|
+
advance(P);
|
|
681
|
+
if (P->depth == 0) { set_syntax(P); return -1; } /* end tag with no open element */
|
|
682
|
+
mkr_xml_node_t *top = P->stack[P->depth - 1];
|
|
683
|
+
uint32_t tql = top->prefix_len ? top->prefix_len + 1 + top->local_len : top->local_len;
|
|
684
|
+
int match;
|
|
685
|
+
if (top->prefix_len) {
|
|
686
|
+
mkr_span_t nsp = mkr_span(nm, nl);
|
|
687
|
+
match = (nl == tql && mkr_span_starts(&nsp, top->prefix, top->prefix_len)
|
|
688
|
+
&& mkr_span_at(&nsp, top->prefix_len) == ':');
|
|
689
|
+
if (match) {
|
|
690
|
+
mkr_span_skip(&nsp, (size_t)top->prefix_len + 1);
|
|
691
|
+
match = mkr_span_starts(&nsp, top->local, top->local_len);
|
|
692
|
+
}
|
|
693
|
+
} else {
|
|
694
|
+
match = mkr_bytes_eq(top->local, top->local_len, nm, nl);
|
|
695
|
+
}
|
|
696
|
+
if (!match) { set_syntax(P); return -1; } /* mismatched end tag */
|
|
697
|
+
P->depth--;
|
|
698
|
+
P->nbind = P->frame[P->depth]; /* pop this element's namespace scope */
|
|
699
|
+
return 0;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/* A quoted literal ("..." or '...'); its content is a slice of the input (no
|
|
703
|
+
* reference recognition). 0 on success, -1 on a missing/unterminated quote. */
|
|
704
|
+
static int
|
|
705
|
+
parse_quoted(mkr_xml_parser_t *P, const char **out, uint32_t *out_len)
|
|
706
|
+
{
|
|
707
|
+
int q = mkr_span_peek(&P->in);
|
|
708
|
+
if (q != '"' && q != '\'') { set_syntax(P); return -1; }
|
|
709
|
+
advance(P);
|
|
710
|
+
const char *s = mkr_span_mark(&P->in);
|
|
711
|
+
while (mkr_span_peek(&P->in) >= 0 && mkr_span_peek(&P->in) != q) advance(P);
|
|
712
|
+
if (mkr_span_left(&P->in) == 0) { set_syntax(P); return -1; } /* unterminated literal */
|
|
713
|
+
size_t n = mkr_span_since(&P->in, s);
|
|
714
|
+
if (n > UINT32_MAX) { P->status = MKR_XML_ERR_LIMIT; return -1; }
|
|
715
|
+
*out = s; *out_len = (uint32_t)n;
|
|
716
|
+
advance(P); /* closing quote */
|
|
717
|
+
return 0;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/* '<!DOCTYPE' (P->p at '!'): the DTD is RECOGNIZED but NOT PROCESSED (§9.4
|
|
721
|
+
* alternative). Valid only in the prolog (before the root element) and at most
|
|
722
|
+
* once. The Name and the ExternalID (SYSTEM/PUBLIC literals) are read for
|
|
723
|
+
* Document#internal_subset, then any internal subset '[ ... ]' is skipped to the
|
|
724
|
+
* true '>' (quote state + bracket depth balance a '>' inside a quoted literal or
|
|
725
|
+
* a markup declaration). NOTHING in the DTD is processed: no entity is defined
|
|
726
|
+
* (so a DTD-defined &name; stays an undefined-entity error per §9.1, keeping XXE
|
|
727
|
+
* and billion-laughs impossible) and no external subset is fetched (zero I/O).
|
|
728
|
+
* The metadata is kept in an off-tree DOCUMENT_TYPE node (doc->doctype). */
|
|
729
|
+
static int
|
|
730
|
+
parse_doctype(mkr_xml_parser_t *P)
|
|
731
|
+
{
|
|
732
|
+
if (P->depth != 0 || P->doc->root != NULL || P->saw_doctype) {
|
|
733
|
+
set_syntax(P); return -1; /* inside an element, after the root, or a second DOCTYPE */
|
|
734
|
+
}
|
|
735
|
+
P->saw_doctype = 1;
|
|
736
|
+
advance_n(P, LIT_LEN("!DOCTYPE"));
|
|
737
|
+
if (!is_space_byte(mkr_span_peek(&P->in))) { set_syntax(P); return -1; } /* S required */
|
|
738
|
+
skip_ws(P);
|
|
739
|
+
const char *name; uint32_t name_len;
|
|
740
|
+
if (scan_name(P, &name, &name_len) != 0) return -1; /* the document type name (a Name) */
|
|
741
|
+
|
|
742
|
+
const char *pub = NULL, *sys = NULL;
|
|
743
|
+
uint32_t pub_len = 0, sys_len = 0;
|
|
744
|
+
|
|
745
|
+
if (is_space_byte(mkr_span_peek(&P->in))) {
|
|
746
|
+
skip_ws(P);
|
|
747
|
+
if (lit_ahead(P, "SYSTEM", 6)) {
|
|
748
|
+
advance_n(P, 6);
|
|
749
|
+
if (!is_space_byte(mkr_span_peek(&P->in))) { set_syntax(P); return -1; }
|
|
750
|
+
skip_ws(P);
|
|
751
|
+
if (parse_quoted(P, &sys, &sys_len) != 0) return -1;
|
|
752
|
+
} else if (lit_ahead(P, "PUBLIC", 6)) {
|
|
753
|
+
advance_n(P, 6);
|
|
754
|
+
if (!is_space_byte(mkr_span_peek(&P->in))) { set_syntax(P); return -1; }
|
|
755
|
+
skip_ws(P);
|
|
756
|
+
if (parse_quoted(P, &pub, &pub_len) != 0) return -1;
|
|
757
|
+
if (!is_space_byte(mkr_span_peek(&P->in))) { set_syntax(P); return -1; }
|
|
758
|
+
skip_ws(P);
|
|
759
|
+
if (parse_quoted(P, &sys, &sys_len) != 0) return -1;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/* Skip the optional internal subset + trailing whitespace to the true '>'. */
|
|
764
|
+
int quote = 0;
|
|
765
|
+
int depth = 0, closed = 0;
|
|
766
|
+
for (;;) {
|
|
767
|
+
int c = mkr_span_peek(&P->in);
|
|
768
|
+
if (c < 0) break;
|
|
769
|
+
if (quote) { if (c == quote) quote = 0; }
|
|
770
|
+
else if (c == '"' || c == '\'') { quote = c; }
|
|
771
|
+
else if (c == '[') { depth++; }
|
|
772
|
+
else if (c == ']') { if (depth > 0) depth--; }
|
|
773
|
+
else if (c == '>' && depth == 0) { advance(P); closed = 1; break; }
|
|
774
|
+
advance(P);
|
|
775
|
+
}
|
|
776
|
+
if (!closed) { set_syntax(P); return -1; } /* unterminated DOCTYPE */
|
|
777
|
+
|
|
778
|
+
/* Retain the metadata off-tree (not a child of any node, so XPath is
|
|
779
|
+
* unaffected) for Document#internal_subset. Fields are repurposed:
|
|
780
|
+
* local/qname = name, prefix = public/external id, value = system id. */
|
|
781
|
+
mkr_xml_node_t *dt = mkr_xml_arena_node(P->doc, MKR_XML_NODE_TYPE_DOCUMENT_TYPE);
|
|
782
|
+
if (dt == NULL) { propagate_oom(P); return -1; }
|
|
783
|
+
dt->local = dt->qname = own(P, name, name_len);
|
|
784
|
+
dt->local_len = dt->qname_len = name_len;
|
|
785
|
+
if (name_len > 0 && dt->local == NULL) return -1;
|
|
786
|
+
if (pub != NULL) {
|
|
787
|
+
dt->prefix = own(P, pub, pub_len); dt->prefix_len = pub_len;
|
|
788
|
+
if (pub_len > 0 && dt->prefix == NULL) return -1;
|
|
789
|
+
}
|
|
790
|
+
if (sys != NULL) {
|
|
791
|
+
dt->value = own(P, sys, sys_len); dt->value_len = sys_len;
|
|
792
|
+
if (sys_len > 0 && dt->value == NULL) return -1;
|
|
793
|
+
}
|
|
794
|
+
P->doc->doctype = dt;
|
|
795
|
+
return 0;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/* '<!' markup (P->p at '!'): a comment, a CDATA section, or a DOCTYPE (recognized
|
|
799
|
+
* but not processed, §9.4). Any other markup declaration is rejected. */
|
|
800
|
+
static int
|
|
801
|
+
parse_markup(mkr_xml_parser_t *P)
|
|
802
|
+
{
|
|
803
|
+
mkr_xml_node_t *parent = cur_parent(P);
|
|
804
|
+
if (lit_ahead(P, "!--", 3)) return parse_comment(P, parent);
|
|
805
|
+
if (lit_ahead(P, "![CDATA[", 8)) return parse_cdata(P, parent);
|
|
806
|
+
if (lit_ahead(P, "!DOCTYPE", 8)) {
|
|
807
|
+
if (P->fragment != NULL) { set_syntax(P); return -1; } /* a fragment has no DOCTYPE */
|
|
808
|
+
return parse_doctype(P);
|
|
809
|
+
}
|
|
810
|
+
set_syntax(P); return -1;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/* Start tag (P->p at the QName's first byte; +tl+/+tc+ = the '<' position).
|
|
814
|
+
* Builds the element, resolves its namespaces, links it into the tree, then on
|
|
815
|
+
* '>' pushes it onto the open-element stack or on '/>' leaves it a leaf. */
|
|
816
|
+
static int
|
|
817
|
+
parse_start_tag(mkr_xml_parser_t *P, uint32_t tl, uint32_t tc)
|
|
818
|
+
{
|
|
819
|
+
const char *nm; uint32_t nl;
|
|
820
|
+
if (scan_name(P, &nm, &nl) != 0) return -1;
|
|
821
|
+
const char *epfx, *eloc; uint32_t epl, ell;
|
|
822
|
+
if (split_qname(nm, nl, &epfx, &epl, &eloc, &ell) != 0) { set_syntax(P); return -1; }
|
|
823
|
+
(void)epfx;
|
|
824
|
+
mkr_xml_node_t *el = mkr_xml_arena_node(P->doc, MKR_XML_NODE_TYPE_ELEMENT);
|
|
825
|
+
if (el == NULL) { propagate_oom(P); return -1; }
|
|
826
|
+
if (set_node_qname(P, el, nm, nl, eloc, ell, epl) != 0) return -1;
|
|
827
|
+
el->line = tl; el->col = tc;
|
|
828
|
+
|
|
829
|
+
if (P->depth == 0 && P->fragment == NULL) {
|
|
830
|
+
if (P->doc->root != NULL) { set_syntax(P); return -1; } /* multiple roots */
|
|
831
|
+
P->doc->root = el;
|
|
832
|
+
} /* a fragment allows multiple top-level elements and sets no doc->root */
|
|
833
|
+
append_child(cur_parent(P), el); /* depth 0: the DOCUMENT (or fragment) node; else the open element */
|
|
834
|
+
|
|
835
|
+
size_t bind_base = P->nbind; /* xmlns on this element go above here */
|
|
836
|
+
int pushed = 0;
|
|
837
|
+
if (parse_element_body(P, el, &pushed) != 0) return -1;
|
|
838
|
+
if (pushed) {
|
|
839
|
+
if (P->depth + 1 > MKR_XML_MAX_DEPTH) { P->status = MKR_XML_ERR_LIMIT; return -1; }
|
|
840
|
+
if (mkr_grow_reserve((void **)&P->stack, &P->scap, P->depth + 1, sizeof(*P->stack)) != MKR_OK
|
|
841
|
+
|| mkr_grow_reserve((void **)&P->frame, &P->fcap, P->depth + 1, sizeof(*P->frame)) != MKR_OK) {
|
|
842
|
+
P->status = MKR_XML_ERR_OOM; return -1;
|
|
843
|
+
}
|
|
844
|
+
P->stack[P->depth] = el; P->frame[P->depth] = bind_base; P->depth++;
|
|
845
|
+
} else {
|
|
846
|
+
P->nbind = bind_base; /* self-closing: pop its namespace scope now */
|
|
847
|
+
}
|
|
848
|
+
return 0;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/* Character data up to the next '<'. Inside an element it becomes a TEXT node
|
|
852
|
+
* (references expanded, XML Char validated); at the document level only white
|
|
853
|
+
* space (misc) is permitted. */
|
|
854
|
+
static int
|
|
855
|
+
parse_text(mkr_xml_parser_t *P)
|
|
856
|
+
{
|
|
857
|
+
const char *tstart = mkr_span_mark(&P->in);
|
|
858
|
+
int nonspace = 0;
|
|
859
|
+
for (;;) {
|
|
860
|
+
int c = mkr_span_peek(&P->in);
|
|
861
|
+
if (c < 0 || c == '<') break;
|
|
862
|
+
/* §2.4: the literal "]]>" MUST NOT appear in character data - only a
|
|
863
|
+
* CDATA section (handled by parse_cdata) may end with it. A lone ']' or
|
|
864
|
+
* "]]" not followed by '>' is fine. */
|
|
865
|
+
if (c == ']' && lit_ahead(P, "]]>", 3)) { set_syntax(P); return -1; }
|
|
866
|
+
if (!(c == ' ' || c == '\t' || c == '\n' || c == '\r')) nonspace = 1;
|
|
867
|
+
advance(P);
|
|
868
|
+
}
|
|
869
|
+
size_t traw = mkr_span_since(&P->in, tstart);
|
|
870
|
+
if (traw > UINT32_MAX) { P->status = MKR_XML_ERR_LIMIT; return -1; } /* fail closed */
|
|
871
|
+
uint32_t tlen = (uint32_t)traw;
|
|
872
|
+
if (P->depth == 0 && P->fragment == NULL) {
|
|
873
|
+
if (nonspace) { set_syntax(P); return -1; } /* non-ws text outside any element */
|
|
874
|
+
return 0; /* document-level whitespace (misc): discarded */
|
|
875
|
+
}
|
|
876
|
+
/* expand char/entity references + validate XML Char (§9.1/§9.2). At the fragment
|
|
877
|
+
* top level (depth 0) char data is a child of the fragment node itself. */
|
|
878
|
+
uint32_t tvlen = 0;
|
|
879
|
+
const char *tval = mkr_xml_expand(P->doc, tstart, tlen, MKR_XML_EXPAND_TEXT, &tvlen, &P->status);
|
|
880
|
+
if (tval == NULL) return -1;
|
|
881
|
+
return append_chardata(P, cur_parent(P), MKR_XML_NODE_TYPE_TEXT, tval, tvlen);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/* Fold CRLF and a lone CR to LF over the whole input before tokenizing (§9.3b-A),
|
|
885
|
+
* so every later stage (attribute-value normalization, line counting) sees only
|
|
886
|
+
* LF. Only when a CR is actually present is the input copied into a freshly
|
|
887
|
+
* malloc'd scratch buffer (the common no-CR input tokenizes in place, zero copy);
|
|
888
|
+
* it can only shrink. On return, body+blen are the bytes to parse and norm is the
|
|
889
|
+
* scratch buffer the caller must free (NULL if none). Returns 0, or -1 on OOM. */
|
|
890
|
+
static int
|
|
891
|
+
normalize_newlines(const char *src, size_t len, const char **body, size_t *blen, char **norm)
|
|
892
|
+
{
|
|
893
|
+
*body = src; *blen = len; *norm = NULL;
|
|
894
|
+
mkr_span_t s = mkr_span(src, len);
|
|
895
|
+
size_t cr_at;
|
|
896
|
+
if (!mkr_span_find(&s, '\r', &cr_at)) return 0;
|
|
897
|
+
char *buf = mkr_reallocarray(NULL, len == 0 ? 1 : len, 1);
|
|
898
|
+
if (buf == NULL) return -1;
|
|
899
|
+
/* read through the span, write through the bounded writer (the output can
|
|
900
|
+
* only shrink, so `len` capacity always suffices - enforced, not trusted). */
|
|
901
|
+
mkr_spanbuf_t w = mkr_spanbuf(buf, len);
|
|
902
|
+
for (;;) {
|
|
903
|
+
int ch = mkr_span_take(&s);
|
|
904
|
+
if (ch < 0) break;
|
|
905
|
+
if (ch == '\r') {
|
|
906
|
+
mkr_spanbuf_putc(&w, '\n');
|
|
907
|
+
if (mkr_span_peek(&s) == '\n') mkr_span_skip(&s, 1); /* CRLF -> single LF */
|
|
908
|
+
} else {
|
|
909
|
+
mkr_spanbuf_putc(&w, (char)ch);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (mkr_spanbuf_finish(&w) == NULL) { free(buf); return -1; } /* can't happen; fail closed */
|
|
913
|
+
*body = buf; *blen = w.pos; *norm = buf;
|
|
914
|
+
return 0;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/* Tokenizer dispatch: classify the construct at P->p and hand it to its handler.
|
|
918
|
+
* The handlers carry all parse state through P (cursor, namespace scope, AND the
|
|
919
|
+
* open-element stack), so this loop stays a thin classifier. Shared by the
|
|
920
|
+
* document and fragment entries - P->fragment selects the depth-0 rules; the
|
|
921
|
+
* caller does the mode-specific post-checks. */
|
|
922
|
+
static void
|
|
923
|
+
run_tokenizer(mkr_xml_parser_t *P)
|
|
924
|
+
{
|
|
925
|
+
while (mkr_span_left(&P->in) > 0) {
|
|
926
|
+
if (mkr_span_peek(&P->in) != '<') {
|
|
927
|
+
if (parse_text(P) != 0) break;
|
|
928
|
+
} else {
|
|
929
|
+
uint32_t tl = P->line, tc = P->col; /* the '<' position (start-tag line/col) */
|
|
930
|
+
int at_start = (mkr_span_mark(&P->in) == P->start) && P->fragment == NULL; /* '<?xml ...?>' only in a document prolog */
|
|
931
|
+
advance(P); /* '<' */
|
|
932
|
+
int c = mkr_span_peek(&P->in);
|
|
933
|
+
if (c < 0) { set_syntax(P); break; }
|
|
934
|
+
int rc;
|
|
935
|
+
switch (c) {
|
|
936
|
+
case '/': rc = parse_end_tag(P); break;
|
|
937
|
+
case '!': rc = parse_markup(P); break; /* comment / CDATA / DTD */
|
|
938
|
+
case '?': rc = parse_pi(P, cur_parent(P), at_start); break;
|
|
939
|
+
default: rc = parse_start_tag(P, tl, tc); break;
|
|
940
|
+
}
|
|
941
|
+
if (rc != 0) break;
|
|
942
|
+
}
|
|
943
|
+
if (P->status != MKR_XML_OK) break;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/* Seed a fragment parser's binding scope with the document root's in-scope
|
|
948
|
+
* namespace declarations (its xmlns / xmlns:* DOM attributes ARE the in-scope set:
|
|
949
|
+
* the root is the top, and the xml: prefix is implicit in ns_lookup), so a
|
|
950
|
+
* prefixed (or default-namespaced) name in a Document#fragment resolves against
|
|
951
|
+
* the document, Nokogiri-faithfully. 0, or -1 on a binding-stack failure. */
|
|
952
|
+
static int
|
|
953
|
+
seed_doc_namespaces(mkr_xml_parser_t *P)
|
|
954
|
+
{
|
|
955
|
+
mkr_xml_node_t *root = P->doc->root;
|
|
956
|
+
if (root == NULL) return 0;
|
|
957
|
+
for (mkr_xml_node_t *a = root->attrs; a != NULL; a = a->next) {
|
|
958
|
+
const char *bpfx; uint32_t bpl;
|
|
959
|
+
if (!mkr_xml_xmlns_prefix(a->qname, a->qname_len, &bpfx, &bpl)) continue;
|
|
960
|
+
if (push_binding(P, bpfx, bpl, a->value ? a->value : "", a->value_len) != 0) return -1;
|
|
961
|
+
}
|
|
962
|
+
return 0;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
mkr_xml_doc_t *
|
|
966
|
+
mkr_xml_parse(const char *src, size_t len, mkr_xml_status_t *status)
|
|
967
|
+
{
|
|
968
|
+
return mkr_xml_parse_ex(src, len, NULL, status);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
mkr_xml_doc_t *
|
|
972
|
+
mkr_xml_parse_ex(const char *src, size_t len, const mkr_xml_limits_t *limits,
|
|
973
|
+
mkr_xml_status_t *status)
|
|
974
|
+
{
|
|
975
|
+
mkr_xml_doc_t *doc = mkr_xml_doc_new();
|
|
976
|
+
if (doc == NULL) { if (status) *status = MKR_XML_ERR_OOM; return NULL; }
|
|
977
|
+
|
|
978
|
+
/* Apply per-parse budget overrides over the compile-time defaults (a 0 field
|
|
979
|
+
* leaves the default; a NULL +limits+ leaves all defaults). Only max_bytes is
|
|
980
|
+
* runtime-configurable today. */
|
|
981
|
+
if (limits != NULL && limits->max_bytes != 0) doc->max_bytes = limits->max_bytes;
|
|
982
|
+
|
|
983
|
+
/* Fail closed on an over-budget input before ANY allocation (src is not yet
|
|
984
|
+
* read): every retained byte is copied into the arena, so an input longer
|
|
985
|
+
* than the arena byte budget can never succeed - reject it now instead of
|
|
986
|
+
* after building most of a doomed document. This also bounds the line-ending
|
|
987
|
+
* scratch below (norm <= len <= max_bytes), so it cannot run a large
|
|
988
|
+
* out-of-budget allocation ahead of the arena's own checks. */
|
|
989
|
+
if (len > doc->max_bytes) {
|
|
990
|
+
if (status) *status = MKR_XML_ERR_LIMIT;
|
|
991
|
+
mkr_xml_doc_destroy(doc);
|
|
992
|
+
return NULL;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/* Create the DOCUMENT node up front: it is the XPath "/" root, what a Ruby
|
|
996
|
+
* Document wraps, and the parent that top-level comments / PIs and the root
|
|
997
|
+
* element attach to during the parse (cur_parent at depth 0). */
|
|
998
|
+
doc->doc_node = mkr_xml_arena_node(doc, MKR_XML_NODE_TYPE_DOCUMENT);
|
|
999
|
+
if (doc->doc_node == NULL) {
|
|
1000
|
+
if (status) *status = doc->oom;
|
|
1001
|
+
mkr_xml_doc_destroy(doc);
|
|
1002
|
+
return NULL;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const char *body; size_t blen; char *norm;
|
|
1006
|
+
if (normalize_newlines(src, len, &body, &blen, &norm) != 0) {
|
|
1007
|
+
if (status) *status = MKR_XML_ERR_OOM;
|
|
1008
|
+
mkr_xml_doc_destroy(doc);
|
|
1009
|
+
return NULL;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
mkr_xml_parser_t P = { .in = mkr_span(body, blen), .start = body,
|
|
1013
|
+
.line = 1, .col = 1, .doc = doc, .status = MKR_XML_OK };
|
|
1014
|
+
run_tokenizer(&P);
|
|
1015
|
+
|
|
1016
|
+
if (P.status == MKR_XML_OK) {
|
|
1017
|
+
if (P.depth != 0) set_syntax(&P); /* unclosed element(s) */
|
|
1018
|
+
else if (doc->root == NULL) set_syntax(&P); /* no root element */
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
free(P.stack);
|
|
1022
|
+
free(P.frame);
|
|
1023
|
+
free(P.binds);
|
|
1024
|
+
free(P.ratt);
|
|
1025
|
+
free(norm); /* scratch line-ending-normalized buffer (if any) */
|
|
1026
|
+
if (P.status != MKR_XML_OK) {
|
|
1027
|
+
if (status) *status = P.status;
|
|
1028
|
+
mkr_xml_doc_destroy(doc);
|
|
1029
|
+
return NULL;
|
|
1030
|
+
}
|
|
1031
|
+
if (status) *status = MKR_XML_OK;
|
|
1032
|
+
return doc;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
mkr_xml_node_t *
|
|
1036
|
+
mkr_xml_parse_fragment(mkr_xml_doc_t *doc, const char *src, size_t len,
|
|
1037
|
+
int inherit_doc_ns, mkr_xml_status_t *status)
|
|
1038
|
+
{
|
|
1039
|
+
/* Coarse fast-fail before any allocation; the arena's per-cut checks enforce
|
|
1040
|
+
* the remaining budget during the parse (an existing document has already
|
|
1041
|
+
* spent some of it). */
|
|
1042
|
+
if (len > doc->max_bytes) { if (status) *status = MKR_XML_ERR_LIMIT; return NULL; }
|
|
1043
|
+
|
|
1044
|
+
mkr_xml_node_t *frag = mkr_xml_arena_node(doc, MKR_XML_NODE_TYPE_DOCUMENT_FRAGMENT);
|
|
1045
|
+
if (frag == NULL) { if (status) *status = doc->oom; return NULL; }
|
|
1046
|
+
|
|
1047
|
+
const char *body; size_t blen; char *norm;
|
|
1048
|
+
if (normalize_newlines(src, len, &body, &blen, &norm) != 0) {
|
|
1049
|
+
if (status) *status = MKR_XML_ERR_OOM;
|
|
1050
|
+
return NULL; /* frag stays in the arena, detached (never freed) */
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
mkr_xml_parser_t P = { .in = mkr_span(body, blen), .start = body, .line = 1,
|
|
1054
|
+
.col = 1, .doc = doc, .fragment = frag, .status = MKR_XML_OK };
|
|
1055
|
+
|
|
1056
|
+
if (inherit_doc_ns && seed_doc_namespaces(&P) != 0) {
|
|
1057
|
+
free(P.binds); free(norm);
|
|
1058
|
+
if (status) *status = P.status;
|
|
1059
|
+
return NULL;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
run_tokenizer(&P);
|
|
1063
|
+
if (P.status == MKR_XML_OK && P.depth != 0) set_syntax(&P); /* unclosed element(s) */
|
|
1064
|
+
|
|
1065
|
+
free(P.stack);
|
|
1066
|
+
free(P.frame);
|
|
1067
|
+
free(P.binds);
|
|
1068
|
+
free(P.ratt);
|
|
1069
|
+
free(norm);
|
|
1070
|
+
if (P.status != MKR_XML_OK) {
|
|
1071
|
+
if (status) *status = P.status;
|
|
1072
|
+
return NULL; /* the partial fragment stays detached in the arena */
|
|
1073
|
+
}
|
|
1074
|
+
if (status) *status = MKR_XML_OK;
|
|
1075
|
+
return frag;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/* ---- parse self-test (structural; run from Makiri.__c_selftest under ASan) --
|
|
1079
|
+
* Returns 0 on success or the 1-based index of the first failing check. Test
|
|
1080
|
+
* strings are compile-time literals, so lengths come from sizeof - 1 (no
|
|
1081
|
+
* strlen on a non-validated string -> clint clean). */
|
|
1082
|
+
static int nlname(const mkr_xml_node_t *n, const char *s, size_t l)
|
|
1083
|
+
{
|
|
1084
|
+
return n && mkr_bytes_eq(n->local, n->local_len, s, l);
|
|
1085
|
+
}
|
|
1086
|
+
static int nlval(const mkr_xml_node_t *n, const char *s, size_t l)
|
|
1087
|
+
{
|
|
1088
|
+
return n && mkr_bytes_eq(n->value, n->value_len, s, l);
|
|
1089
|
+
}
|
|
1090
|
+
static int nlns(const mkr_xml_node_t *n, const char *s, size_t l)
|
|
1091
|
+
{
|
|
1092
|
+
return n && mkr_bytes_eq(n->ns_uri, n->ns_uri_len, s, l);
|
|
1093
|
+
}
|
|
1094
|
+
static int nlpfx(const mkr_xml_node_t *n, const char *s, size_t l)
|
|
1095
|
+
{
|
|
1096
|
+
return n && mkr_bytes_eq(n->prefix, n->prefix_len, s, l);
|
|
1097
|
+
}
|
|
1098
|
+
#define NAME_IS(n, lit) nlname((n), (lit), sizeof(lit) - 1)
|
|
1099
|
+
#define VAL_IS(n, lit) nlval((n), (lit), sizeof(lit) - 1)
|
|
1100
|
+
#define NS_IS(n, lit) nlns((n), (lit), sizeof(lit) - 1)
|
|
1101
|
+
#define PFX_IS(n, lit) nlpfx((n), (lit), sizeof(lit) - 1)
|
|
1102
|
+
#define NS_NONE(n) ((n) && (n)->ns_uri_len == 0)
|
|
1103
|
+
#define PARSE_LIT(lit, stp) mkr_xml_parse((lit), sizeof(lit) - 1, (stp))
|
|
1104
|
+
|
|
1105
|
+
int
|
|
1106
|
+
mkr_xml_parse_selftest(void)
|
|
1107
|
+
{
|
|
1108
|
+
mkr_xml_status_t st = MKR_XML_OK;
|
|
1109
|
+
int i = 0;
|
|
1110
|
+
|
|
1111
|
+
/* happy path: structure, attributes, mixed content, case preservation */
|
|
1112
|
+
i++; /* 1 */
|
|
1113
|
+
mkr_xml_doc_t *d = PARSE_LIT("<Feed x='1' y='two'>hi<b/>z</Feed>", &st);
|
|
1114
|
+
if (d == NULL || st != MKR_XML_OK) { if (d) mkr_xml_doc_destroy(d); return i; }
|
|
1115
|
+
|
|
1116
|
+
i++; /* 2: root element, case-preserved name, source position recorded */
|
|
1117
|
+
mkr_xml_node_t *root = d->root;
|
|
1118
|
+
if (!NAME_IS(root, "Feed") || root->type != MKR_XML_NODE_TYPE_ELEMENT
|
|
1119
|
+
|| root->line != 1 || root->col != 1) { mkr_xml_doc_destroy(d); return i; }
|
|
1120
|
+
|
|
1121
|
+
i++; /* 3: attributes x=1, y=two in order */
|
|
1122
|
+
mkr_xml_node_t *a0 = root->attrs;
|
|
1123
|
+
mkr_xml_node_t *a1 = a0 ? a0->next : NULL;
|
|
1124
|
+
if (!a0 || a0->type != MKR_XML_NODE_TYPE_ATTRIBUTE || !NAME_IS(a0, "x") || !VAL_IS(a0, "1")
|
|
1125
|
+
|| !a1 || !NAME_IS(a1, "y") || !VAL_IS(a1, "two") || a1->next != NULL) {
|
|
1126
|
+
mkr_xml_doc_destroy(d); return i;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
i++; /* 4: children = text "hi", element <b>, text "z" */
|
|
1130
|
+
mkr_xml_node_t *c0 = root->first_child;
|
|
1131
|
+
mkr_xml_node_t *c1 = c0 ? c0->next : NULL;
|
|
1132
|
+
mkr_xml_node_t *c2 = c1 ? c1->next : NULL;
|
|
1133
|
+
if (!c0 || c0->type != MKR_XML_NODE_TYPE_TEXT || !VAL_IS(c0, "hi")
|
|
1134
|
+
|| !c1 || c1->type != MKR_XML_NODE_TYPE_ELEMENT || !NAME_IS(c1, "b") || c1->first_child != NULL
|
|
1135
|
+
|| !c2 || c2->type != MKR_XML_NODE_TYPE_TEXT || !VAL_IS(c2, "z") || c2->next != NULL) {
|
|
1136
|
+
mkr_xml_doc_destroy(d); return i;
|
|
1137
|
+
}
|
|
1138
|
+
i++; /* 5: parent/sibling links */
|
|
1139
|
+
if (c1->parent != root || c1->prev != c0 || c2->prev != c1) { mkr_xml_doc_destroy(d); return i; }
|
|
1140
|
+
mkr_xml_doc_destroy(d);
|
|
1141
|
+
|
|
1142
|
+
/* case sensitivity: <X> and <x> are distinct */
|
|
1143
|
+
i++; /* 6 */
|
|
1144
|
+
d = PARSE_LIT("<X><x/></X>", &st);
|
|
1145
|
+
if (d == NULL || !NAME_IS(d->root, "X") || !NAME_IS(d->root->first_child, "x")) {
|
|
1146
|
+
if (d) mkr_xml_doc_destroy(d); return i;
|
|
1147
|
+
}
|
|
1148
|
+
mkr_xml_doc_destroy(d);
|
|
1149
|
+
|
|
1150
|
+
/* well-formedness errors fail closed (NULL + SYNTAX, no leak) */
|
|
1151
|
+
i++; /* 7: one literal per case keeps sizeof-based lengths */
|
|
1152
|
+
st = MKR_XML_OK;
|
|
1153
|
+
mkr_xml_doc_t *e;
|
|
1154
|
+
e = PARSE_LIT("<a>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; }
|
|
1155
|
+
st = MKR_XML_OK;
|
|
1156
|
+
e = PARSE_LIT("<a></b>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; }
|
|
1157
|
+
st = MKR_XML_OK;
|
|
1158
|
+
e = PARSE_LIT("<a/><b/>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; }
|
|
1159
|
+
st = MKR_XML_OK;
|
|
1160
|
+
e = PARSE_LIT("x<a/>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; }
|
|
1161
|
+
st = MKR_XML_OK;
|
|
1162
|
+
e = PARSE_LIT("<a x=>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; }
|
|
1163
|
+
st = MKR_XML_OK;
|
|
1164
|
+
e = PARSE_LIT("<a y='<'>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; }
|
|
1165
|
+
|
|
1166
|
+
/* §9.1: predefined + numeric references expand; text and attribute values */
|
|
1167
|
+
i++; /* 8 */
|
|
1168
|
+
d = PARSE_LIT("<a x='p&q' y='AB'>1<2>3&4'5"6</a>", &st);
|
|
1169
|
+
if (d == NULL || st != MKR_XML_OK) { if (d) mkr_xml_doc_destroy(d); return i; }
|
|
1170
|
+
{
|
|
1171
|
+
mkr_xml_node_t *r = d->root;
|
|
1172
|
+
mkr_xml_node_t *ax = r->attrs, *ay = ax ? ax->next : NULL;
|
|
1173
|
+
mkr_xml_node_t *tx = r->first_child;
|
|
1174
|
+
if (!VAL_IS(ax, "p&q") || !VAL_IS(ay, "AB")
|
|
1175
|
+
|| !tx || tx->type != MKR_XML_NODE_TYPE_TEXT || !VAL_IS(tx, "1<2>3&4'5\"6")) {
|
|
1176
|
+
mkr_xml_doc_destroy(d); return i;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
mkr_xml_doc_destroy(d);
|
|
1180
|
+
|
|
1181
|
+
/* §9.1/§9.2: undefined entity, bad/empty reference, and non-XML-Char fail closed */
|
|
1182
|
+
i++; /* 9 */
|
|
1183
|
+
st = MKR_XML_OK;
|
|
1184
|
+
e = PARSE_LIT("<a> </a>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; }
|
|
1185
|
+
st = MKR_XML_OK;
|
|
1186
|
+
e = PARSE_LIT("<a>x & y</a>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; } /* bare & */
|
|
1187
|
+
st = MKR_XML_OK;
|
|
1188
|
+
e = PARSE_LIT("<a>�</a>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; } /* NUL not XML Char */
|
|
1189
|
+
st = MKR_XML_OK;
|
|
1190
|
+
e = PARSE_LIT("<a>�</a>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; } /* surrogate */
|
|
1191
|
+
st = MKR_XML_OK;
|
|
1192
|
+
e = PARSE_LIT("<a>&#;</a>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; } /* no digits */
|
|
1193
|
+
|
|
1194
|
+
/* §7: namespaces - prefixed element + attribute, default-ns inheritance,
|
|
1195
|
+
* unprefixed attribute has no namespace, xmlns kept as a DOM attribute */
|
|
1196
|
+
i++; /* 10 */
|
|
1197
|
+
d = PARSE_LIT("<a:e xmlns:a='urn:a' xmlns='urn:d' a:x='1' y='2'><c/></a:e>", &st);
|
|
1198
|
+
if (d == NULL || st != MKR_XML_OK) { if (d) mkr_xml_doc_destroy(d); return i; }
|
|
1199
|
+
{
|
|
1200
|
+
mkr_xml_node_t *r = d->root;
|
|
1201
|
+
if (!NAME_IS(r, "e") || !PFX_IS(r, "a") || !NS_IS(r, "urn:a")) { mkr_xml_doc_destroy(d); return i; }
|
|
1202
|
+
mkr_xml_node_t *c = r->first_child;
|
|
1203
|
+
if (!NAME_IS(c, "c") || !NS_IS(c, "urn:d")) { mkr_xml_doc_destroy(d); return i; } /* default ns inherited */
|
|
1204
|
+
/* attrs in order: xmlns:a, xmlns, a:x, y */
|
|
1205
|
+
mkr_xml_node_t *a = r->attrs;
|
|
1206
|
+
if (!a || !NAME_IS(a, "a") || !PFX_IS(a, "xmlns") || !NS_IS(a, XMLNS_NS_URI)) { mkr_xml_doc_destroy(d); return i; }
|
|
1207
|
+
a = a->next; /* xmlns */
|
|
1208
|
+
if (!a || !NAME_IS(a, "xmlns") || a->prefix_len != 0) { mkr_xml_doc_destroy(d); return i; }
|
|
1209
|
+
a = a->next; /* a:x */
|
|
1210
|
+
if (!a || !NAME_IS(a, "x") || !PFX_IS(a, "a") || !NS_IS(a, "urn:a") || !VAL_IS(a, "1")) { mkr_xml_doc_destroy(d); return i; }
|
|
1211
|
+
a = a->next; /* y -> NO namespace (default ns never applies to attributes) */
|
|
1212
|
+
if (!a || !NAME_IS(a, "y") || a->prefix_len != 0 || !NS_NONE(a) || !VAL_IS(a, "2")) { mkr_xml_doc_destroy(d); return i; }
|
|
1213
|
+
}
|
|
1214
|
+
mkr_xml_doc_destroy(d);
|
|
1215
|
+
|
|
1216
|
+
/* §7.1: namespace errors fail closed */
|
|
1217
|
+
i++; /* 11 */
|
|
1218
|
+
st = MKR_XML_OK;
|
|
1219
|
+
e = PARSE_LIT("<a:b/>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; } /* unbound element prefix */
|
|
1220
|
+
st = MKR_XML_OK;
|
|
1221
|
+
e = PARSE_LIT("<a x:y='1'/>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; } /* unbound attr prefix */
|
|
1222
|
+
st = MKR_XML_OK;
|
|
1223
|
+
e = PARSE_LIT("<a xmlns:xml='wrong'/>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; } /* rebinding xml */
|
|
1224
|
+
st = MKR_XML_OK;
|
|
1225
|
+
e = PARSE_LIT("<a:b xmlns:a=''/>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; } /* xmlns:p="" */
|
|
1226
|
+
|
|
1227
|
+
/* §3.3.3: attribute-value normalization - a LITERAL tab/newline/CR in the
|
|
1228
|
+
* source folds to a space, while a tab/LF from a numeric reference is kept.
|
|
1229
|
+
* Text content is NOT folded (the literal newline survives verbatim). */
|
|
1230
|
+
i++; /* 12 */
|
|
1231
|
+
d = PARSE_LIT("<a x=\"p\tq\nr\" y=\"p	q r\">u\tv\nw</a>", &st);
|
|
1232
|
+
if (d == NULL || st != MKR_XML_OK) { if (d) mkr_xml_doc_destroy(d); return i; }
|
|
1233
|
+
{
|
|
1234
|
+
mkr_xml_node_t *r = d->root;
|
|
1235
|
+
mkr_xml_node_t *ax = r->attrs, *ay = ax ? ax->next : NULL;
|
|
1236
|
+
mkr_xml_node_t *tx = r->first_child;
|
|
1237
|
+
if (!VAL_IS(ax, "p q r") /* literal whitespace -> spaces */
|
|
1238
|
+
|| !VAL_IS(ay, "p\tq\nr") /* reference-derived whitespace preserved */
|
|
1239
|
+
|| !tx || tx->type != MKR_XML_NODE_TYPE_TEXT || !VAL_IS(tx, "u\tv\nw")) { /* text unfolded */
|
|
1240
|
+
mkr_xml_doc_destroy(d); return i;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
mkr_xml_doc_destroy(d);
|
|
1244
|
+
|
|
1245
|
+
/* §9: comment / CDATA / PI nodes inside an element; the prolog XML declaration
|
|
1246
|
+
* is not retained, but a prolog PI / comment IS, as a child of the DOCUMENT
|
|
1247
|
+
* node (before the root); '<!DOCTYPE' is fail-closed elsewhere. */
|
|
1248
|
+
i++; /* 13 */
|
|
1249
|
+
d = PARSE_LIT("<?xml version=\"1.0\"?><?xml-stylesheet href=\"x\"?><!--top-->"
|
|
1250
|
+
"<r><!--c--><![CDATA[a<b]]><?pi dat?></r><?tail t?>", &st);
|
|
1251
|
+
if (d == NULL || st != MKR_XML_OK) { if (d) mkr_xml_doc_destroy(d); return i; }
|
|
1252
|
+
{
|
|
1253
|
+
mkr_xml_node_t *r = d->root;
|
|
1254
|
+
if (!NAME_IS(r, "r")) { mkr_xml_doc_destroy(d); return i; }
|
|
1255
|
+
mkr_xml_node_t *cm = r->first_child;
|
|
1256
|
+
mkr_xml_node_t *cd = cm ? cm->next : NULL;
|
|
1257
|
+
mkr_xml_node_t *pi = cd ? cd->next : NULL;
|
|
1258
|
+
if (!cm || cm->type != MKR_XML_NODE_TYPE_COMMENT || !VAL_IS(cm, "c")
|
|
1259
|
+
|| !cd || cd->type != MKR_XML_NODE_TYPE_CDATA_SECTION || !VAL_IS(cd, "a<b") /* raw '<' kept */
|
|
1260
|
+
|| !pi || pi->type != MKR_XML_NODE_TYPE_PI || !NAME_IS(pi, "pi") || !VAL_IS(pi, "dat")
|
|
1261
|
+
|| pi->next != NULL) {
|
|
1262
|
+
mkr_xml_doc_destroy(d); return i;
|
|
1263
|
+
}
|
|
1264
|
+
/* the DOCUMENT node's children: prolog PI (xml-stylesheet) + comment,
|
|
1265
|
+
* then the root, then the epilog PI (the <?xml?> declaration is NOT a
|
|
1266
|
+
* node); inter-construct whitespace is not retained. */
|
|
1267
|
+
mkr_xml_node_t *dn = d->doc_node;
|
|
1268
|
+
mkr_xml_node_t *p1 = dn ? dn->first_child : NULL; /* <?xml-stylesheet?> */
|
|
1269
|
+
mkr_xml_node_t *p2 = p1 ? p1->next : NULL; /* <!--top--> */
|
|
1270
|
+
mkr_xml_node_t *p3 = p2 ? p2->next : NULL; /* <r> (root) */
|
|
1271
|
+
mkr_xml_node_t *p4 = p3 ? p3->next : NULL; /* <?tail?> */
|
|
1272
|
+
if (!p1 || p1->type != MKR_XML_NODE_TYPE_PI || !NAME_IS(p1, "xml-stylesheet")
|
|
1273
|
+
|| !p2 || p2->type != MKR_XML_NODE_TYPE_COMMENT || !VAL_IS(p2, "top")
|
|
1274
|
+
|| p3 != r || r->parent != dn
|
|
1275
|
+
|| !p4 || p4->type != MKR_XML_NODE_TYPE_PI || !NAME_IS(p4, "tail")
|
|
1276
|
+
|| p4->next != NULL) {
|
|
1277
|
+
mkr_xml_doc_destroy(d); return i;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
mkr_xml_doc_destroy(d);
|
|
1281
|
+
|
|
1282
|
+
i++; /* 14: §9 fail-closed cases */
|
|
1283
|
+
st = MKR_XML_OK;
|
|
1284
|
+
e = PARSE_LIT("<r/><!DOCTYPE r>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; } /* DOCTYPE after root */
|
|
1285
|
+
st = MKR_XML_OK;
|
|
1286
|
+
e = PARSE_LIT("<!DOCTYPE r [ <!ENTITY x \"y\"> ]><r>&x;</r>", &st);
|
|
1287
|
+
if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; } /* DTD entity not registered -> &x; undefined */
|
|
1288
|
+
st = MKR_XML_OK;
|
|
1289
|
+
e = PARSE_LIT("<r><!-- a--b --></r>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; } /* '--' in comment */
|
|
1290
|
+
st = MKR_XML_OK;
|
|
1291
|
+
e = PARSE_LIT("<r><!-- c </r>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; } /* unterminated comment */
|
|
1292
|
+
st = MKR_XML_OK;
|
|
1293
|
+
e = PARSE_LIT(" <?xml version=\"1.0\"?><r/>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; } /* decl not at start */
|
|
1294
|
+
st = MKR_XML_OK;
|
|
1295
|
+
e = PARSE_LIT("<![CDATA[x]]><r/>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; } /* CDATA outside root */
|
|
1296
|
+
|
|
1297
|
+
/* §9.4 alternative: DOCTYPE is recognized but not processed - the external ID
|
|
1298
|
+
* and the internal subset (with a '>' inside a markup decl, and '>' inside a
|
|
1299
|
+
* quoted literal) are skipped to the true '>', nothing in the DTD is
|
|
1300
|
+
* registered, and parsing continues into the root. The name + external ID are
|
|
1301
|
+
* retained in an off-tree DOCUMENT_TYPE node (doc->doctype, NOT a child of any
|
|
1302
|
+
* node, so the tree/XPath is unaffected); here SYSTEM "a>b" (with a quoted
|
|
1303
|
+
* '>') gives system id "a>b" and no public id. */
|
|
1304
|
+
i++; /* 14b */
|
|
1305
|
+
d = PARSE_LIT("<!DOCTYPE r SYSTEM \"a>b\" [ <!ELEMENT r (#PCDATA)> ]><r>ok</r>", &st);
|
|
1306
|
+
if (d == NULL || st != MKR_XML_OK || !NAME_IS(d->root, "r")
|
|
1307
|
+
|| !VAL_IS(d->root->first_child, "ok")) {
|
|
1308
|
+
if (d) mkr_xml_doc_destroy(d); return i;
|
|
1309
|
+
}
|
|
1310
|
+
{
|
|
1311
|
+
mkr_xml_node_t *dt = d->doctype;
|
|
1312
|
+
if (dt == NULL || dt->type != MKR_XML_NODE_TYPE_DOCUMENT_TYPE
|
|
1313
|
+
|| dt->parent != NULL || dt->next != NULL || dt->prev != NULL /* off-tree */
|
|
1314
|
+
|| !slice_eq(dt->local, dt->local_len, "r", 1)
|
|
1315
|
+
|| dt->prefix != NULL /* no PUBLIC id */
|
|
1316
|
+
|| !slice_eq(dt->value, dt->value_len, "a>b", 3)) { /* SYSTEM id */
|
|
1317
|
+
mkr_xml_doc_destroy(d); return i;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
mkr_xml_doc_destroy(d);
|
|
1321
|
+
|
|
1322
|
+
/* §9.3b-A: line-ending normalization happens before attribute-value
|
|
1323
|
+
* normalization, so a literal CRLF inside an attribute collapses to ONE space
|
|
1324
|
+
* (CRLF -> LF -> space); in text content CRLF/CR just fold to LF, unfolded. */
|
|
1325
|
+
i++; /* 15 */
|
|
1326
|
+
d = PARSE_LIT("<a x=\"p\r\nq\r\">m\r\nn\ro</a>", &st);
|
|
1327
|
+
if (d == NULL || st != MKR_XML_OK) { if (d) mkr_xml_doc_destroy(d); return i; }
|
|
1328
|
+
{
|
|
1329
|
+
mkr_xml_node_t *r = d->root;
|
|
1330
|
+
mkr_xml_node_t *ax = r->attrs;
|
|
1331
|
+
mkr_xml_node_t *tx = r->first_child;
|
|
1332
|
+
if (!VAL_IS(ax, "p q ") /* CRLF and lone CR each -> one space */
|
|
1333
|
+
|| !tx || tx->type != MKR_XML_NODE_TYPE_TEXT || !VAL_IS(tx, "m\nn\no")) { /* folded to LF */
|
|
1334
|
+
mkr_xml_doc_destroy(d); return i;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
mkr_xml_doc_destroy(d);
|
|
1338
|
+
|
|
1339
|
+
i++; /* 16: §9.2b strict names + §9.3 duplicate attributes fail closed */
|
|
1340
|
+
st = MKR_XML_OK;
|
|
1341
|
+
e = PARSE_LIT("<1bad/>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; } /* NameStartChar */
|
|
1342
|
+
st = MKR_XML_OK;
|
|
1343
|
+
e = PARSE_LIT("<a:1b xmlns:a='u'/>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; } /* NCName local */
|
|
1344
|
+
st = MKR_XML_OK;
|
|
1345
|
+
e = PARSE_LIT("<a x='1' x='2'/>", &st); if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; } /* duplicate attr */
|
|
1346
|
+
st = MKR_XML_OK;
|
|
1347
|
+
e = PARSE_LIT("<e xmlns:a='u' xmlns:b='u' a:x='1' b:x='2'/>", &st);
|
|
1348
|
+
if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; } /* same-URI duplicate */
|
|
1349
|
+
st = MKR_XML_OK;
|
|
1350
|
+
e = PARSE_LIT("<a>foo]]>bar</a>", &st); /* §2.4: literal ]]> forbidden in content */
|
|
1351
|
+
if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; }
|
|
1352
|
+
st = MKR_XML_OK;
|
|
1353
|
+
d = PARSE_LIT("<a>1]2]]3</a>", &st); /* but a lone ] / ]] is fine */
|
|
1354
|
+
if (d == NULL || st != MKR_XML_OK || !VAL_IS(d->root->first_child, "1]2]]3")) {
|
|
1355
|
+
if (d) mkr_xml_doc_destroy(d); return i;
|
|
1356
|
+
}
|
|
1357
|
+
mkr_xml_doc_destroy(d);
|
|
1358
|
+
|
|
1359
|
+
i++; /* 17: §2.8 XML declaration grammar + §2.6 reserved/colon PI targets */
|
|
1360
|
+
{
|
|
1361
|
+
/* a fully-specified valid declaration parses */
|
|
1362
|
+
st = MKR_XML_OK;
|
|
1363
|
+
d = PARSE_LIT("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><r/>", &st);
|
|
1364
|
+
if (d == NULL || st != MKR_XML_OK) { if (d) mkr_xml_doc_destroy(d); return i; }
|
|
1365
|
+
mkr_xml_doc_destroy(d);
|
|
1366
|
+
static const char *bad[] = {
|
|
1367
|
+
"<?xml VERSION=\"1.0\"?><r/>", /* keyword case */
|
|
1368
|
+
"<?xml version=\"1.0\" standalone=\"YES\"?><r/>", /* value case */
|
|
1369
|
+
"<?xml encoding=\"UTF-8\"?><r/>", /* version required */
|
|
1370
|
+
"<?xml version=\"1.0\"encoding=\"UTF-8\"?><r/>", /* missing S separator */
|
|
1371
|
+
"<?xml version=\"1.0\" version=\"1.0\"?><r/>", /* duplicate */
|
|
1372
|
+
"<?xml version=\"1.0\" valid=\"no\"?><r/>", /* unknown pseudo-attr */
|
|
1373
|
+
"<?xml version=\"1.0' ?><r/>", /* mismatched quotes */
|
|
1374
|
+
"<?xml version=\"1.0^\"?><r/>", /* bad VersionNum char */
|
|
1375
|
+
"<?XML version=\"1.0\"?><r/>", /* reserved target (wrong case) */
|
|
1376
|
+
"<r><?a:b data?></r>", /* colon in PITarget (NS) */
|
|
1377
|
+
"<a x=\"1\"y=\"2\"/>", /* §3.1: missing S between attrs */
|
|
1378
|
+
"<a>X</a>", /* §4.1: hex marker must be lowercase x */
|
|
1379
|
+
};
|
|
1380
|
+
for (size_t k = 0; k < sizeof(bad) / sizeof(bad[0]); k++) {
|
|
1381
|
+
st = MKR_XML_OK;
|
|
1382
|
+
e = PARSE_LIT(bad[k], &st);
|
|
1383
|
+
if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; }
|
|
1384
|
+
}
|
|
1385
|
+
/* a well-formed but unsupported version (XML 1.1 / 1.x) is fail-closed
|
|
1386
|
+
* with its own status, distinct from a malformedness SYNTAX error. */
|
|
1387
|
+
st = MKR_XML_OK;
|
|
1388
|
+
e = PARSE_LIT("<?xml version=\"1.1\"?><r/>", &st);
|
|
1389
|
+
if (e || st != MKR_XML_ERR_VERSION) { if (e) mkr_xml_doc_destroy(e); return i; }
|
|
1390
|
+
st = MKR_XML_OK;
|
|
1391
|
+
e = PARSE_LIT("<?xml version=\"1.5\"?><r/>", &st);
|
|
1392
|
+
if (e || st != MKR_XML_ERR_VERSION) { if (e) mkr_xml_doc_destroy(e); return i; }
|
|
1393
|
+
/* version="2.0" is not even a valid VersionNum ('1.' [0-9]+) -> SYNTAX */
|
|
1394
|
+
st = MKR_XML_OK;
|
|
1395
|
+
e = PARSE_LIT("<?xml version=\"2.0\"?><r/>", &st);
|
|
1396
|
+
if (e || st != MKR_XML_ERR_SYNTAX) { if (e) mkr_xml_doc_destroy(e); return i; }
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/* §4 byte-budget entry guard: an input longer than MKR_XML_MAX_BYTES fails
|
|
1400
|
+
* closed (MKR_XML_ERR_LIMIT) before any allocation. The guard tests `len`
|
|
1401
|
+
* only, so a tiny buffer with a huge claimed length exercises it without
|
|
1402
|
+
* allocating - and proves `src` is not dereferenced past the budget. */
|
|
1403
|
+
i++; /* 18 */
|
|
1404
|
+
st = MKR_XML_OK;
|
|
1405
|
+
{
|
|
1406
|
+
static const char tiny[] = "<r/>";
|
|
1407
|
+
if (mkr_xml_parse(tiny, (size_t)MKR_XML_MAX_BYTES + 1u, &st) != NULL
|
|
1408
|
+
|| st != MKR_XML_ERR_LIMIT) {
|
|
1409
|
+
return i;
|
|
1410
|
+
}
|
|
1411
|
+
/* and the boundary (== budget) is NOT rejected by the guard (it parses,
|
|
1412
|
+
* since this real 4-byte input is well under the budget). */
|
|
1413
|
+
st = MKR_XML_OK;
|
|
1414
|
+
d = mkr_xml_parse(tiny, sizeof(tiny) - 1, &st);
|
|
1415
|
+
if (d == NULL || st != MKR_XML_OK) { if (d) mkr_xml_doc_destroy(d); return i; }
|
|
1416
|
+
mkr_xml_doc_destroy(d);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/* §4 per-parse override (mkr_xml_parse_ex): a tiny max_bytes lowers the
|
|
1420
|
+
* effective budget so a document that parses under the default now fails
|
|
1421
|
+
* closed, and a NULL/zeroed limits reproduces the default. */
|
|
1422
|
+
i++; /* 19 */
|
|
1423
|
+
{
|
|
1424
|
+
static const char doc_src[] = "<root><a/><b/><c/></root>";
|
|
1425
|
+
size_t dlen = sizeof(doc_src) - 1;
|
|
1426
|
+
/* a 2-byte budget trips the entry guard (len > max_bytes) */
|
|
1427
|
+
st = MKR_XML_OK;
|
|
1428
|
+
mkr_xml_limits_t tiny_budget = { .max_bytes = 2 };
|
|
1429
|
+
if (mkr_xml_parse_ex(doc_src, dlen, &tiny_budget, &st) != NULL || st != MKR_XML_ERR_LIMIT) {
|
|
1430
|
+
return i;
|
|
1431
|
+
}
|
|
1432
|
+
/* a budget that passes the entry guard but cannot hold the nodes trips the
|
|
1433
|
+
* arena allocator instead (one 128-byte node already exceeds 64). */
|
|
1434
|
+
st = MKR_XML_OK;
|
|
1435
|
+
mkr_xml_limits_t small_budget = { .max_bytes = 64 };
|
|
1436
|
+
if (mkr_xml_parse_ex(doc_src, dlen, &small_budget, &st) != NULL || st != MKR_XML_ERR_LIMIT) {
|
|
1437
|
+
return i;
|
|
1438
|
+
}
|
|
1439
|
+
/* a generous override parses the same document; zeroed limits == default */
|
|
1440
|
+
st = MKR_XML_OK;
|
|
1441
|
+
mkr_xml_limits_t big_budget = { .max_bytes = (size_t)1024u * 1024u };
|
|
1442
|
+
d = mkr_xml_parse_ex(doc_src, dlen, &big_budget, &st);
|
|
1443
|
+
if (d == NULL || st != MKR_XML_OK || !NAME_IS(d->root, "root")) {
|
|
1444
|
+
if (d) mkr_xml_doc_destroy(d); return i;
|
|
1445
|
+
}
|
|
1446
|
+
mkr_xml_doc_destroy(d);
|
|
1447
|
+
st = MKR_XML_OK;
|
|
1448
|
+
mkr_xml_limits_t zero = { 0 };
|
|
1449
|
+
d = mkr_xml_parse_ex(doc_src, dlen, &zero, &st);
|
|
1450
|
+
if (d == NULL || st != MKR_XML_OK) { if (d) mkr_xml_doc_destroy(d); return i; }
|
|
1451
|
+
mkr_xml_doc_destroy(d);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/* 20: fragment - multiple top-level nodes (no single-root rule), char data at
|
|
1455
|
+
* the top level, a self-declared namespace. */
|
|
1456
|
+
i++;
|
|
1457
|
+
{
|
|
1458
|
+
static const char fsrc[] = "<a/>txt<p:b xmlns:p='urn:p'>x</p:b>";
|
|
1459
|
+
mkr_xml_doc_t *fd = mkr_xml_doc_new();
|
|
1460
|
+
if (fd == NULL) return i;
|
|
1461
|
+
st = MKR_XML_OK;
|
|
1462
|
+
mkr_xml_node_t *frag = mkr_xml_parse_fragment(fd, fsrc, sizeof(fsrc) - 1, 0, &st);
|
|
1463
|
+
if (frag == NULL || st != MKR_XML_OK
|
|
1464
|
+
|| frag->type != MKR_XML_NODE_TYPE_DOCUMENT_FRAGMENT) { mkr_xml_doc_destroy(fd); return i; }
|
|
1465
|
+
mkr_xml_node_t *c0 = frag->first_child;
|
|
1466
|
+
mkr_xml_node_t *c1 = c0 ? c0->next : NULL;
|
|
1467
|
+
mkr_xml_node_t *c2 = c1 ? c1->next : NULL;
|
|
1468
|
+
if (!NAME_IS(c0, "a") || c0->type != MKR_XML_NODE_TYPE_ELEMENT
|
|
1469
|
+
|| c1 == NULL || c1->type != MKR_XML_NODE_TYPE_TEXT || !VAL_IS(c1, "txt")
|
|
1470
|
+
|| !NAME_IS(c2, "b") || !NS_IS(c2, "urn:p") || c2->next != NULL) {
|
|
1471
|
+
mkr_xml_doc_destroy(fd); return i;
|
|
1472
|
+
}
|
|
1473
|
+
mkr_xml_doc_destroy(fd);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/* 21: a fragment fails closed on an XML declaration, a DOCTYPE, a stray end
|
|
1477
|
+
* tag, an unclosed element, and (standalone) an unbound prefix. */
|
|
1478
|
+
i++;
|
|
1479
|
+
{
|
|
1480
|
+
mkr_xml_doc_t *fd = mkr_xml_doc_new();
|
|
1481
|
+
if (fd == NULL) return i;
|
|
1482
|
+
#define FRAG_REJECTS(lit) do { \
|
|
1483
|
+
st = MKR_XML_OK; \
|
|
1484
|
+
if (mkr_xml_parse_fragment(fd, (lit), sizeof(lit) - 1, 0, &st) != NULL \
|
|
1485
|
+
|| st == MKR_XML_OK) { mkr_xml_doc_destroy(fd); return i; } \
|
|
1486
|
+
} while (0)
|
|
1487
|
+
FRAG_REJECTS("<?xml version='1.0'?>");
|
|
1488
|
+
FRAG_REJECTS("<!DOCTYPE r>");
|
|
1489
|
+
FRAG_REJECTS("</x>");
|
|
1490
|
+
FRAG_REJECTS("<a>");
|
|
1491
|
+
FRAG_REJECTS("<p:a/>");
|
|
1492
|
+
#undef FRAG_REJECTS
|
|
1493
|
+
mkr_xml_doc_destroy(fd);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/* 22: inherit_doc_ns resolves a prefixed (and default-namespaced) fragment name
|
|
1497
|
+
* against the document's root namespaces (Document#fragment). */
|
|
1498
|
+
i++;
|
|
1499
|
+
{
|
|
1500
|
+
static const char fsrc[] = "<p:a/><plain/>";
|
|
1501
|
+
st = MKR_XML_OK;
|
|
1502
|
+
mkr_xml_doc_t *fd = PARSE_LIT("<r xmlns:p='urn:p' xmlns='urn:d'/>", &st);
|
|
1503
|
+
if (fd == NULL || st != MKR_XML_OK) { if (fd) mkr_xml_doc_destroy(fd); return i; }
|
|
1504
|
+
st = MKR_XML_OK;
|
|
1505
|
+
mkr_xml_node_t *frag = mkr_xml_parse_fragment(fd, fsrc, sizeof(fsrc) - 1, 1, &st);
|
|
1506
|
+
mkr_xml_node_t *a = frag ? frag->first_child : NULL;
|
|
1507
|
+
mkr_xml_node_t *plain = a ? a->next : NULL;
|
|
1508
|
+
if (frag == NULL || st != MKR_XML_OK || !NS_IS(a, "urn:p") || !NS_IS(plain, "urn:d")) {
|
|
1509
|
+
mkr_xml_doc_destroy(fd); return i;
|
|
1510
|
+
}
|
|
1511
|
+
mkr_xml_doc_destroy(fd);
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
return 0;
|
|
1515
|
+
}
|