makiri 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/conformance.yml +22 -0
  3. data/.github/workflows/libfuzzer.yml +83 -0
  4. data/.github/workflows/release.yml +12 -7
  5. data/.github/workflows/security.yml +88 -3
  6. data/.github/workflows/valgrind.yml +135 -0
  7. data/CHANGELOG.md +152 -15
  8. data/README.md +183 -13
  9. data/Rakefile +294 -7
  10. data/ext/makiri/bridge/bridge.h +28 -0
  11. data/ext/makiri/bridge/ruby_string.c +282 -12
  12. data/ext/makiri/core/mkr_alloc.c +40 -3
  13. data/ext/makiri/core/mkr_alloc.h +28 -5
  14. data/ext/makiri/core/mkr_buf.c +47 -3
  15. data/ext/makiri/core/mkr_buf.h +112 -3
  16. data/ext/makiri/core/mkr_core.c +143 -0
  17. data/ext/makiri/core/mkr_core.h +11 -2
  18. data/ext/makiri/core/mkr_hash.h +1 -1
  19. data/ext/makiri/core/mkr_span.h +186 -0
  20. data/ext/makiri/core/mkr_text.h +8 -8
  21. data/ext/makiri/core/mkr_utf8.c +101 -0
  22. data/ext/makiri/core/mkr_utf8.h +88 -0
  23. data/ext/makiri/extconf.rb +123 -10
  24. data/ext/makiri/fuzz/Makefile +95 -0
  25. data/ext/makiri/fuzz/check_fuzzer.cc +4 -0
  26. data/ext/makiri/fuzz/xml_fuzz.c +24 -0
  27. data/ext/makiri/fuzz/xpath_fuzz.c +109 -0
  28. data/ext/makiri/glue/glue.h +55 -11
  29. data/ext/makiri/glue/ruby_doc.c +129 -59
  30. data/ext/makiri/glue/ruby_html_css.c +292 -0
  31. data/ext/makiri/glue/{ruby_mutate.c → ruby_html_mutate.c} +248 -52
  32. data/ext/makiri/glue/ruby_html_node.c +859 -0
  33. data/ext/makiri/glue/ruby_html_serialize.c +154 -0
  34. data/ext/makiri/glue/ruby_node.c +74 -729
  35. data/ext/makiri/glue/ruby_node_set.c +167 -32
  36. data/ext/makiri/glue/ruby_xml.c +602 -0
  37. data/ext/makiri/glue/ruby_xml_node.c +1373 -0
  38. data/ext/makiri/glue/ruby_xpath.c +63 -30
  39. data/ext/makiri/glue/ruby_xpath.h +19 -0
  40. data/ext/makiri/lexbor_compat/compat.h +42 -9
  41. data/ext/makiri/lexbor_compat/compat_internal.h +1 -1
  42. data/ext/makiri/lexbor_compat/dom_index.c +2 -2
  43. data/ext/makiri/lexbor_compat/post_parse.c +100 -10
  44. data/ext/makiri/lexbor_compat/source_loc.c +15 -13
  45. data/ext/makiri/lexbor_compat/text_index.c +14 -8
  46. data/ext/makiri/lexbor_compat/utf8_input.c +19 -33
  47. data/ext/makiri/makiri.c +184 -6
  48. data/ext/makiri/makiri.h +43 -2
  49. data/ext/makiri/xml/mkr_xml.h +125 -0
  50. data/ext/makiri/xml/mkr_xml_chars.c +195 -0
  51. data/ext/makiri/xml/mkr_xml_index.c +169 -0
  52. data/ext/makiri/xml/mkr_xml_index.h +48 -0
  53. data/ext/makiri/xml/mkr_xml_mutate.c +817 -0
  54. data/ext/makiri/xml/mkr_xml_mutate.h +139 -0
  55. data/ext/makiri/xml/mkr_xml_node.c +399 -0
  56. data/ext/makiri/xml/mkr_xml_node.h +184 -0
  57. data/ext/makiri/xml/mkr_xml_tree.c +1515 -0
  58. data/ext/makiri/xpath/mkr_css.c +1023 -0
  59. data/ext/makiri/xpath/mkr_css.h +65 -0
  60. data/ext/makiri/xpath/mkr_xpath.c +96 -32
  61. data/ext/makiri/xpath/mkr_xpath.h +109 -4
  62. data/ext/makiri/xpath/mkr_xpath_engine_html.c +17 -0
  63. data/ext/makiri/xpath/mkr_xpath_engine_xml.c +12 -0
  64. data/ext/makiri/xpath/{mkr_xpath_eval.c → mkr_xpath_eval_body.h} +551 -241
  65. data/ext/makiri/xpath/{mkr_xpath_funcs.c → mkr_xpath_funcs_body.h} +318 -276
  66. data/ext/makiri/xpath/mkr_xpath_internal.h +177 -206
  67. data/ext/makiri/xpath/mkr_xpath_lex.c +95 -125
  68. data/ext/makiri/xpath/mkr_xpath_node_access_html.h +138 -0
  69. data/ext/makiri/xpath/mkr_xpath_node_access_xml.h +145 -0
  70. data/ext/makiri/xpath/mkr_xpath_number.c +109 -0
  71. data/ext/makiri/xpath/mkr_xpath_parse.c +83 -94
  72. data/ext/makiri/xpath/mkr_xpath_prelude_html.h +30 -0
  73. data/ext/makiri/xpath/mkr_xpath_prelude_xml.h +28 -0
  74. data/ext/makiri/xpath/mkr_xpath_shared.c +609 -0
  75. data/ext/makiri/xpath/mkr_xpath_value_body.h +801 -0
  76. data/ext/makiri/xpath/mkr_xpath_xml_selftest.c +76 -0
  77. data/lib/makiri/{attribute.rb → attr.rb} +7 -3
  78. data/lib/makiri/cdata_section.rb +19 -0
  79. data/lib/makiri/comment.rb +10 -0
  80. data/lib/makiri/compat_aliases.rb +30 -0
  81. data/lib/makiri/document.rb +9 -73
  82. data/lib/makiri/document_fragment.rb +14 -9
  83. data/lib/makiri/element.rb +4 -4
  84. data/lib/makiri/html/document.rb +106 -0
  85. data/lib/makiri/html/node_methods.rb +19 -0
  86. data/lib/makiri/html.rb +12 -0
  87. data/lib/makiri/node.rb +58 -15
  88. data/lib/makiri/node_set.rb +8 -0
  89. data/lib/makiri/processing_instruction.rb +10 -0
  90. data/lib/makiri/text.rb +1 -1
  91. data/lib/makiri/version.rb +1 -1
  92. data/lib/makiri/xml/builder.rb +263 -0
  93. data/lib/makiri/xml/document.rb +24 -0
  94. data/lib/makiri/xml/node_methods.rb +84 -0
  95. data/lib/makiri/xml.rb +10 -0
  96. data/lib/makiri/xpath_context.rb +1 -1
  97. data/lib/makiri.rb +24 -5
  98. data/script/build_native_gem.rb +2 -2
  99. data/script/check_alloc_failures.rb +266 -0
  100. data/script/check_c_safety.rb +77 -2
  101. data/script/check_c_safety_allowlist.yml +102 -0
  102. data/script/check_leaks.rb +64 -0
  103. data/script/leaks_harness.rb +64 -0
  104. data/vendor/lexbor/CMakeLists.txt +6 -0
  105. data/vendor/lexbor/README.md +12 -0
  106. data/vendor/lexbor/config.cmake +1 -1
  107. data/vendor/lexbor/source/lexbor/core/base.h +1 -1
  108. data/vendor/lexbor/source/lexbor/core/config.cmake +9 -1
  109. data/vendor/lexbor/source/lexbor/css/selectors/pseudo_state.c +2 -3
  110. data/vendor/lexbor/source/lexbor/css/selectors/state.c +3 -0
  111. data/vendor/lexbor/source/lexbor/dom/interfaces/element.c +21 -0
  112. data/vendor/lexbor/source/lexbor/dom/interfaces/element.h +5 -0
  113. data/vendor/lexbor/source/lexbor/encoding/decode.c +33 -4
  114. data/vendor/lexbor/source/lexbor/html/base.h +1 -1
  115. data/vendor/lexbor/source/lexbor/html/interfaces/select_element.c +4 -0
  116. data/vendor/lexbor/source/lexbor/html/serialize.c +545 -41
  117. data/vendor/lexbor/source/lexbor/html/serialize.h +2 -1
  118. data/vendor/lexbor/source/lexbor/html/tokenizer.h +2 -2
  119. data/vendor/lexbor/source/lexbor/html/tree/insertion_mode/in_body.c +1 -1
  120. data/vendor/lexbor/source/lexbor/html/tree.c +6 -6
  121. data/vendor/lexbor/source/lexbor/selectors/selectors.c +12 -3
  122. data/vendor/lexbor/source/lexbor/url/base.h +1 -1
  123. data/vendor/lexbor/source/lexbor/url/url.c +5 -2
  124. data/vendor/lexbor/source/lexbor/url/url.h +9 -0
  125. data/vendor/lexbor/version +1 -1
  126. metadata +53 -9
  127. data/ext/makiri/glue/ruby_css.c +0 -185
  128. data/ext/makiri/glue/ruby_serialize.c +0 -92
  129. data/ext/makiri/xpath/mkr_xpath_value.c +0 -1286
  130. data/lib/makiri/cdata.rb +0 -6
