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,139 @@
|
|
|
1
|
+
/* mkr_xml_mutate.h - Ruby-free XML tree mutation primitives (Phase 1).
|
|
2
|
+
*
|
|
3
|
+
* The write counterpart of mkr_xml_tree.c's read-only build: in-place edits over
|
|
4
|
+
* the custom node arena (rename, attribute set/remove, content set, detach). All
|
|
5
|
+
* primitives are Ruby-free and operate only on arena-stable pointers, so they are
|
|
6
|
+
* fully covered by ASan/UBSan and the fuzzer; the Ruby boundary (glue/
|
|
7
|
+
* ruby_xml_node.c) coerces+verifies arguments and maps the status codes below to
|
|
8
|
+
* exceptions.
|
|
9
|
+
*
|
|
10
|
+
* DETACH-NEVER-DESTROY: a removed node is unlinked, never freed - the arena owns
|
|
11
|
+
* node memory and a live Ruby wrapper may still alias a removed node (the same
|
|
12
|
+
* invariant the read-only reader relied on). New nodes append to the arena and
|
|
13
|
+
* count against the per-document byte/node budgets (fail-closed -> MKR_XML_MUT_OOM).
|
|
14
|
+
*
|
|
15
|
+
* SECURITY: names are validated as XML 1.0 QNames and values as XML Char, so the
|
|
16
|
+
* tree stays serializable to well-formed XML; an unbound prefix or a reserved
|
|
17
|
+
* name fails closed rather than producing a wrong/unserializable node.
|
|
18
|
+
*/
|
|
19
|
+
#ifndef MKR_XML_MUTATE_H
|
|
20
|
+
#define MKR_XML_MUTATE_H
|
|
21
|
+
|
|
22
|
+
#include <stdbool.h>
|
|
23
|
+
#include <stdint.h>
|
|
24
|
+
#include "mkr_xml_node.h"
|
|
25
|
+
|
|
26
|
+
/* Mutation outcome (the glue maps each to a Ruby exception). */
|
|
27
|
+
typedef enum {
|
|
28
|
+
MKR_XML_MUT_OK = 0,
|
|
29
|
+
MKR_XML_MUT_OOM, /* arena byte/node budget or allocation failure */
|
|
30
|
+
MKR_XML_MUT_BAD_NAME, /* not a well-formed XML 1.0 QName (or a reserved one) */
|
|
31
|
+
MKR_XML_MUT_BAD_CHARS, /* a non-XML-Char byte (§2.2) or a forbidden sequence
|
|
32
|
+
* for the context ("?>" in a PI, "--" in a comment,
|
|
33
|
+
* "]]>" in a CDATA section) */
|
|
34
|
+
MKR_XML_MUT_UNBOUND_NS, /* a prefix has no in-scope namespace binding */
|
|
35
|
+
MKR_XML_MUT_TYPE, /* the operation is invalid for this node type */
|
|
36
|
+
MKR_XML_MUT_CYCLE, /* the node would become a descendant of itself */
|
|
37
|
+
MKR_XML_MUT_HIERARCHY, /* invalid placement (attr/document child, second root, parentless ref) */
|
|
38
|
+
MKR_XML_MUT_BAD_NS_DECL /* a namespace declaration is invalid (xmlns:prefix bound to "") */
|
|
39
|
+
} mkr_xml_mut_status_t;
|
|
40
|
+
|
|
41
|
+
/* Detach +node+ from the tree: from its parent's child list, or - for an
|
|
42
|
+
* attribute - from its owner element's attribute list. No-op when already
|
|
43
|
+
* detached (no parent). The arena keeps the memory (detach-never-destroy); the
|
|
44
|
+
* caller clears doc->root if it detaches the root element. */
|
|
45
|
+
void mkr_xml_detach(mkr_xml_node_t *node);
|
|
46
|
+
|
|
47
|
+
/* Rename an element or attribute in place to the QName [name, name+nlen):
|
|
48
|
+
* validates it, copies it contiguously into the arena, re-splits prefix/local,
|
|
49
|
+
* and re-resolves the namespace against the node's in-scope declarations (the
|
|
50
|
+
* element itself for an element; the owner element for an attribute). The node
|
|
51
|
+
* pointer - and thus its identity and tree position - is preserved. */
|
|
52
|
+
mkr_xml_mut_status_t mkr_xml_rename(mkr_xml_doc_t *doc, mkr_xml_node_t *node,
|
|
53
|
+
const char *name, uint32_t nlen);
|
|
54
|
+
|
|
55
|
+
/* Set attribute [name,nlen]="[val,vlen]" on element +el+: replaces the value of
|
|
56
|
+
* an existing attribute with the same raw QName, otherwise appends a new
|
|
57
|
+
* attribute node. The namespace is resolved like the parser (xmlns / xmlns:* ->
|
|
58
|
+
* the xmlns namespace; the predefined xml: prefix -> the XML namespace; any other
|
|
59
|
+
* prefix -> its in-scope URI, else MKR_XML_MUT_UNBOUND_NS; unprefixed -> none).
|
|
60
|
+
* +val+ must be all XML Char. *out (may be NULL) receives the affected node. */
|
|
61
|
+
mkr_xml_mut_status_t mkr_xml_set_attribute(mkr_xml_doc_t *doc, mkr_xml_node_t *el,
|
|
62
|
+
const char *name, uint32_t nlen,
|
|
63
|
+
const char *val, uint32_t vlen,
|
|
64
|
+
mkr_xml_node_t **out);
|
|
65
|
+
|
|
66
|
+
/* Remove the first attribute of element +el+ whose raw QName is [name,nlen].
|
|
67
|
+
* Returns 1 if one was removed, 0 if none matched (detach-never-destroy). */
|
|
68
|
+
int mkr_xml_remove_attribute(mkr_xml_node_t *el, const char *name, uint32_t nlen);
|
|
69
|
+
|
|
70
|
+
/* Set +node+'s text content. For an ELEMENT: detach every child and append a
|
|
71
|
+
* single TEXT node holding [text,tlen] (an empty +text+ leaves an empty element).
|
|
72
|
+
* For a TEXT/CDATA/COMMENT/PI leaf: replace its value. +text+ must be all XML
|
|
73
|
+
* Char. Any other node type is MKR_XML_MUT_TYPE. */
|
|
74
|
+
mkr_xml_mut_status_t mkr_xml_set_content(mkr_xml_doc_t *doc, mkr_xml_node_t *node,
|
|
75
|
+
const char *text, uint32_t tlen);
|
|
76
|
+
|
|
77
|
+
/* ---- Phase 2: building new subtrees ---------------------------------------
|
|
78
|
+
*
|
|
79
|
+
* Factories build a DETACHED node in +doc+'s arena (no parent); its namespace is
|
|
80
|
+
* left UNRESOLVED (ns_uri NULL) until it is inserted, when the insertion site
|
|
81
|
+
* resolves the whole inserted subtree against its new in-scope declarations. */
|
|
82
|
+
|
|
83
|
+
/* A new detached ELEMENT named [name,nlen] (QName-validated + split). */
|
|
84
|
+
mkr_xml_mut_status_t mkr_xml_new_element(mkr_xml_doc_t *doc, const char *name, uint32_t nlen,
|
|
85
|
+
mkr_xml_node_t **out);
|
|
86
|
+
|
|
87
|
+
/* A new detached TEXT / CDATA_SECTION / COMMENT node holding [text,tlen]
|
|
88
|
+
* (validated as XML Char). +type+ selects which. */
|
|
89
|
+
mkr_xml_mut_status_t mkr_xml_new_chardata(mkr_xml_doc_t *doc, uint8_t type,
|
|
90
|
+
const char *text, uint32_t tlen,
|
|
91
|
+
mkr_xml_node_t **out);
|
|
92
|
+
|
|
93
|
+
/* A new detached PROCESSING_INSTRUCTION with target [target,tlen] (an NCName,
|
|
94
|
+
* not "xml" in any case) and data [data,dlen] (XML Char, may not contain "?>"). */
|
|
95
|
+
mkr_xml_mut_status_t mkr_xml_new_pi(mkr_xml_doc_t *doc, const char *target, uint32_t tlen,
|
|
96
|
+
const char *data, uint32_t dlen, mkr_xml_node_t **out);
|
|
97
|
+
|
|
98
|
+
/* Deep-copy +src+ and its whole subtree (attributes included) from its arena into
|
|
99
|
+
* +doc+'s arena, returning the detached copy in *out. Namespaces are left
|
|
100
|
+
* unresolved (the insertion site resolves them). Iterative (no recursion -> no
|
|
101
|
+
* stack DoS); fails closed on budget/OOM (the partial copy is abandoned in the
|
|
102
|
+
* arena and freed with the document). For moving a node BETWEEN documents. */
|
|
103
|
+
mkr_xml_mut_status_t mkr_xml_import_subtree(mkr_xml_doc_t *doc, const mkr_xml_node_t *src,
|
|
104
|
+
mkr_xml_node_t **out);
|
|
105
|
+
|
|
106
|
+
/* cloneNode: a detached deep (+deep+ true) or shallow copy of +src+ in the SAME
|
|
107
|
+
* document's arena, returned in *out (parent/siblings NULL). Unlike import the
|
|
108
|
+
* resolved namespace URI IS copied (a clone is never re-resolved), so the result
|
|
109
|
+
* keeps its namespace; element/attribute name case and the CDATA node type are
|
|
110
|
+
* preserved. Fails closed on budget/OOM. */
|
|
111
|
+
mkr_xml_mut_status_t mkr_xml_clone_node(mkr_xml_doc_t *doc, const mkr_xml_node_t *src,
|
|
112
|
+
bool deep, mkr_xml_node_t **out);
|
|
113
|
+
|
|
114
|
+
/* Insert +node+ into +doc+'s tree: as the last child of +parent+, or before /
|
|
115
|
+
* after the sibling +ref+, or in place of +ref+ (replace). +node+ MUST already
|
|
116
|
+
* live in +doc+'s arena (the caller imports a cross-document node first). Each:
|
|
117
|
+
* - validates placement first (no cycle, no attribute/document/doctype as a
|
|
118
|
+
* child, a single document-element root, a ref that has a parent) and
|
|
119
|
+
* resolves the inserted subtree's namespaces against the prospective context
|
|
120
|
+
* - ALL before any structural change, so a failure leaves the tree untouched
|
|
121
|
+
* (fully fail-closed, even for a move);
|
|
122
|
+
* - then detaches +node+ from any current position (move) and links it;
|
|
123
|
+
* - keeps doc->root in sync when the container is the document node.
|
|
124
|
+
* Detach-never-destroy: a replaced/displaced node is unlinked, never freed. */
|
|
125
|
+
mkr_xml_mut_status_t mkr_xml_insert_child (mkr_xml_doc_t *doc, mkr_xml_node_t *parent,
|
|
126
|
+
mkr_xml_node_t *node);
|
|
127
|
+
mkr_xml_mut_status_t mkr_xml_insert_before(mkr_xml_doc_t *doc, mkr_xml_node_t *ref,
|
|
128
|
+
mkr_xml_node_t *node);
|
|
129
|
+
mkr_xml_mut_status_t mkr_xml_insert_after (mkr_xml_doc_t *doc, mkr_xml_node_t *ref,
|
|
130
|
+
mkr_xml_node_t *node);
|
|
131
|
+
mkr_xml_mut_status_t mkr_xml_replace_node (mkr_xml_doc_t *doc, mkr_xml_node_t *ref,
|
|
132
|
+
mkr_xml_node_t *node);
|
|
133
|
+
|
|
134
|
+
/* Structural self-test (validation, namespace resolution, link/unlink). Returns 0
|
|
135
|
+
* on success or the 1-based index of the first failing check. Run from
|
|
136
|
+
* Makiri.__c_selftest. */
|
|
137
|
+
int mkr_xml_mutate_selftest(void);
|
|
138
|
+
|
|
139
|
+
#endif /* MKR_XML_MUTATE_H */
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/* mkr_xml_node.c - secure-by-design append-only arena + custom XML node.
|
|
2
|
+
* Ruby-free. See mkr_xml_node.h and docs/xml_parser_plan.ja.md §8.1/§8.2. */
|
|
3
|
+
#include "mkr_xml_node.h"
|
|
4
|
+
#include "mkr_xml.h"
|
|
5
|
+
#include "mkr_xml_index.h"
|
|
6
|
+
#include "../core/mkr_core.h"
|
|
7
|
+
|
|
8
|
+
#include <stddef.h> /* max_align_t */
|
|
9
|
+
#include <stdlib.h>
|
|
10
|
+
#include <string.h>
|
|
11
|
+
|
|
12
|
+
/* ASan red-zoning for the bump arena. Like Lexbor's mraw, our arena carves many
|
|
13
|
+
* sub-allocations out of one malloc'd chunk, so a write past one cut into the
|
|
14
|
+
* next stays inside that single malloc and is INVISIBLE to a plain ASan build
|
|
15
|
+
* (the heap red-zones only guard the chunk's outer boundary - this is exactly
|
|
16
|
+
* how the v3.0.0 :lexbor-contains overflow hid). So we poison each fresh chunk
|
|
17
|
+
* and unpoison only the bytes a cut actually hands out, leaving the alignment
|
|
18
|
+
* tail and not-yet-cut space poisoned: an intra-arena overrun then writes into
|
|
19
|
+
* poisoned memory and ASan reports it. Auto-active whenever our ext is built
|
|
20
|
+
* with -fsanitize=address (rake sanitize / fuzz:sanitize) - no extra flag,
|
|
21
|
+
* because unlike vendored Lexbor this is our own TU. A no-op otherwise. */
|
|
22
|
+
#if defined(__SANITIZE_ADDRESS__)
|
|
23
|
+
# define MKR_XML_HAVE_ASAN 1
|
|
24
|
+
#elif defined(__has_feature)
|
|
25
|
+
# if __has_feature(address_sanitizer)
|
|
26
|
+
# define MKR_XML_HAVE_ASAN 1
|
|
27
|
+
# endif
|
|
28
|
+
#endif
|
|
29
|
+
#if defined(MKR_XML_HAVE_ASAN)
|
|
30
|
+
# include <sanitizer/asan_interface.h>
|
|
31
|
+
# define MKR_ARENA_POISON(p, n) __asan_poison_memory_region((p), (n))
|
|
32
|
+
# define MKR_ARENA_UNPOISON(p, n) __asan_unpoison_memory_region((p), (n))
|
|
33
|
+
#else
|
|
34
|
+
# define MKR_ARENA_POISON(p, n) ((void)(p), (void)(n))
|
|
35
|
+
# define MKR_ARENA_UNPOISON(p, n) ((void)(p), (void)(n))
|
|
36
|
+
#endif
|
|
37
|
+
|
|
38
|
+
/* Alignment for arena cuts: the strictest fundamental alignment, so any object
|
|
39
|
+
* fits. The chunk payload starts at an aligned offset past the header, and every
|
|
40
|
+
* cut size is rounded up to ARENA_ALIGN, so each returned pointer is aligned. */
|
|
41
|
+
#define ARENA_ALIGN _Alignof(max_align_t)
|
|
42
|
+
_Static_assert((ARENA_ALIGN & (ARENA_ALIGN - 1)) == 0, "ARENA_ALIGN must be a power of two");
|
|
43
|
+
#define CHUNK_MIN (64u * 1024u)
|
|
44
|
+
|
|
45
|
+
struct mkr_xml_arena_chunk {
|
|
46
|
+
struct mkr_xml_arena_chunk *next;
|
|
47
|
+
size_t used, cap;
|
|
48
|
+
/* payload follows, at offset align_up(sizeof(chunk), ARENA_ALIGN) */
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
static inline size_t align_up(size_t n, size_t a) { return (n + (a - 1)) & ~(a - 1); }
|
|
52
|
+
|
|
53
|
+
mkr_xml_doc_t *
|
|
54
|
+
mkr_xml_doc_new(void)
|
|
55
|
+
{
|
|
56
|
+
mkr_xml_doc_t *doc = mkr_callocarray(1, sizeof *doc);
|
|
57
|
+
if (doc == NULL) return NULL;
|
|
58
|
+
doc->max_bytes = MKR_XML_MAX_BYTES;
|
|
59
|
+
doc->max_nodes = MKR_XML_MAX_NODES;
|
|
60
|
+
return doc;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
void
|
|
64
|
+
mkr_xml_doc_destroy(mkr_xml_doc_t *doc)
|
|
65
|
+
{
|
|
66
|
+
if (doc == NULL) return;
|
|
67
|
+
mkr_xml_name_index_invalidate(doc); /* free the heap-side element-name index */
|
|
68
|
+
/* whole-arena free: no individual node/byte free anywhere (read-only). */
|
|
69
|
+
const size_t hdr = align_up(sizeof(mkr_xml_arena_chunk_t), ARENA_ALIGN);
|
|
70
|
+
for (mkr_xml_arena_chunk_t *c = doc->chunks; c != NULL; ) {
|
|
71
|
+
mkr_xml_arena_chunk_t *n = c->next;
|
|
72
|
+
MKR_ARENA_UNPOISON((char *)c, hdr + c->cap); /* hand clean memory back to ASan's allocator */
|
|
73
|
+
free(c);
|
|
74
|
+
c = n;
|
|
75
|
+
}
|
|
76
|
+
free(doc);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
size_t
|
|
80
|
+
mkr_xml_doc_memsize(const mkr_xml_doc_t *doc)
|
|
81
|
+
{
|
|
82
|
+
if (doc == NULL) return 0;
|
|
83
|
+
size_t total = sizeof *doc;
|
|
84
|
+
const size_t hdr = align_up(sizeof(mkr_xml_arena_chunk_t), ARENA_ALIGN);
|
|
85
|
+
for (const mkr_xml_arena_chunk_t *c = doc->chunks; c != NULL; c = c->next) {
|
|
86
|
+
size_t chunk;
|
|
87
|
+
/* saturate rather than wrap: a bogus huge memsize is harmless, a wrapped
|
|
88
|
+
* small one would lie to Ruby's GC accounting. */
|
|
89
|
+
if (!mkr_size_add(hdr, c->cap, &chunk) || !mkr_size_add(total, chunk, &total)) {
|
|
90
|
+
return SIZE_MAX;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return total;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* THE single checked alloc choke point. PRIVATE - callers use the typed
|
|
97
|
+
* wrappers below. Overflow- and budget-checked; on any failure sets doc->oom
|
|
98
|
+
* (sticky) and returns NULL. Nothing else cuts arena. */
|
|
99
|
+
static void *
|
|
100
|
+
arena_alloc(mkr_xml_doc_t *doc, size_t size)
|
|
101
|
+
{
|
|
102
|
+
/* fail-closed contract: the internal callers never pass NULL, but a primitive
|
|
103
|
+
* must not deref it - return NULL rather than crash (there is no doc to mark). */
|
|
104
|
+
if (doc == NULL || doc->oom) return NULL;
|
|
105
|
+
|
|
106
|
+
size_t need = align_up(size, ARENA_ALIGN);
|
|
107
|
+
if (need < size) { doc->oom = MKR_XML_ERR_LIMIT; return NULL; } /* align overflow */
|
|
108
|
+
|
|
109
|
+
/* budget BEFORE allocation - fail-closed */
|
|
110
|
+
size_t projected;
|
|
111
|
+
if (!mkr_size_add(doc->arena_bytes, need, &projected) || projected > doc->max_bytes) {
|
|
112
|
+
doc->oom = MKR_XML_ERR_LIMIT;
|
|
113
|
+
return NULL;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const size_t hdr = align_up(sizeof(mkr_xml_arena_chunk_t), ARENA_ALIGN);
|
|
117
|
+
mkr_xml_arena_chunk_t *c = doc->chunks;
|
|
118
|
+
/* subtractive form (used <= cap invariant) avoids an addition that could
|
|
119
|
+
* overflow with a pathological chunk. */
|
|
120
|
+
if (c == NULL || need > c->cap - c->used) {
|
|
121
|
+
size_t cap = need > CHUNK_MIN ? need : CHUNK_MIN;
|
|
122
|
+
size_t total;
|
|
123
|
+
if (!mkr_size_add(hdr, cap, &total)) { doc->oom = MKR_XML_ERR_LIMIT; return NULL; }
|
|
124
|
+
mkr_xml_arena_chunk_t *nc = mkr_reallocarray(NULL, total, 1);
|
|
125
|
+
if (nc == NULL) { doc->oom = MKR_XML_ERR_OOM; return NULL; }
|
|
126
|
+
nc->next = doc->chunks;
|
|
127
|
+
nc->used = 0;
|
|
128
|
+
nc->cap = cap;
|
|
129
|
+
doc->chunks = nc;
|
|
130
|
+
c = nc;
|
|
131
|
+
/* poison the whole payload; each cut unpoisons only what it returns. */
|
|
132
|
+
MKR_ARENA_POISON((char *)nc + hdr, cap);
|
|
133
|
+
}
|
|
134
|
+
void *p = (char *)c + hdr + c->used;
|
|
135
|
+
c->used += need;
|
|
136
|
+
doc->arena_bytes += need;
|
|
137
|
+
/* Hand out exactly +size+ usable bytes; the [size, need) alignment tail (and
|
|
138
|
+
* everything past it in the chunk) stays poisoned as a red-zone, so a write
|
|
139
|
+
* one byte past the request is caught instead of silently clobbering the
|
|
140
|
+
* next cut. ASan tolerates the sub-granule trailing byte (ARENA_ALIGN cuts
|
|
141
|
+
* keep each slot on an 8-byte shadow boundary). */
|
|
142
|
+
MKR_ARENA_UNPOISON(p, size);
|
|
143
|
+
return p;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static int
|
|
147
|
+
valid_xn_type(mkr_xml_node_type_t type)
|
|
148
|
+
{
|
|
149
|
+
switch (type) {
|
|
150
|
+
case MKR_XML_NODE_TYPE_ELEMENT: case MKR_XML_NODE_TYPE_ATTRIBUTE: case MKR_XML_NODE_TYPE_TEXT:
|
|
151
|
+
case MKR_XML_NODE_TYPE_CDATA_SECTION: case MKR_XML_NODE_TYPE_PI: case MKR_XML_NODE_TYPE_COMMENT:
|
|
152
|
+
case MKR_XML_NODE_TYPE_DOCUMENT: case MKR_XML_NODE_TYPE_DOCUMENT_TYPE:
|
|
153
|
+
case MKR_XML_NODE_TYPE_DOCUMENT_FRAGMENT:
|
|
154
|
+
return 1;
|
|
155
|
+
default:
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
mkr_xml_node_t *
|
|
161
|
+
mkr_xml_arena_node(mkr_xml_doc_t *doc, mkr_xml_node_type_t type)
|
|
162
|
+
{
|
|
163
|
+
if (doc == NULL) return NULL; /* fail-closed: never deref a NULL document */
|
|
164
|
+
/* guard against a caller passing a bogus type (programming error) so it can
|
|
165
|
+
* never be silently miscounted/mis-walked - caught by the self-test/ASan. */
|
|
166
|
+
if (!valid_xn_type(type)) { doc->oom = MKR_XML_ERR_INTERNAL; return NULL; }
|
|
167
|
+
/* every node (attributes included) counts toward the single node budget; the
|
|
168
|
+
* per-element attribute cap is enforced by the tree builder. */
|
|
169
|
+
if (doc->nodes + 1 > doc->max_nodes) { doc->oom = MKR_XML_ERR_LIMIT; return NULL; }
|
|
170
|
+
|
|
171
|
+
mkr_xml_node_t *n = arena_alloc(doc, sizeof *n);
|
|
172
|
+
if (n == NULL) return NULL;
|
|
173
|
+
memset(n, 0, sizeof *n); /* zero-init: no uninitialised reads; 0 = "no source loc" */
|
|
174
|
+
n->type = type;
|
|
175
|
+
doc->nodes++;
|
|
176
|
+
return n;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const char *
|
|
180
|
+
mkr_xml_arena_bytes(mkr_xml_doc_t *doc, const char *src, uint32_t len)
|
|
181
|
+
{
|
|
182
|
+
if (len == 0) return ""; /* never read off the "" literal */
|
|
183
|
+
if (src == NULL) return NULL; /* contract: len>0 needs a real source (fail closed) */
|
|
184
|
+
char *p = arena_alloc(doc, len);
|
|
185
|
+
if (p == NULL) return NULL;
|
|
186
|
+
memcpy(p, src, len); /* copy-on-store: the arena owns the bytes */
|
|
187
|
+
return p;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* A raw, uninitialised arena buffer of exactly +len+ bytes. PRIVATE: the only
|
|
191
|
+
* way to fill arena bytes by hand is mkr_xml_arena_spanbuf below, which wraps
|
|
192
|
+
* this in a core mkr_spanbuf (cursor + bounds check) so a caller can never
|
|
193
|
+
* overrun it. Budget-/
|
|
194
|
+
* overflow-checked via the same choke point. */
|
|
195
|
+
static char *
|
|
196
|
+
arena_scratch_bytes(mkr_xml_doc_t *doc, size_t len)
|
|
197
|
+
{
|
|
198
|
+
/* a 0-length buffer needs no storage - never cut a whole chunk for it. The
|
|
199
|
+
* sentinel is a valid non-NULL pointer the caller must not write to (there is
|
|
200
|
+
* nothing to write), mirroring mkr_xml_arena_bytes's "" return. */
|
|
201
|
+
if (len == 0) return (char *)"";
|
|
202
|
+
return arena_alloc(doc, len);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* Carve +cap+ arena bytes and wrap them in a core mkr_spanbuf (see
|
|
206
|
+
* mkr_xml_node.h). The spanbuf owns the cursor + bounds check, so no caller can
|
|
207
|
+
* overrun the cut; this is the sole sanctioned hand-fill path and the only thing
|
|
208
|
+
* that touches the private arena_scratch_bytes. On alloc failure the buffer is
|
|
209
|
+
* NULL, so the returned writer is already not-ok (every write a no-op). */
|
|
210
|
+
mkr_spanbuf_t
|
|
211
|
+
mkr_xml_arena_spanbuf(mkr_xml_doc_t *doc, size_t cap)
|
|
212
|
+
{
|
|
213
|
+
return mkr_spanbuf(arena_scratch_bytes(doc, cap), cap);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
int
|
|
217
|
+
mkr_xml_xmlns_prefix(const char *name, uint32_t len, const char **prefix, uint32_t *plen)
|
|
218
|
+
{
|
|
219
|
+
const char *p; uint32_t pl;
|
|
220
|
+
mkr_span_t s = mkr_span(name, len);
|
|
221
|
+
if (len == 5 && mkr_span_starts(&s, "xmlns", 5)) { /* read through the span - no raw name deref (NULL-safe) */
|
|
222
|
+
p = ""; pl = 0;
|
|
223
|
+
} else if (len > 6 && mkr_span_starts(&s, "xmlns:", 6)) {
|
|
224
|
+
p = name + 6; pl = len - 6;
|
|
225
|
+
} else {
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
if (prefix != NULL) *prefix = p;
|
|
229
|
+
if (plen != NULL) *plen = pl;
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
int
|
|
234
|
+
mkr_xml_node_xmlns_decl(const mkr_xml_node_t *a, const char **prefix, uint32_t *plen,
|
|
235
|
+
const char **uri, uint32_t *ulen)
|
|
236
|
+
{
|
|
237
|
+
/* a built node's local IS qname+6 for "xmlns:p", so the byte detector's
|
|
238
|
+
* prefix slice equals the node's local - one shared rule, value read here. */
|
|
239
|
+
if (!mkr_xml_xmlns_prefix(a->qname, a->qname_len, prefix, plen)) return 0;
|
|
240
|
+
*uri = a->value ? a->value : ""; *ulen = a->value_len;
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
mkr_xml_node_t *
|
|
245
|
+
mkr_xml_preorder_next(const mkr_xml_node_t *root, mkr_xml_node_t *cur)
|
|
246
|
+
{
|
|
247
|
+
if (cur->first_child != NULL) return cur->first_child;
|
|
248
|
+
/* climb until a node with a next sibling, stopping at (and not above) root */
|
|
249
|
+
while (cur != root && cur->next == NULL) cur = cur->parent;
|
|
250
|
+
if (cur == root) return NULL;
|
|
251
|
+
return cur->next;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
int
|
|
255
|
+
mkr_xml_qname_split(const char *name, uint32_t len, mkr_xml_qname_t *out)
|
|
256
|
+
{
|
|
257
|
+
if (len == 0) return -1;
|
|
258
|
+
mkr_span_t s = mkr_span(name, len);
|
|
259
|
+
uint32_t cp;
|
|
260
|
+
int bl = mkr_utf8_decode1_span(&s, &cp);
|
|
261
|
+
if (bl == 0 || !mkr_xml_is_name_start(cp)) return -1;
|
|
262
|
+
mkr_span_skip(&s, (size_t)bl);
|
|
263
|
+
while (mkr_span_left(&s) > 0) {
|
|
264
|
+
bl = mkr_utf8_decode1_span(&s, &cp);
|
|
265
|
+
if (bl == 0 || !mkr_xml_is_name_char(cp)) return -1;
|
|
266
|
+
mkr_span_skip(&s, (size_t)bl);
|
|
267
|
+
}
|
|
268
|
+
out->qname = name; out->qname_len = len;
|
|
269
|
+
mkr_span_t q = mkr_span(name, len);
|
|
270
|
+
size_t colon_at;
|
|
271
|
+
if (!mkr_span_find(&q, ':', &colon_at)) { /* no prefix */
|
|
272
|
+
out->prefix = name; out->prefix_len = 0;
|
|
273
|
+
out->local = name; out->local_len = len;
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
uint32_t pl = (uint32_t)colon_at;
|
|
277
|
+
const char *ls = name + colon_at + 1;
|
|
278
|
+
uint32_t ll = len - pl - 1;
|
|
279
|
+
if (pl == 0 || ll == 0) return -1; /* ":x" or "x:" */
|
|
280
|
+
mkr_span_t lsp = mkr_span(ls, ll);
|
|
281
|
+
size_t second;
|
|
282
|
+
if (mkr_span_find(&lsp, ':', &second)) return -1; /* a second colon */
|
|
283
|
+
if (mkr_utf8_decode1_span(&lsp, &cp) == 0 || !mkr_xml_is_name_start(cp))
|
|
284
|
+
return -1; /* local must be an NCName */
|
|
285
|
+
out->prefix = name; out->prefix_len = pl;
|
|
286
|
+
out->local = ls; out->local_len = ll;
|
|
287
|
+
return 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
int
|
|
291
|
+
mkr_xml_qname_assign(mkr_xml_doc_t *doc, mkr_xml_node_t *node, const mkr_xml_qname_t *qn)
|
|
292
|
+
{
|
|
293
|
+
/* Contract: qn->local must alias INTO [qn->qname, qn->qname+qname_len) - the
|
|
294
|
+
* rebase below adds (local - qname) onto the arena copy, so a non-aliasing
|
|
295
|
+
* local would mint an out-of-bounds slice (wrong-but-plausible data, the worst
|
|
296
|
+
* failure mode here). Both in-tree producers (split_qname in mkr_xml_tree.c,
|
|
297
|
+
* mkr_xml_qname_split above) honour it; pin it so a violation fails closed
|
|
298
|
+
* (INTERNAL, sticky) instead of silently passing. */
|
|
299
|
+
if (!(qn->local >= qn->qname && qn->local_len <= qn->qname_len
|
|
300
|
+
&& (size_t)(qn->local - qn->qname) <= qn->qname_len - qn->local_len)) {
|
|
301
|
+
if (doc != NULL) doc->oom = MKR_XML_ERR_INTERNAL;
|
|
302
|
+
return -1;
|
|
303
|
+
}
|
|
304
|
+
const char *q = mkr_xml_arena_bytes(doc, qn->qname, qn->qname_len);
|
|
305
|
+
if (qn->qname_len > 0 && q == NULL) return -1; /* OOM: node left untouched */
|
|
306
|
+
node->qname = q; node->qname_len = qn->qname_len;
|
|
307
|
+
node->local = q + (size_t)(qn->local - qn->qname); node->local_len = qn->local_len;
|
|
308
|
+
node->prefix = q; node->prefix_len = qn->prefix_len;
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* ---- self-test (Makiri.__c_selftest) - mirrors tmp/xml_spike/arena_spike.c --- */
|
|
313
|
+
int
|
|
314
|
+
mkr_xml_node_selftest(void)
|
|
315
|
+
{
|
|
316
|
+
int idx = 0;
|
|
317
|
+
mkr_xml_doc_t *doc = mkr_xml_doc_new();
|
|
318
|
+
if (doc == NULL) return ++idx; /* 1 */
|
|
319
|
+
|
|
320
|
+
/* node zero-init + name copy */
|
|
321
|
+
idx++; /* 2 */
|
|
322
|
+
mkr_xml_node_t *root = mkr_xml_arena_node(doc, MKR_XML_NODE_TYPE_ELEMENT);
|
|
323
|
+
char nm[] = "Feed";
|
|
324
|
+
const char *local = mkr_xml_arena_bytes(doc, nm, 4);
|
|
325
|
+
if (root == NULL || root->first_child != NULL || root->type != MKR_XML_NODE_TYPE_ELEMENT
|
|
326
|
+
|| local == NULL || local == nm || !mkr_bytes_eq(local, 4, "Feed", 4)) {
|
|
327
|
+
mkr_xml_doc_destroy(doc); return idx;
|
|
328
|
+
}
|
|
329
|
+
/* pointer alignment */
|
|
330
|
+
idx++; /* 3 */
|
|
331
|
+
if (((uintptr_t)root % ARENA_ALIGN) != 0) { mkr_xml_doc_destroy(doc); return idx; }
|
|
332
|
+
|
|
333
|
+
/* build 1000 children */
|
|
334
|
+
idx++; /* 4 */
|
|
335
|
+
for (int i = 0; i < 1000; i++) {
|
|
336
|
+
mkr_xml_node_t *c = mkr_xml_arena_node(doc, MKR_XML_NODE_TYPE_ELEMENT);
|
|
337
|
+
if (c == NULL) { mkr_xml_doc_destroy(doc); return idx; }
|
|
338
|
+
c->parent = root;
|
|
339
|
+
if (root->last_child) { root->last_child->next = c; c->prev = root->last_child; }
|
|
340
|
+
else root->first_child = c;
|
|
341
|
+
root->last_child = c;
|
|
342
|
+
}
|
|
343
|
+
size_t cnt = 0;
|
|
344
|
+
for (mkr_xml_node_t *c = root->first_child; c != NULL; c = c->next) cnt++;
|
|
345
|
+
if (cnt != 1000 || doc->oom != MKR_XML_OK) { mkr_xml_doc_destroy(doc); return idx; }
|
|
346
|
+
mkr_xml_doc_destroy(doc);
|
|
347
|
+
|
|
348
|
+
/* pathological size fails closed (no overflow / wrap) */
|
|
349
|
+
idx++; /* 5 */
|
|
350
|
+
doc = mkr_xml_doc_new();
|
|
351
|
+
if (doc == NULL) return idx;
|
|
352
|
+
if (arena_alloc(doc, ((size_t)-1) - 8) != NULL || doc->oom == MKR_XML_OK) {
|
|
353
|
+
mkr_xml_doc_destroy(doc); return idx;
|
|
354
|
+
}
|
|
355
|
+
mkr_xml_doc_destroy(doc);
|
|
356
|
+
|
|
357
|
+
/* byte budget enforced inside the allocator (LIMIT before overrun) */
|
|
358
|
+
idx++; /* 6 */
|
|
359
|
+
doc = mkr_xml_doc_new();
|
|
360
|
+
if (doc == NULL) return idx;
|
|
361
|
+
doc->max_bytes = 4096;
|
|
362
|
+
int hit = 0;
|
|
363
|
+
for (int i = 0; i < 100000; i++) {
|
|
364
|
+
if (mkr_xml_arena_node(doc, MKR_XML_NODE_TYPE_ELEMENT) == NULL) {
|
|
365
|
+
hit = (doc->oom == MKR_XML_ERR_LIMIT);
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (!hit) { mkr_xml_doc_destroy(doc); return idx; }
|
|
370
|
+
mkr_xml_doc_destroy(doc);
|
|
371
|
+
|
|
372
|
+
/* node budget enforced */
|
|
373
|
+
idx++; /* 7 */
|
|
374
|
+
doc = mkr_xml_doc_new();
|
|
375
|
+
if (doc == NULL) return idx;
|
|
376
|
+
doc->max_nodes = 10;
|
|
377
|
+
int nlimit = 0;
|
|
378
|
+
for (int i = 0; i < 100; i++) {
|
|
379
|
+
if (mkr_xml_arena_node(doc, MKR_XML_NODE_TYPE_ELEMENT) == NULL) {
|
|
380
|
+
nlimit = (doc->oom == MKR_XML_ERR_LIMIT);
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (!nlimit) { mkr_xml_doc_destroy(doc); return idx; }
|
|
385
|
+
mkr_xml_doc_destroy(doc);
|
|
386
|
+
|
|
387
|
+
/* fail-closed on a NULL document (contract guard, no deref/crash) */
|
|
388
|
+
idx++; /* 8 */
|
|
389
|
+
if (mkr_xml_arena_node(NULL, MKR_XML_NODE_TYPE_ELEMENT) != NULL
|
|
390
|
+
|| mkr_xml_arena_bytes(NULL, "x", 1) != NULL
|
|
391
|
+
|| mkr_xml_arena_spanbuf(NULL, 1).ok) { /* alloc fails -> not ok */
|
|
392
|
+
return idx;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return 0; /* all checks passed */
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/* mkr_xml_parse now lives in mkr_xml_tree.c (the minimal tokenizer + tree
|
|
399
|
+
* builder). This file owns only the node/arena primitives. */
|