@@ -0,0 +1,65 @@
1
+ #ifndef MKR_CSS_H
2
+ #define MKR_CSS_H
3
+
4
+ #include "mkr_xpath.h" /* mkr_node_t, mkr_xpath_limits_t, mkr_xpath_error_t, mkr_verified_text_t */
5
+
6
+ #ifdef __cplusplus
7
+ extern "C" {
8
+ #endif
9
+
10
+ /*
11
+ * CSS selector front-end for the native XPath engine.
12
+ *
13
+ * Rather than convert CSS to an XPath *string* and re-parse it, this lowers a
14
+ * CSS selector directly into the engine's compiled AST (the same mkr_node_t that
15
+ * mkr_parse produces), which is then run by mkr_xpath_eval_compiled[_first].
16
+ * The whole evaluation - per-evaluate budgets, document order, dedup, namespace
17
+ * resolution - is therefore shared with XPath, and CSS gains no new evaluator
18
+ * opcodes: every supported selector lowers to existing PATH/step/predicate nodes
19
+ * (name tests + contains/starts-with/substring/count/position/not + arithmetic).
20
+ *
21
+ * The CSS *parsing* reuses Lexbor's selector parser (which, unlike its matcher,
22
+ * preserves name case), so this layer only walks the parsed selector list and
23
+ * builds AST. The result AST is owned by the caller and freed with mkr_node_free,
24
+ * exactly like an mkr_parse result.
25
+ *
26
+ * Namespaces (Nokogiri-compatible): a bare type selector binds to the document's
27
+ * DEFAULT namespace when one is in scope. The glue collects the in-scope
28
+ * namespaces (Ruby side, mirroring Nokogiri's `document.namespaces`), registers
29
+ * them on the context so the engine resolves prefixes at eval, and registers the
30
+ * default namespace under the synthetic prefix "xmlns" (Nokogiri's convention).
31
+ * +default_prefix+ is that synthetic prefix ("xmlns") when a default namespace is
32
+ * in scope, or NULL otherwise; lowering uses it to emit name tests:
33
+ * - bare `el` -> prefix = default_prefix (when set), else no-namespace
34
+ * - `p|el` -> prefix = "p" (resolved by the engine; unbound -> error)
35
+ * - `*|el` -> any namespace (nodetest wildcard + local-name() predicate)
36
+ * - `|el` -> no namespace
37
+ */
38
+ /* The synthetic prefix bound to the document's default namespace (Nokogiri's
39
+ * convention). The glue registers this prefix -> default-ns URI on the context. */
40
+ #define MKR_CSS_DEFAULT_NS_PREFIX "xmlns"
41
+
42
+ typedef struct {
43
+ const char *default_prefix; /* MKR_CSS_DEFAULT_NS_PREFIX when a default ns is in scope, else NULL */
44
+ } mkr_css_ns_t;
45
+
46
+ /*
47
+ * Compile +selector+ into a freshly allocated AST (caller frees with
48
+ * mkr_node_free). Returns NULL on error with *err filled:
49
+ * MKR_XPATH_ERR_SYNTAX - malformed selector or an unsupported construct
50
+ * (jQuery extensions, pseudo-elements, the case
51
+ * modifier)
52
+ * MKR_XPATH_ERR_OOM / _LIMIT - allocation failure / selector-complexity cap
53
+ * +ns+ may be NULL (bare selectors then match no-namespace). +limits+ bounds the
54
+ * AST node count.
55
+ */
56
+ mkr_node_t *mkr_css_compile(mkr_verified_text_t selector,
57
+ const mkr_css_ns_t *ns,
58
+ mkr_xpath_limits_t *limits,
59
+ mkr_xpath_error_t *err);
60
+
61
+ #ifdef __cplusplus
62
+ }
63
+ #endif
64
+
65
+ #endif /* MKR_CSS_H */
@@ -9,7 +9,7 @@
9
9
  #include <string.h>
10
10
 
11
11
  /*
12
- * Native XPath engine top-level: context lifetime, namespace + variable
12
+ * Native XPath engine - top-level: context lifetime, namespace + variable
13
13
  * registries, the eval entry point that drives parser + evaluator.
14
14
  *
15
15
  * Phase 1 builds the engine up to a working subset (see plan file).
@@ -27,8 +27,8 @@ typedef struct {
27
27
  } mkr_var_entry_t;
28
28
 
29
29
  struct mkr_xpath_context_s {
30
- lxb_dom_document_t *doc;
31
- lxb_dom_node_t *node;
30
+ MKR_DOM_DOCUMENT *doc;
31
+ MKR_DOM_NODE *node;
32
32
 
33
33
  mkr_ns_entry_t *ns;
34
34
  size_t ns_count;
@@ -60,6 +60,14 @@ struct mkr_xpath_context_s {
60
60
  mkr_tag_index_lookup_t tag_lookup;
61
61
  mkr_tag_index_foreign_t tag_has_foreign;
62
62
 
63
+ /* XML element-name index: the owning document plus a lazy getter and a
64
+ * string-keyed lookup, injected by the XML glue. NULL disables the
65
+ * descendant-name fast path (the engine then walks). HTML uses the tag-id
66
+ * index above instead. */
67
+ void *name_index_owner;
68
+ mkr_name_index_get_t name_index_get;
69
+ mkr_name_index_lookup_t name_index_lookup;
70
+
63
71
  /* Namespace-matching policy for UNPREFIXED name tests. 0 (default) =
64
72
  * strict/HTML5-faithful: an unprefixed name resolves in the HTML namespace
65
73
  * (matches HTML-namespace or no-namespace nodes only; foreign SVG/MathML
@@ -67,8 +75,36 @@ struct mkr_xpath_context_s {
67
75
  * tests and wildcards are unaffected. Set by the glue from the
68
76
  * namespace_matching: option. */
69
77
  int unprefixed_lax;
78
+
79
+ /* Which monomorphized engine instance walks this context's nodes: 0 = HTML
80
+ * (lxb_dom), 1 = XML (custom node). Set by the glue at context creation. Only
81
+ * the two node-dereferencing entries (eval, first-match) dispatch on it; all
82
+ * other per-evaluate machinery is representation-neutral (pointer/string only),
83
+ * so it runs the HTML copy for both. */
84
+ int engine_kind;
70
85
  };
71
86
 
87
+ /* The two node-dereferencing engine ENTRY points, one pair per monomorphized
88
+ * instance (mkr_xpath_engine_{html,xml}.c - the bodies suffix them _html / _xml
89
+ * via the prelude). The driver holds them as void node pointers and dispatches
90
+ * on ctx->engine_kind; every other per-evaluate helper is representation-neutral
91
+ * and shared (mkr_xpath_shared.c), called by its bare name. The signatures are
92
+ * node-pointer-only, hence ABI-identical across the two instances. */
93
+ int mkr_eval_ast_html(mkr_xpath_context_t *ctx, const mkr_node_t *ast,
94
+ mkr_val_t *out, mkr_xpath_error_t *err);
95
+ int mkr_eval_ast_xml (mkr_xpath_context_t *ctx, const mkr_node_t *ast,
96
+ mkr_val_t *out, mkr_xpath_error_t *err);
97
+ int mkr_try_first_match_html(mkr_xpath_context_t *ctx, const mkr_node_t *ast,
98
+ MKR_DOM_NODE **out_node, mkr_xpath_error_t *err);
99
+ int mkr_try_first_match_xml (mkr_xpath_context_t *ctx, const mkr_node_t *ast,
100
+ MKR_DOM_NODE **out_node, mkr_xpath_error_t *err);
101
+
102
+ void
103
+ mkr_xpath_set_engine_kind(mkr_xpath_context_t *ctx, int kind)
104
+ {
105
+ if (ctx) ctx->engine_kind = kind ? 1 : 0;
106
+ }
107
+
72
108
  mkr_doc_order_index_t *
73
109
  mkr_ctx_order_index(mkr_xpath_context_t *ctx)
74
110
  {
@@ -104,6 +140,35 @@ mkr_ctx_tag_has_foreign(mkr_xpath_context_t *ctx)
104
140
  return ctx ? ctx->tag_has_foreign : NULL;
105
141
  }
106
142
 
143
+ void
144
+ mkr_xpath_context_set_name_index(mkr_xpath_context_t *ctx, void *owner,
145
+ mkr_name_index_get_t get,
146
+ mkr_name_index_lookup_t lookup)
147
+ {
148
+ if (ctx == NULL) return;
149
+ ctx->name_index_owner = owner;
150
+ ctx->name_index_get = get;
151
+ ctx->name_index_lookup = lookup;
152
+ }
153
+
154
+ void *
155
+ mkr_ctx_name_index_owner(mkr_xpath_context_t *ctx)
156
+ {
157
+ return ctx ? ctx->name_index_owner : NULL;
158
+ }
159
+
160
+ mkr_name_index_get_t
161
+ mkr_ctx_name_index_get(mkr_xpath_context_t *ctx)
162
+ {
163
+ return ctx ? ctx->name_index_get : NULL;
164
+ }
165
+
166
+ mkr_name_index_lookup_t
167
+ mkr_ctx_name_index_lookup(mkr_xpath_context_t *ctx)
168
+ {
169
+ return ctx ? ctx->name_index_lookup : NULL;
170
+ }
171
+
107
172
  mkr_str_cache_t *
108
173
  mkr_ctx_str_cache(mkr_xpath_context_t *ctx)
109
174
  {
@@ -163,7 +228,7 @@ mkr_xpath_limits_init_defaults(mkr_xpath_limits_t *L)
163
228
  L->max_steps = 256; /* path step count */
164
229
  L->max_predicates = 64; /* per-step predicates */
165
230
  L->max_function_args = 64;
166
- L->max_nodeset_size = 10 * 1000 * 1000; /* 10M nodes large but bounded */
231
+ L->max_nodeset_size = 10 * 1000 * 1000; /* 10M nodes - large but bounded */
167
232
  L->max_eval_ops = 50 * 1000 * 1000; /* 50M evaluator steps */
168
233
  L->max_string_bytes = 64 * 1024 * 1024; /* 64 MB string-value */
169
234
  L->max_recursion_depth = 256;
@@ -272,29 +337,10 @@ mkr_limit_check_expr_bytes(mkr_xpath_limits_t *L, size_t bytes, mkr_xpath_error_
272
337
 
273
338
  /* ---------- node qualified name ---------- */
274
339
 
275
- /*
276
- * Borrowed qualified name for any node. HTML elements report their
277
- * lowercase local name (lxb_dom_element_qualified_name), matching
278
- * Makiri::Node#name and the lowercase-tag HTML data model the engine
279
- * assumes; every other node kind defers to lxb_dom_node_name.
280
- */
281
- const lxb_char_t *
282
- mkr_dom_node_name_qualified(lxb_dom_node_t *node, size_t *len)
283
- {
284
- if (node == NULL) {
285
- if (len) *len = 0;
286
- return NULL;
287
- }
288
- if (node->type == LXB_DOM_NODE_TYPE_ELEMENT) {
289
- return lxb_dom_element_qualified_name(lxb_dom_interface_element(node), len);
290
- }
291
- return lxb_dom_node_name(node, len);
292
- }
293
-
294
340
  /* ---------- context ---------- */
295
341
 
296
342
  mkr_xpath_context_t *
297
- mkr_xpath_context_new(lxb_dom_document_t *doc, lxb_dom_node_t *node)
343
+ mkr_xpath_context_new(MKR_DOM_DOCUMENT *doc, MKR_DOM_NODE *node)
298
344
  {
299
345
  mkr_xpath_context_t *ctx = mkr_callocarray(1, sizeof(*ctx));
300
346
  if (ctx == NULL) {
@@ -447,19 +493,19 @@ mkr_ctx_lookup_variable_text(mkr_xpath_context_t *ctx, const char *prefix,
447
493
  return 0;
448
494
  }
449
495
 
450
- lxb_dom_node_t *
496
+ MKR_DOM_NODE *
451
497
  mkr_ctx_node(mkr_xpath_context_t *ctx)
452
498
  {
453
499
  return ctx ? ctx->node : NULL;
454
500
  }
455
501
 
456
502
  void
457
- mkr_ctx_set_node(mkr_xpath_context_t *ctx, lxb_dom_node_t *node)
503
+ mkr_ctx_set_node(mkr_xpath_context_t *ctx, MKR_DOM_NODE *node)
458
504
  {
459
505
  if (ctx) ctx->node = node;
460
506
  }
461
507
 
462
- lxb_dom_document_t *
508
+ MKR_DOM_DOCUMENT *
463
509
  mkr_ctx_document(mkr_xpath_context_t *ctx)
464
510
  {
465
511
  return ctx ? ctx->doc : NULL;
@@ -563,7 +609,7 @@ mkr_xpath_eval_compiled(mkr_xpath_context_t *ctx, mkr_node_t *ast,
563
609
 
564
610
  /* String-value cache snapshot. Nested eval (handler that calls back
565
611
  * into XPath on the same context) sees outer entries but anything it
566
- * adds is discarded on return outer borrowed pointers stay valid. */
612
+ * adds is discarded on return - outer borrowed pointers stay valid. */
567
613
  size_t str_cache_snapshot = ctx->str_cache.count;
568
614
  /* Document-order index: outermost evaluate owns the lifecycle. If the
569
615
  * outer wasn't built, the inner's build (if any) gets cleared at this
@@ -572,13 +618,14 @@ mkr_xpath_eval_compiled(mkr_xpath_context_t *ctx, mkr_node_t *ast,
572
618
  int order_was_built = ctx->order_index.built;
573
619
 
574
620
  mkr_val_t v = {0};
575
- int eval_rc = mkr_eval_ast(ctx, ast, &v, &err);
621
+ int eval_rc = ctx->engine_kind ? mkr_eval_ast_xml(ctx, ast, &v, &err)
622
+ : mkr_eval_ast_html(ctx, ast, &v, &err);
576
623
  mkr_str_cache_truncate(&ctx->str_cache, str_cache_snapshot);
577
624
  if (!order_was_built && ctx->order_index.built) {
578
625
  mkr_doc_order_index_clear(&ctx->order_index);
579
626
  }
580
627
  /* Clear memoized values regardless of success: they are valid only
581
- * within a single evaluate scope. Defensive also clears on error. */
628
+ * within a single evaluate scope. Defensive - also clears on error. */
582
629
  mkr_node_clear_memos(ast);
583
630
  if (eval_rc != 0) {
584
631
  if (out_error) *out_error = err; else mkr_xpath_error_clear(&err);
@@ -600,8 +647,25 @@ mkr_xpath_eval_compiled_first(mkr_xpath_context_t *ctx, mkr_node_t *ast,
600
647
  }
601
648
  return -1;
602
649
  }
603
- lxb_dom_node_t *node = NULL;
604
- if (mkr_try_first_match(ctx, ast, &node)) {
650
+ /* Reset the per-evaluate counters before the first-match fast path: its
651
+ * descendant walk charges every visited node to max_eval_ops, so it is
652
+ * bounded fail-closed exactly like the full evaluator (which resets these in
653
+ * mkr_xpath_eval_compiled). The not-recognised fallback below resets them
654
+ * again; the walk only runs for recognised shapes, so nothing double-counts. */
655
+ ctx->limits.eval_ops = 0;
656
+ ctx->limits.recursion_depth = 0;
657
+
658
+ MKR_DOM_NODE *node = NULL;
659
+ mkr_xpath_error_t err = {0};
660
+ int matched = ctx->engine_kind ? mkr_try_first_match_xml(ctx, ast, &node, &err)
661
+ : mkr_try_first_match_html(ctx, ast, &node, &err);
662
+ if (matched < 0) {
663
+ /* Op budget exceeded while walking - fail closed (do NOT fall back to the
664
+ * full evaluator, which would hit the same wall). */
665
+ if (out_error) *out_error = err; else mkr_xpath_error_clear(&err);
666
+ return -1;
667
+ }
668
+ if (matched) {
605
669
  /* Recognised first-match shape: return a 0-or-1-node node-set without
606
670
  * building (or sorting) the full descendant set. */
607
671
  mkr_val_t v = {0};
@@ -17,7 +17,7 @@ extern "C" {
17
17
  *
18
18
  * Security: every evaluate enforces per-call budgets (op count, recursion
19
19
  * depth, node-set / string caps); any overrun fails closed with
20
- * MKR_XPATH_ERR_LIMIT never a truncated or wrong result.
20
+ * MKR_XPATH_ERR_LIMIT - never a truncated or wrong result.
21
21
  */
22
22
 
23
23
  typedef enum {
@@ -45,7 +45,10 @@ typedef struct {
45
45
  mkr_xpath_type_t type;
46
46
  union {
47
47
  struct {
48
- lxb_dom_node_t **nodes;
48
+ /* Representation-agnostic node pointers: lxb_dom_node_t* for an HTML
49
+ * document, mkr_xml_node_t* for an XML one. The glue knows the kind and
50
+ * casts; the engine boundary stays neutral. */
51
+ void **nodes;
49
52
  size_t count;
50
53
  } nodeset;
51
54
  mkr_owned_text_t string; /* owned; valid when type == MKR_XPATH_TYPE_STRING */
@@ -59,7 +62,10 @@ typedef struct {
59
62
  char *message; /* heap-allocated, freed with mkr_xpath_error_free */
60
63
  } mkr_xpath_error_t;
61
64
 
62
- mkr_xpath_context_t *mkr_xpath_context_new(lxb_dom_document_t *doc, lxb_dom_node_t *node);
65
+ /* Create a context for +node+ in +doc+ (representation-agnostic void* - the glue
66
+ * passes lxb_dom_* for HTML, mkr_xml_* for XML; the engine kind is selected via
67
+ * mkr_xpath_set_engine_kind). */
68
+ mkr_xpath_context_t *mkr_xpath_context_new(void *doc, void *node);
63
69
  void mkr_xpath_context_free(mkr_xpath_context_t *ctx);
64
70
 
65
71
  int mkr_xpath_register_ns(mkr_xpath_context_t *ctx, mkr_verified_text_t prefix, mkr_verified_text_t uri);
@@ -108,7 +114,7 @@ void mkr_xpath_set_func_resolver (mkr_xpath_context_t *ctx, mkr_func_resolver
108
114
 
109
115
  /*
110
116
  * Element-index hooks, injected by the glue layer alongside an opaque index
111
- * pointer. The engine never sees the index's concrete type it only calls
117
+ * pointer. The engine never sees the index's concrete type - it only calls
112
118
  * back through these, exactly as it calls Ruby only through the func resolver
113
119
  * above. This keeps the engine free of the lexbor_compat headers.
114
120
  *
@@ -129,10 +135,109 @@ void mkr_xpath_context_set_element_index(mkr_xpath_context_t *ctx, void *index,
129
135
  mkr_tag_index_lookup_t lookup,
130
136
  mkr_tag_index_foreign_t has_foreign);
131
137
 
138
+ /* XML element-name index hooks (string-keyed; the HTML tag-id index above is
139
+ * integer-keyed). get(owner) lazily builds + caches the index on the owning
140
+ * document and returns it (or NULL on OOM -> the engine walks); lookup returns
141
+ * the document-ordered bucket for (local, ns_uri). Node pointers are void*. */
142
+ typedef void *(*mkr_name_index_get_t)(void *owner);
143
+ typedef void *const *(*mkr_name_index_lookup_t)(const void *index,
144
+ const char *local, size_t local_len,
145
+ const char *ns_uri, size_t ns_uri_len,
146
+ size_t *count);
147
+ void mkr_xpath_context_set_name_index(mkr_xpath_context_t *ctx, void *owner,
148
+ mkr_name_index_get_t get,
149
+ mkr_name_index_lookup_t lookup);
150
+
132
151
 
133
152
  void mkr_xpath_value_clear(mkr_xpath_value_t *v);
134
153
  void mkr_xpath_error_clear(mkr_xpath_error_t *e);
135
154
 
155
+ /* ------------------------------------------------------------------ *
156
+ * Engine-client API: parser, compiled-AST evaluator, context accessors
157
+ * and the internal value model. The Ruby glue (the engine's client) drives the
158
+ * engine through these and bridges custom-function handlers, so they live on
159
+ * this public boundary -- representation-neutral (node pointers are void*; the
160
+ * glue casts to the document's kind) and free of the MKR_DOM_* monomorphization
161
+ * machinery in mkr_xpath_internal.h, which the glue never includes. The engine
162
+ * itself reaches them through internal.h, which includes this header.
163
+ * ------------------------------------------------------------------ */
164
+
165
+ /* Per-evaluate budgets + live counters. Filled by
166
+ * mkr_xpath_limits_init_defaults; every overrun fails closed with
167
+ * MKR_XPATH_ERR_LIMIT. */
168
+ typedef struct {
169
+ /* Static budgets. */
170
+ size_t max_expr_bytes;
171
+ size_t max_ast_nodes;
172
+ size_t max_steps;
173
+ size_t max_predicates;
174
+ size_t max_function_args;
175
+ size_t max_nodeset_size;
176
+ size_t max_eval_ops;
177
+ size_t max_string_bytes;
178
+ size_t max_recursion_depth;
179
+ /* Live counters. */
180
+ size_t ast_nodes;
181
+ size_t eval_ops;
182
+ size_t recursion_depth;
183
+ } mkr_xpath_limits_t;
184
+
185
+ /* The compiled AST, opaque to clients (built by mkr_parse, freed by
186
+ * mkr_node_free, run by the evaluator). */
187
+ typedef struct mkr_node_s mkr_node_t;
188
+
189
+ /* The engine's internal value -- the type a custom-function resolver receives
190
+ * as its void* args / out (distinct from mkr_xpath_value_t, the result-boundary
191
+ * type, which names the node array `nodes`). Node pointers are void*; the glue
192
+ * handler bridge reads/writes them and casts to the document's representation. */
193
+ typedef struct {
194
+ void **items;
195
+ size_t count;
196
+ size_t capacity;
197
+ } mkr_nodeset_t;
198
+
199
+ typedef struct {
200
+ mkr_xpath_type_t type;
201
+ union {
202
+ mkr_nodeset_t nodeset;
203
+ mkr_owned_text_t string;
204
+ double number;
205
+ int boolean;
206
+ } u;
207
+ } mkr_val_t;
208
+
209
+ /* Parse an expression into a compiled AST (caller frees with mkr_node_free);
210
+ * NULL on error (*err filled). 'limits' bounds the AST node count. */
211
+ mkr_node_t *mkr_parse(mkr_verified_text_t expr, mkr_xpath_limits_t *limits, mkr_xpath_error_t *err);
212
+ void mkr_node_free(mkr_node_t *n);
213
+
214
+ /* Evaluate a compiled AST; fills *out_value / *out_error. _first is the
215
+ * Node#at_xpath variant (a 0-or-1-node result without building the full set). */
216
+ int mkr_xpath_eval_compiled (mkr_xpath_context_t *ctx, mkr_node_t *ast,
217
+ mkr_xpath_value_t *out_value, mkr_xpath_error_t *out_error);
218
+ int mkr_xpath_eval_compiled_first(mkr_xpath_context_t *ctx, mkr_node_t *ast,
219
+ mkr_xpath_value_t *out_value, mkr_xpath_error_t *out_error);
220
+
221
+ /* Select the monomorphized engine instance: 0 = HTML (lxb_dom), 1 = XML. */
222
+ void mkr_xpath_set_engine_kind(mkr_xpath_context_t *ctx, int kind);
223
+
224
+ /* Context accessors used by the glue (node pointers are void*). */
225
+ mkr_xpath_limits_t *mkr_ctx_limits (mkr_xpath_context_t *ctx);
226
+ void *mkr_ctx_document (mkr_xpath_context_t *ctx);
227
+ void mkr_ctx_set_node (mkr_xpath_context_t *ctx, void *node);
228
+ void mkr_ctx_set_unprefixed_lax(mkr_xpath_context_t *ctx, int lax);
229
+
230
+ /* Error helpers (fill *err; never raise). */
231
+ void mkr_err_set (mkr_xpath_error_t *err, mkr_xpath_status_t status, const char *msg);
232
+ void mkr_err_setf(mkr_xpath_error_t *err, mkr_xpath_status_t status, const char *fmt, ...);
233
+
234
+ /* Node-set builder + value setter, for the custom-function handler bridge. */
235
+ void mkr_nodeset_init (mkr_nodeset_t *ns);
236
+ int mkr_nodeset_push (mkr_nodeset_t *ns, void *node, mkr_xpath_limits_t *limits, mkr_xpath_error_t *err);
237
+ void mkr_nodeset_clear(mkr_nodeset_t *ns);
238
+ int mkr_val_set_borrowed_text_copy(mkr_val_t *v, mkr_borrowed_text_t text,
239
+ mkr_xpath_error_t *err, const char *what);
240
+
136
241
  #ifdef __cplusplus
137
242
  }
138
243
  #endif
@@ -0,0 +1,17 @@
1
+ /* mkr_xpath_engine_html.c - the HTML monomorphization of the XPath engine.
2
+ *
3
+ * One translation unit holding the whole per-instance engine: the value model,
4
+ * the built-in function library, and the evaluator. The prelude binds the
5
+ * MKR_NODE_* / MKR_DOM_* contract to Lexbor's lxb_dom (mkr_xpath_engine_xml.c is
6
+ * the identical build for the custom XML node). Because all three bodies share
7
+ * this one TU, every helper they pass between each other is file-static - the
8
+ * engine internals never collide with the XML instance and need no per-symbol
9
+ * renaming. Only the two node-dereferencing ENTRY points the driver dispatches
10
+ * on (mkr_eval_ast, mkr_try_first_match) are external, suffixed _html by the
11
+ * prelude. The representation-independent primitives both instances call live
12
+ * once in mkr_xpath_shared.c. */
13
+ #include "mkr_xpath_prelude_html.h"
14
+
15
+ #include "mkr_xpath_value_body.h"
16
+ #include "mkr_xpath_funcs_body.h"
17
+ #include "mkr_xpath_eval_body.h"
@@ -0,0 +1,12 @@
1
+ /* mkr_xpath_engine_xml.c - the XML monomorphization of the XPath engine.
2
+ *
3
+ * Byte-for-byte the same bodies as mkr_xpath_engine_html.c, but the prelude
4
+ * binds the MKR_NODE_* / MKR_DOM_* contract to the custom mkr_xml_node_t and
5
+ * selects the engine's XML host-policy branches (MKR_HOST_XML). See
6
+ * mkr_xpath_engine_html.c for the one-TU / file-static rationale; here the two
7
+ * external entries are suffixed _xml. */
8
+ #include "mkr_xpath_prelude_xml.h"
9
+
10
+ #include "mkr_xpath_value_body.h"
11
+ #include "mkr_xpath_funcs_body.h"
12
+ #include "mkr_xpath_eval_body.h"