makiri 0.10.0 → 0.11.0.rc1

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 (201) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/libfuzzer.yml +1 -1
  3. data/.github/workflows/release.yml +11 -2
  4. data/.github/workflows/security.yml +7 -8
  5. data/CHANGELOG.md +130 -0
  6. data/NOKOGIRI_DIFFERENCES.md +122 -7
  7. data/README.md +8 -3
  8. data/Rakefile +25 -9
  9. data/ext/makiri/rust/Cargo.toml +7 -7
  10. data/ext/makiri/rust/build.rs +52 -29
  11. data/ext/makiri/rust/clippy.toml +28 -9
  12. data/ext/makiri/rust/extconf.rb +3 -1
  13. data/ext/makiri/rust/fuzz/Cargo.toml +9 -2
  14. data/ext/makiri/rust/fuzz/fuzz_targets/common.rs +4 -11
  15. data/ext/makiri/rust/fuzz/fuzz_targets/css.rs +3 -3
  16. data/ext/makiri/rust/fuzz/fuzz_targets/html.rs +5 -5
  17. data/ext/makiri/rust/fuzz/fuzz_targets/html_css.rs +101 -0
  18. data/ext/makiri/rust/fuzz/fuzz_targets/html_xpath.rs +5 -2
  19. data/ext/makiri/rust/fuzz/fuzz_targets/xml_xpath.rs +15 -17
  20. data/ext/makiri/rust/fuzz/fuzz_targets/xpath.rs +22 -24
  21. data/ext/makiri/rust/src/bridge/doc.rs +64 -37
  22. data/ext/makiri/rust/src/bridge/fragment.rs +121 -102
  23. data/ext/makiri/rust/src/bridge/gvl.rs +59 -19
  24. data/ext/makiri/rust/src/bridge/html.rs +316 -250
  25. data/ext/makiri/rust/src/bridge/mod.rs +8 -0
  26. data/ext/makiri/rust/src/bridge/node_set.rs +131 -137
  27. data/ext/makiri/rust/src/bridge/node_wrap.rs +34 -0
  28. data/ext/makiri/rust/src/bridge/ruby.rs +89 -59
  29. data/ext/makiri/rust/src/bridge/stack.rs +17 -0
  30. data/ext/makiri/rust/src/bridge/string.rs +332 -188
  31. data/ext/makiri/rust/src/bridge/typed.rs +47 -40
  32. data/ext/makiri/rust/src/bridge/wrapper.rs +425 -111
  33. data/ext/makiri/rust/src/bridge/xml.rs +179 -179
  34. data/ext/makiri/rust/src/bridge/xml_decode.rs +44 -24
  35. data/ext/makiri/rust/src/bridge/xpath/context_object.rs +68 -38
  36. data/ext/makiri/rust/src/bridge/xpath/handler.rs +101 -61
  37. data/ext/makiri/rust/src/bridge/xpath/mod.rs +193 -115
  38. data/ext/makiri/rust/src/caught.rs +1 -3
  39. data/ext/makiri/rust/src/cbuf/verify.rs +66 -40
  40. data/ext/makiri/rust/src/cbuf.rs +237 -237
  41. data/ext/makiri/rust/src/css/build.rs +91 -42
  42. data/ext/makiri/rust/src/css/lower.rs +243 -185
  43. data/ext/makiri/rust/src/css/mod.rs +67 -22
  44. data/ext/makiri/rust/src/cutf8.rs +44 -25
  45. data/ext/makiri/rust/src/dom_rules/tests.rs +322 -0
  46. data/ext/makiri/rust/src/dom_rules.rs +238 -0
  47. data/ext/makiri/rust/src/{xpath/msg.rs → engine_error.rs} +64 -73
  48. data/ext/makiri/rust/src/falloc/cstr/verify.rs +40 -0
  49. data/ext/makiri/rust/src/falloc/cstr.rs +15 -24
  50. data/ext/makiri/rust/src/falloc/inject.rs +27 -16
  51. data/ext/makiri/rust/src/falloc/mod.rs +191 -67
  52. data/ext/makiri/rust/src/falloc/verify.rs +30 -16
  53. data/ext/makiri/rust/src/glue/hash.rs +50 -0
  54. data/ext/makiri/rust/src/glue/html_doc.rs +96 -98
  55. data/ext/makiri/rust/src/glue/html_node/css.rs +93 -36
  56. data/ext/makiri/rust/src/glue/html_node/mod.rs +87 -152
  57. data/ext/makiri/rust/src/glue/html_node/mutate.rs +293 -174
  58. data/ext/makiri/rust/src/glue/html_node/read.rs +202 -218
  59. data/ext/makiri/rust/src/glue/html_node/serialize.rs +24 -33
  60. data/ext/makiri/rust/src/glue/kwargs.rs +104 -0
  61. data/ext/makiri/rust/src/glue/mod.rs +6 -0
  62. data/ext/makiri/rust/src/glue/node.rs +18 -15
  63. data/ext/makiri/rust/src/glue/node_set.rs +85 -89
  64. data/ext/makiri/rust/src/glue/query.rs +71 -51
  65. data/ext/makiri/rust/src/glue/stylesheet.rs +41 -38
  66. data/ext/makiri/rust/src/glue/xml_doc.rs +41 -47
  67. data/ext/makiri/rust/src/glue/xml_node/css.rs +18 -23
  68. data/ext/makiri/rust/src/glue/xml_node/mod.rs +80 -130
  69. data/ext/makiri/rust/src/glue/xml_node/mutate.rs +194 -167
  70. data/ext/makiri/rust/src/glue/xml_node/ns.rs +43 -26
  71. data/ext/makiri/rust/src/glue/xml_node/read.rs +176 -130
  72. data/ext/makiri/rust/src/glue/xml_node/serialize.rs +46 -43
  73. data/ext/makiri/rust/src/glue/xpath_context.rs +41 -39
  74. data/ext/makiri/rust/src/init.rs +232 -202
  75. data/ext/makiri/rust/src/lexbor/abi.rs +34 -18
  76. data/ext/makiri/rust/src/lexbor/adapter/arena_bytes.rs +183 -22
  77. data/ext/makiri/rust/src/lexbor/adapter/cross_import.rs +224 -200
  78. data/ext/makiri/rust/src/lexbor/adapter/dom_index.rs +42 -82
  79. data/ext/makiri/rust/src/lexbor/adapter/html/attrs.rs +452 -0
  80. data/ext/makiri/rust/src/lexbor/adapter/html/build.rs +173 -108
  81. data/ext/makiri/rust/src/lexbor/adapter/html/mod.rs +657 -224
  82. data/ext/makiri/rust/src/lexbor/adapter/html/mutate.rs +145 -119
  83. data/ext/makiri/rust/src/lexbor/adapter/mod.rs +45 -4
  84. data/ext/makiri/rust/src/lexbor/adapter/post_parse.rs +239 -129
  85. data/ext/makiri/rust/src/lexbor/adapter/source_loc.rs +119 -180
  86. data/ext/makiri/rust/src/lexbor/adapter/text_index.rs +125 -69
  87. data/ext/makiri/rust/src/lexbor/adapter/tree_guard.rs +387 -0
  88. data/ext/makiri/rust/src/lexbor/chunks.rs +98 -0
  89. data/ext/makiri/rust/src/lexbor/contains_guard.rs +196 -243
  90. data/ext/makiri/rust/src/lexbor/css_engine.rs +44 -11
  91. data/ext/makiri/rust/src/lexbor/css_match/compile.rs +378 -0
  92. data/ext/makiri/rust/src/lexbor/css_match/mod.rs +413 -0
  93. data/ext/makiri/rust/src/lexbor/css_match/positions.rs +244 -0
  94. data/ext/makiri/rust/src/lexbor/css_match/query.rs +871 -0
  95. data/ext/makiri/rust/src/lexbor/css_match/scratch.rs +153 -0
  96. data/ext/makiri/rust/src/lexbor/css_match/simple.rs +413 -0
  97. data/ext/makiri/rust/src/lexbor/css_match/state.rs +242 -0
  98. data/ext/makiri/rust/src/lexbor/css_match/tree.rs +100 -0
  99. data/ext/makiri/rust/src/lexbor/css_parser.rs +119 -31
  100. data/ext/makiri/rust/src/lexbor/css_tokens.rs +204 -0
  101. data/ext/makiri/rust/src/lexbor/fragment.rs +226 -128
  102. data/ext/makiri/rust/src/lexbor/memory.rs +78 -0
  103. data/ext/makiri/rust/src/lexbor/mod.rs +21 -2
  104. data/ext/makiri/rust/src/lexbor/selector_cache.rs +356 -0
  105. data/ext/makiri/rust/src/lexbor/selectors.rs +165 -317
  106. data/ext/makiri/rust/src/lexbor/serialize.rs +108 -67
  107. data/ext/makiri/rust/src/lexbor/stylesheet.rs +62 -155
  108. data/ext/makiri/rust/src/lexbor/tests.rs +2248 -1
  109. data/ext/makiri/rust/src/lexbor/xpath.rs +77 -81
  110. data/ext/makiri/rust/src/lib.rs +37 -12
  111. data/ext/makiri/rust/src/node_type.rs +63 -0
  112. data/ext/makiri/rust/src/ptr_table.rs +81 -21
  113. data/ext/makiri/rust/src/rust_tests.rs +46 -42
  114. data/ext/makiri/rust/src/stack.rs +48 -0
  115. data/ext/makiri/rust/src/text.rs +43 -160
  116. data/ext/makiri/rust/src/token.rs +9 -11
  117. data/ext/makiri/rust/src/{lexbor/adapter/utf8_input.rs → utf8_input.rs} +7 -21
  118. data/ext/makiri/rust/src/xml/arena.rs +248 -190
  119. data/ext/makiri/rust/src/xml/attr_key.rs +91 -0
  120. data/ext/makiri/rust/src/xml/chars/mod.rs +42 -19
  121. data/ext/makiri/rust/src/xml/dom_name.rs +22 -3
  122. data/ext/makiri/rust/src/xml/index.rs +2 -2
  123. data/ext/makiri/rust/src/xml/mod.rs +1 -0
  124. data/ext/makiri/rust/src/xml/model.rs +286 -169
  125. data/ext/makiri/rust/src/xml/mutate/attr.rs +103 -95
  126. data/ext/makiri/rust/src/xml/mutate/copy.rs +84 -46
  127. data/ext/makiri/rust/src/xml/mutate/edit.rs +21 -68
  128. data/ext/makiri/rust/src/xml/mutate/factory.rs +51 -61
  129. data/ext/makiri/rust/src/xml/mutate/insert.rs +114 -254
  130. data/ext/makiri/rust/src/xml/mutate/mod.rs +15 -27
  131. data/ext/makiri/rust/src/xml/mutate/ns.rs +241 -75
  132. data/ext/makiri/rust/src/xml/qname.rs +70 -0
  133. data/ext/makiri/rust/src/xml/selftest.rs +156 -129
  134. data/ext/makiri/rust/src/xml/serialize/bindings.rs +253 -0
  135. data/ext/makiri/rust/src/xml/serialize/c14n.rs +190 -112
  136. data/ext/makiri/rust/src/xml/serialize/mod.rs +39 -6
  137. data/ext/makiri/rust/src/xml/serialize/out.rs +17 -5
  138. data/ext/makiri/rust/src/xml/serialize/xml.rs +139 -199
  139. data/ext/makiri/rust/src/xml/tree/cursor.rs +20 -44
  140. data/ext/makiri/rust/src/xml/tree/dtd.rs +7 -7
  141. data/ext/makiri/rust/src/xml/tree/mod.rs +110 -172
  142. data/ext/makiri/rust/src/xml/tree/scope.rs +11 -17
  143. data/ext/makiri/rust/src/xml/xpath.rs +50 -33
  144. data/ext/makiri/rust/src/xpath/abi.rs +6 -10
  145. data/ext/makiri/rust/src/xpath/ast.rs +61 -26
  146. data/ext/makiri/rust/src/xpath/ast_ops.rs +2 -3
  147. data/ext/makiri/rust/src/xpath/attr_pred.rs +14 -18
  148. data/ext/makiri/rust/src/xpath/axis.rs +24 -24
  149. data/ext/makiri/rust/src/xpath/ctx.rs +229 -190
  150. data/ext/makiri/rust/src/xpath/dom.rs +23 -33
  151. data/ext/makiri/rust/src/xpath/eval.rs +166 -127
  152. data/ext/makiri/rust/src/xpath/funcs/ext.rs +195 -47
  153. data/ext/makiri/rust/src/xpath/funcs/mod.rs +339 -233
  154. data/ext/makiri/rust/src/xpath/lex.rs +36 -85
  155. data/ext/makiri/rust/src/xpath/limits.rs +63 -9
  156. data/ext/makiri/rust/src/xpath/mod.rs +7 -6
  157. data/ext/makiri/rust/src/xpath/nodetest.rs +47 -28
  158. data/ext/makiri/rust/src/xpath/number.rs +76 -64
  159. data/ext/makiri/rust/src/xpath/order.rs +39 -38
  160. data/ext/makiri/rust/src/xpath/parse.rs +108 -89
  161. data/ext/makiri/rust/src/xpath/step_index.rs +44 -42
  162. data/ext/makiri/rust/src/xpath/str_cache.rs +76 -29
  163. data/ext/makiri/rust/src/xpath/tests.rs +248 -21
  164. data/ext/makiri/rust/src/xpath/value.rs +295 -151
  165. data/ext/makiri/rust/src/xpath/verify.rs +53 -0
  166. data/ext/makiri/rust/tests/alloc_inject_map_insert.rs +47 -0
  167. data/lib/makiri/attr.rb +15 -0
  168. data/lib/makiri/cdata_section.rb +8 -0
  169. data/lib/makiri/clone_via_dup.rb +19 -0
  170. data/lib/makiri/comment.rb +7 -0
  171. data/lib/makiri/css.rb +5 -4
  172. data/lib/makiri/document_fragment.rb +5 -2
  173. data/lib/makiri/document_type.rb +2 -3
  174. data/lib/makiri/element.rb +9 -0
  175. data/lib/makiri/error.rb +26 -0
  176. data/lib/makiri/html/document.rb +19 -14
  177. data/lib/makiri/html/node_methods.rb +9 -8
  178. data/lib/makiri/html.rb +3 -3
  179. data/lib/makiri/node.rb +13 -72
  180. data/lib/makiri/node_path.rb +95 -0
  181. data/lib/makiri/node_set.rb +50 -25
  182. data/lib/makiri/processing_instruction.rb +10 -8
  183. data/lib/makiri/reader_aliases.rb +28 -0
  184. data/lib/makiri/text.rb +7 -0
  185. data/lib/makiri/version.rb +1 -1
  186. data/lib/makiri/xml/builder.rb +17 -8
  187. data/lib/makiri/xml/document.rb +23 -3
  188. data/lib/makiri/xml/node_methods.rb +26 -32
  189. data/lib/makiri/xml.rb +4 -3
  190. data/lib/makiri/xpath.rb +8 -8
  191. data/lib/makiri/xpath_context.rb +1 -1
  192. data/lib/makiri/xpath_syntax.rb +43 -0
  193. data/lib/makiri.rb +13 -7
  194. data/script/api_manifest.rb +25 -3
  195. data/script/check_alloc_failures.rb +344 -16
  196. data/script/check_unsafe_boundaries.rb +370 -66
  197. data/script/leaks_harness.rb +14 -1
  198. data/suppressions/ruby.supp +10 -8
  199. metadata +35 -5
  200. data/ext/makiri/rust/src/falloc/calloc_verify.rs +0 -139
  201. data/ext/makiri/rust/src/falloc/raw.rs +0 -62
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6163794fb07f39d4476e91a5d9db367d82b02888e167bc41435bb12de32de49
4
- data.tar.gz: 4c075f42cbdf69b39b9783e3ad66bd09acb48f380447e8e80a47402dcdd427e5
3
+ metadata.gz: 7421d59a83c782fab157fb3b220761908b885190db8405dd5173ddaeb7343e83
4
+ data.tar.gz: 4f64149f513b8a8f5288da9158c6dd5fa673e79ca33f5acc2c06a863d6f46641
5
5
  SHA512:
6
- metadata.gz: d6f1d154116c49d5b42e57e08d584781af42c2d0a02a6d3a1d6073659437cd7b0920a30a42451fedc70b173de242c9738a34ea4fd8d582dd3d199094bbfe10ab
7
- data.tar.gz: 7278778cf2e98166e03ef79364ef9a4b0feec3b410949c2e4889f82f899594da8ba492ac00a6d75de2ee7a553430e5dc69a9af52a41292ab86249af830dea7fe
6
+ metadata.gz: b1da6427faed2ac5e96ff02c6eeeb207c999eee5322c8b65e63b95e6038dee2ffbcaa457580f2980b73de6a9d7436e8e602134e7a3bb5e0e5fc399072b6eddaf
7
+ data.tar.gz: 4ee2def4dbe0cc740450bfa2e0d8a448a7908f1356f6de877d2c91f1d7e58aa92d022fd9f553b55463e6e444875f0889f567102c2883ece072f886a93eb06a44
@@ -25,7 +25,7 @@ jobs:
25
25
  strategy:
26
26
  fail-fast: false
27
27
  matrix:
28
- target: [xml, xpath, xml_xpath, css, html, html_xpath]
28
+ target: [xml, xpath, xml_xpath, css, html, html_xpath, html_css]
29
29
 
30
30
  steps:
31
31
  - name: Checkout (with vendored Lexbor submodule)
@@ -86,9 +86,18 @@ jobs:
86
86
  # names the GNU triple; this installs the std that goes with it.
87
87
  # Empty elsewhere, so the other runners are unaffected.
88
88
  targets: ${{ runner.os == 'Windows' && 'x86_64-pc-windows-gnu' || '' }}
89
- - name: Install libclang (bindgen, via rb-sys)
89
+ # clang/lld/llvm are for the vendored Lexbor's LTO, not for bindgen: on
90
+ # Linux the whole chain has to be LLVM or extconf turns LTO off (gcc
91
+ # emits GIMPLE, which rust-lld cannot read - see the note in
92
+ # extconf.rb). It DETECTS them, so a runner without them still builds,
93
+ # just without LTO - which is what this job used to ship, silently,
94
+ # to every user who installs the precompiled gem (see the measured win
95
+ # in CLAUDE.md's "Vendored Lexbor is built with LTO" section: this is
96
+ # the job that produces those gems, and `ci.yml` installing the chain
97
+ # only exercises the fast path in CI, it does not ship it).
98
+ - name: Install libclang (bindgen) and the LLVM chain (Lexbor LTO)
90
99
  if: runner.os == 'Linux'
91
- run: sudo apt-get update && sudo apt-get install -y libclang-dev
100
+ run: sudo apt-get update && sudo apt-get install -y libclang-dev clang lld llvm
92
101
  - uses: ruby/setup-ruby@v1
93
102
  with:
94
103
  ruby-version: ${{ matrix.ruby }}
@@ -55,7 +55,7 @@ jobs:
55
55
  bundler-cache: true
56
56
 
57
57
  - name: Run short fuzz under sanitizers
58
- run: bundle exec rake fuzz:sanitize FUZZ_ARGS="--time 30"
58
+ run: FUZZ_TIME=30 bundle exec rake fuzz:sanitize
59
59
 
60
60
  # macOS-only malloc-leak gate: ASan everywhere runs with detect_leaks=0 (Ruby
61
61
  # and Lexbor are uninstrumented), so this is the ONLY automated leak check. It
@@ -98,9 +98,9 @@ jobs:
98
98
  run: bundle exec rake leaks
99
99
 
100
100
  # OOM-injection sweep: rebuilds with MAKIRI_ALLOC_INJECT=1 and fails each core
101
- # C allocation site in turn, gating that every OOM branch fails closed - a
102
- # clean exception or a baseline-identical result, never truncated output
103
- # (see script/check_alloc_failures.rb).
101
+ # Rust allocation site in turn, gating that every OOM branch fails closed - a
102
+ # clean exception or a baseline-identical result, with stateful objects reusable
103
+ # after the failure (see script/check_alloc_failures.rb).
104
104
  security-alloc-inject:
105
105
  name: OOM-injection sweep
106
106
  runs-on: ubuntu-latest
@@ -137,9 +137,8 @@ jobs:
137
137
  - name: Run the OOM-injection sweep
138
138
  run: bundle exec rake oom
139
139
 
140
- # Nightly: fuzz EVERY target (the 30s PR fuzz covers only the default xpath
141
- # target; the CSS engine reuse and the XML parser/mutator are each their own
142
- # documented memory-safety risk, so each gets a full 300s run).
140
+ # Nightly: fuzz EVERY target for a longer run. The PR job already exercises
141
+ # every surface for 30s; nightly gives each target a full 300s run.
143
142
  security-fuzz-nightly:
144
143
  name: Nightly sanitized fuzz (${{ matrix.target }})
145
144
  runs-on: ubuntu-latest
@@ -178,7 +177,7 @@ jobs:
178
177
  bundler-cache: true
179
178
 
180
179
  - name: Run nightly fuzz under sanitizers
181
- run: bundle exec rake fuzz:sanitize FUZZ_ARGS="--target ${{ matrix.target }} --time 300"
180
+ run: FUZZ_ARGS="--target ${{ matrix.target }} --time 300" bundle exec rake fuzz:sanitize
182
181
 
183
182
  # Nightly: the whole spec suite with Lexbor ITSELF built under ASan (mraw
184
183
  # poisoning on), catching intra-arena overflows that a plain ASan build cannot
data/CHANGELOG.md CHANGED
@@ -1,5 +1,135 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.0.rc1] - 2026-09-29
4
+
5
+ ### Changed
6
+
7
+ * HTML `#css` / `#at_css` / `#matches?` now match with Makiri's own Rust
8
+ implementation instead of Lexbor's `lxb_selectors` (selectors are still
9
+ parsed by Lexbor). Where the answers differ:
10
+ * `:nth-child(An+B of S)` / `:nth-last-child(... of S)` count by the CSS
11
+ definition; Lexbor miscounted when `S` was a comma list, held a
12
+ combinator, or carried pseudo-classes such as `:enabled`.
13
+ * `:disabled`, `:enabled` and `:checked` follow the HTML Standard: a
14
+ control inside a `<fieldset disabled>` (outside its first `<legend>`) is
15
+ disabled, `<option>` / `<optgroup>` count, and `:enabled` matches only
16
+ form elements.
17
+ * Attribute selector names are matched by qualified name, and
18
+ case-sensitively on SVG/MathML elements: `[href]` no longer finds
19
+ `xlink:href`, nor `[viewbox]` an SVG `viewBox`.
20
+ * `#id` / `.class` match only the no-namespace `id` / `class` attribute.
21
+ * A selector chain is capped at 64 compounds (as `Makiri::XML` already
22
+ was), and a query at 50M steps of work (XPath's limit); past either,
23
+ `Makiri::Error`.
24
+ * The column combinator `||` raises `Makiri::Error` instead of matching
25
+ nothing.
26
+
27
+ ### Removed
28
+
29
+ * `:lexbor-contains()` in HTML `#css` / `#at_css` / `#matches?`: it still
30
+ parses, but raises `Makiri::Error`. `Makiri::XML`'s `#css` keeps it. See
31
+ NOKOGIRI_DIFFERENCES.md.
32
+ * `Node#name=` / `#node_name=`. The DOM cannot rename an element, and renaming
33
+ a Lexbor element in place could crash (`div` to `template`). Create a new
34
+ element and `replace` the old one.
35
+
36
+ ### Security
37
+
38
+ * Hardening: every heap block the vendored Lexbor allocates carries 16 bytes
39
+ of slack past its end, so a small overrun cannot reach neighbouring memory
40
+ (not in sanitizer builds).
41
+ * Adding an `id` or `class` attribute beside an existing one
42
+ (`set_attribute_ns(nil, "ID", v)`, a namespaced `id`) no longer frees the
43
+ existing one under a held `Attr`. HTML attribute reads and writes follow the
44
+ DOM: `#[]`, `#key?`, `#[]=` and `#delete` match the qualified name
45
+ (`svg_a["href"]` no longer returns `xlink:href`), copies and imports keep
46
+ every attribute, and a no-namespace name keeps its case.
47
+ * `content=` on an HTML element and `delete(name)` detach the nodes they
48
+ remove instead of freeing them under a live wrapper.
49
+ * `el[name] = value` on an existing attribute no longer leaves a destroyed
50
+ attribute linked when storing the value runs out of memory.
51
+ * A checked argument String is locked while its bytes are in use, so a later
52
+ argument's `#to_s` cannot rewrite it (it raises instead); a receiver frozen
53
+ by an argument's `#to_s` is not edited; and a mutator's argument can no
54
+ longer rebuild the document's indexes in the middle of the edit.
55
+ * HTML element and attribute names follow the DOM's rules:
56
+ `create_element("img src=x onerror=alert(1)")` raises `ArgumentError`
57
+ instead of writing that markup. See NOKOGIRI_DIFFERENCES.md.
58
+ * HTML parsing bounds the tree depth (`max_tree_depth:`, default 400) and the
59
+ `<option>`s one `<select>` receives (10,000), raising `Makiri::Error` past
60
+ either; both made the parse quadratic.
61
+ * `Makiri::Lexbor::CSS.parse_stylesheet` no longer crashes on a
62
+ `:lexbor-contains()` after a string ended by CR, FF or a newline.
63
+ * A CSS query that runs out of memory raises instead of answering with what it
64
+ had collected; a selector-parse OOM is reported as OOM, not
65
+ `CSS::SyntaxError`.
66
+ * Inputs whose cost grew faster than their size are linear or budgeted: many
67
+ namespace bindings, the `preceding` axis and reverse-axis steps, XML
68
+ duplicate-attribute checks, `contains` / `substring-*` / `translate` on long
69
+ strings, XML CSS `:nth-*`, CDATA full of `]]>`, and stylesheets with
70
+ rejected `:lexbor-contains()`.
71
+ * A panic in any Ruby method Makiri defines raises `Makiri::InternalError`
72
+ (rescuable), not `fatal`.
73
+
74
+ ### Fixed
75
+
76
+ * A document that grows by editing (appended nodes, `inner_html=`) reports
77
+ its new size to the GC, so memory pressure from it triggers collections; it
78
+ was reported once, at parse time.
79
+ * A node wrapped while memory runs out raises `Makiri::Error` instead of
80
+ coming back as a second Ruby object for the same node, without the first
81
+ one's `freeze`, instance variables or singleton methods.
82
+ * Insertion follows the DOM's pre-insertion rules in HTML and XML alike: no
83
+ cycles through a template's contents (which hung `dup`), no children on
84
+ Text/Comment/PI/DocumentType/Attr, no Text directly under an XML Document,
85
+ and an XML `DocumentFragment` takes children.
86
+ * `inner_html` / `inner_html=` on a `<template>` target its contents, agreeing
87
+ with `#to_html` and `content_fragment`.
88
+ * HTML `#keys` / `#values` raise on out-of-memory instead of returning a
89
+ truncated Array.
90
+ * `dup`, `clone_node` and `import_node` keep an element's name as written
91
+ (`linearGradient`, `q:Bar`), and a copied XML doctype keeps its PUBLIC id.
92
+ * Namespaces across HTML <-> XML `import_node`: attributes keep their own
93
+ namespace, a prefixed element keeps its prefix and case, names XML cannot
94
+ write are refused where they would be written, and copied `xmlns`
95
+ attributes no longer move elements between namespaces.
96
+ * XML namespaces: `set_attribute_ns` on a detached element survives insertion;
97
+ `[]=` on an existing attribute changes only its value; the mutators refuse
98
+ what the parser refuses (forbidden declarations, duplicate expanded names,
99
+ bad doctype names and PUBLIC ids); `canonicalize` raises instead of writing a
100
+ wrong namespace; and a refused declaration says which rule it broke.
101
+ * `to_xml` output re-parses: CDATA holding `]]>`, a SYSTEM id holding `"`, and
102
+ attributes with a namespace but no prefix.
103
+ * XPath: `string()` of a number follows libxml2 (as Nokogiri does);
104
+ `substring()` rounds per spec; node-set vs boolean comparisons follow §3.4;
105
+ the `xml` prefix is always bound; a comparison no longer raises
106
+ `LimitExceeded` because its values added up past 64 MB.
107
+ * `Node#path` round-trips through `#at_xpath` for CDATA, PIs, and namespaced
108
+ nodes (SVG/MathML, XML namespaces, `xlink:href`, Vue/Word-style names); an
109
+ unreachable node answers `"?"` as in Nokogiri.
110
+ * CSS over XML agrees with the HTML matcher on `.x\ y`, `:root`, empty and
111
+ whitespace attribute values, `$=` with non-ASCII, `:empty` beside a comment,
112
+ and `[|a]`; the `s` modifier is accepted.
113
+ * An HTML document refuses a second root element or a text child; an
114
+ attribute's parent is always its owner element.
115
+ * `Node#line` of a node copied from another document is nil.
116
+ * `Makiri::XML` nodes compare with `<=>` in document order (an attribute with
117
+ itself too), and `XML::Document#dup` / `#clone` return a copy.
118
+ * `clone_node` on a Document raises instead of returning the document.
119
+ * `NodeSet#css` / `#xpath` / `#at_css` / `#at_xpath` pass their extra
120
+ arguments through; `#xpath` of a non-node-set expression raises
121
+ `ArgumentError`, as in Nokogiri.
122
+ * `XML::Builder` no longer answers `respond_to?` for `to_ary` and similar.
123
+ * Namespace Hashes and `register_namespace` arguments are read as a Hash and
124
+ with `String()`, with one length cap and one error message.
125
+ * A rejected stylesheet rule's `selector_text` is sliced by Lexbor's offsets.
126
+
127
+ ### Performance
128
+
129
+ * `NodeSet#at_css` / `#at_xpath` stop at the first node with a match.
130
+ * `XML::Node#canonicalize` looks namespaces up by prefix in the scope it keeps:
131
+ a 1000-deep document of declarations went from 0.7 s to 0.02 s.
132
+
3
133
  ## [0.10.0] - 2026-09-22
4
134
 
5
135
  ### Fixed
@@ -26,11 +26,34 @@ what browsers do - rather than libxml2. Detailed, test-backed notes live in
26
26
  exactly (`//*[@refX]`, not `@refx`). Only ASCII folds: `Ø` still differs from `ø`.
27
27
  This holds in `namespace_matching: :lax` too.
28
28
  * `Nokogiri::HTML5` is case-sensitive there.
29
+ * `Node#path` names a node by expanded name where a bare name would not reach it
30
+ * An SVG, MathML or namespaced-XML node, and an HTML name that is no plain
31
+ XPath name (`<o:p>`, `xml:lang`, Vue's `@click`), give
32
+ `*[local-name()='path' and namespace-uri()='http://www.w3.org/2000/svg']`,
33
+ which `#at_xpath` evaluates with no prefix registered. Nokogiri writes
34
+ `svg:svg` or `/*/*[2]` for the first kinds; the paths are equivalent, the
35
+ strings are not.
36
+ * A node not attached to its document answers `"?"`, as a doctype does.
37
+ Nokogiri answers `"/div/p"` for a detached `<div><p>`, which is the path of
38
+ the document's own `/div/p` when it has one.
29
39
  * A foreign element's namespace declarations are not attributes
30
40
  * `<svg xmlns="...">` has no `@xmlns` for `//*[@xmlns]` or `@*`, as in browsers
31
41
  and in XPath's data model. An `xmlns` on an HTML element is an ordinary
32
42
  attribute and stays visible.
33
43
 
44
+ * A `StandardError` raised by a custom-function handler becomes `Makiri::Error`
45
+ ("handler raised: <message>"), with the handler's exception as its `#cause`
46
+ * Nokogiri re-raises the handler's exception itself. Anything that is not a
47
+ `StandardError` (`Interrupt`, `SystemExit`, Timeout's exception) and a
48
+ `throw` reach the caller unchanged in both.
49
+
50
+ * A number literal is read as the nearest double; libxml2's own reader is not
51
+ correctly rounded for a literal with more digits than a double holds, so
52
+ `string(0.72609133372266155)` is `0.726091333722662` in Makiri and
53
+ `0.726091333722661` in Nokogiri (the digits differ in the last place). The
54
+ number is then written by libxml2's rule in both (`string(1234567890.5)` is
55
+ `1.2345678905e+09`); only the value read differs.
56
+
34
57
  ## XML
35
58
 
36
59
  * `Makiri::XML` is XML 1.0 (Fifth Edition) only and non-validating.
@@ -100,6 +123,14 @@ what browsers do - rather than libxml2. Detailed, test-backed notes live in
100
123
  the property-based differential), including namespaces, prolog/epilog comments
101
124
  and PIs, and adjacent-CDATA coalescing.
102
125
 
126
+ * `to_xml` keeps an element's namespace when an `xmlns` attribute on it says
127
+ otherwise
128
+ * `root["xmlns"] = "urn:x"` on an element in no namespace is not written: the
129
+ element stays in no namespace when the output is re-read, as the DOM Parsing
130
+ and Serialization spec asks. Nokogiri writes `<r xmlns="urn:x">`, which moves
131
+ the element into `urn:x` on re-parse. Create the element in the namespace
132
+ instead (`create_element("r", "xmlns" => "urn:x")`, or parse it so).
133
+
103
134
  ## HTML parsing
104
135
 
105
136
  * `<?php ... ?>` in HTML input is a **ProcessingInstruction** node; `#to_html`
@@ -109,6 +140,69 @@ what browsers do - rather than libxml2. Detailed, test-backed notes live in
109
140
  still produces the older bogus comment (`<!--?php ... ?-->`), and
110
141
  `Nokogiri::HTML` (libxml2) its own comment.
111
142
 
143
+ * A tree deeper than `max_tree_depth` raises **`Makiri::Error`**, where
144
+ `Nokogiri::HTML5` raises `ArgumentError`; the default (400) and boundary are
145
+ Nokogiri's. `inner_html=`, `outer_html=` and `Node#parse` take no keyword.
146
+ `Nokogiri::HTML` (libxml2) instead stops at depth 256 and returns the
147
+ truncated document.
148
+ * More than 10,000 `<option>`s in one `<select>` raise `Makiri::Error`;
149
+ Nokogiri has no such limit.
150
+
151
+ ## HTML mutation
152
+
153
+ * There is no `Node#name=` / `#node_name=` (on HTML or XML nodes). Nokogiri
154
+ renames a node in place and keeps its identity; the DOM has no rename, and
155
+ Lexbor keeps elements such as `<template>`, `<option>` and `<style>` in
156
+ structs of their own, so a node cannot change its tag safely. Create an
157
+ element of the new name, move the attributes and children, and `replace`
158
+ the old one (see CHANGELOG.md).
159
+ * Element and attribute names follow the WHATWG DOM's rules
160
+ * `create_element`, `[]=` and `set_attribute_ns` raise `ArgumentError` for a
161
+ name the DOM refuses - one holding whitespace, `/`, `>` (or `=` for an
162
+ attribute) - where `Nokogiri::HTML5` accepts it and writes it into the
163
+ markup as it stands: `create_element("img src=x onerror=alert(1)")`
164
+ serializes as that tag.
165
+ The names HTML actually uses (`data-*`, `aria-*`, `@click`, `:href`,
166
+ `v-on:x`, custom elements) are accepted.
167
+ * `set_attribute_ns(nil, "x:y")` raises, as the DOM's `setAttributeNS` does:
168
+ a prefix needs a namespace.
169
+ * An HTML `<template>` follows the WHATWG content model, which Nokogiri does
170
+ not: its parsed contents live in the separate fragment `Element#content_fragment`
171
+ returns, `template.children` is empty, and `inner_html` / `inner_html=`
172
+ special-case the contents (the WHATWG DOM special-cases `innerHTML` alone;
173
+ `append_child`, `content=`, and `children` act on the element's own empty
174
+ children, as the specification says). Nokogiri treats `<template>` as an
175
+ ordinary element, with the parsed nodes as its children, so
176
+ `template.inner_html` and `template.children` answer the other way round and
177
+ there is no `content_fragment`.
178
+ * An HTML document has one root element and no text child, as the DOM requires;
179
+ `doc << element` beside an existing root raises.
180
+ * An insertion the DOM refuses - a child under a text, comment, PI, doctype
181
+ or attribute node, text under an XML Document, a second root - raises
182
+ `Makiri::Error` in both representations. Nokogiri refuses the same ones with
183
+ `ArgumentError` (or `RuntimeError` for a second XML root).
184
+ * Moving HTML into an XML document (`xml_doc.import_node(html_node)`, or
185
+ inserting one) keeps every name's namespace, and refuses what XML cannot
186
+ write that way. An attribute in no namespace whose name has a prefix other
187
+ than `xml` - `v-on:click`, `fb:like`, an `xlink:href` on an HTML (not SVG)
188
+ element - raises `Makiri::Error`: as XML it would be a prefix bound to
189
+ nothing. Nokogiri copies it and writes `v-on:click="..."` into output that is
190
+ not namespace-well-formed. An element named with a colon (`<fb:like>`)
191
+ crosses as a DOM-loose name, which `to_xml` refuses.
192
+ * A known gap, in Lexbor's tag table: an HTML document that already holds a
193
+ parsed element named with a colon (`<x:y>`, one local name) and then
194
+ receives, by `import_node` from another document, a prefixed element
195
+ written the same way (`x:y` from XML: prefix `x`, local name `y`) re-points
196
+ the table's entry for that spelling - the parsed element then no longer
197
+ matches CSS `x\:y`. Copies within one document do not touch the table.
198
+ * An XML element with a prefix, imported into HTML, keeps it (`h:div` in
199
+ XHTML has the local name `div`). Two readers then disagree, as they do in
200
+ browsers: CSS's `div` matches it (Lexbor matches the local name), XPath's
201
+ `//div` does not (an HTML element's name test reads its qualified name). And
202
+ Lexbor's HTML serializer writes the prefix (`<h:div>`), where the HTML
203
+ standard writes the local name, so the HTML does not re-parse to the same
204
+ element.
205
+
112
206
  ## CSS
113
207
 
114
208
  * Most jQuery/Nokogiri CSS extensions are not supported (`:gt`, `:lt`, `:eq`, `:first`, ...)
@@ -116,13 +210,18 @@ what browsers do - rather than libxml2. Detailed, test-backed notes live in
116
210
  text-containment extension. Use XPath (`xpath("//p[contains(., 'x')]")`) or
117
211
  Enumerable (`css('li')[1]`) for the rest.
118
212
  Standard Level-4 selectors (`:is` / `:where` / `:has`) are supported; some of which Nokogiri rejects.
119
- * `:lexbor-contains("text")` is supported (on both HTML and XML) - Lexbor's
120
- spelling of the jQuery `:contains()` substring filter, matching an element
121
- whose text contains the string; append ` i` (`:lexbor-contains("text" i)`)
122
- for an ASCII case-insensitive match. (Nokogiri's name `:contains` is not an
123
- alias.) Like Lexbor's matcher, it tests the element's immediate child text
124
- nodes (not the deep string-value), so HTML and XML agree; on XML it lowers
125
- to XPath `child::text()[contains(., "text")]`.
213
+ * `:lexbor-contains("text")` - Lexbor's spelling of the jQuery `:contains()`
214
+ substring filter, matching an element whose text contains the string;
215
+ append ` i` (`:lexbor-contains("text" i)`) for an ASCII case-insensitive
216
+ match. (Nokogiri's name `:contains` is not an alias.) **XML only**: it
217
+ lowers to XPath `child::text()[contains(., "text")]`, testing the
218
+ element's immediate child text nodes, not the deep string-value. HTML no
219
+ longer supports it - `lexbor::css_match`, the safe-Rust port that
220
+ replaced Lexbor's own matcher for HTML, deliberately does not reimplement
221
+ it, so a well-formed `:lexbor-contains()` now raises `Makiri::Error`
222
+ ("could not be run") on HTML rather than ever matching (it still PARSES,
223
+ and an actually malformed one still raises `Makiri::CSS::SyntaxError`, as
224
+ before).
126
225
  * Untyped `:*-of-type` (`:first-of-type`, `:nth-of-type(an+b)`, ... with no type
127
226
  selector) is supported and correct on both HTML and XML - the "type" is the
128
227
  element's own expanded name.
@@ -138,6 +237,22 @@ what browsers do - rather than libxml2. Detailed, test-backed notes live in
138
237
  prefix IS resolved against the bindings, when the namespace matters.
139
238
  * `Makiri::XML` resolves CSS prefixes properly - it lowers the selector to the
140
239
  XPath engine, which registers the bindings.
240
+ * The same holds for attribute selectors: HTML `[|href]` (no namespace) and
241
+ plain `[href]` also find an SVG `xlink:href`, which `Nokogiri::HTML5`
242
+ does not.
243
+ `Makiri::XML` reads `[|a]` as the no-namespace attribute.
244
+ * A selector under a node matches the way `Element#querySelectorAll` does in a
245
+ browser, not scoped to that node, on HTML: `at_css("#c").css("div p")` finds a
246
+ `p` inside `#c` when `#c` is itself a `div`, since the selector is matched
247
+ against the whole document and only the results are kept to descendants.
248
+ `Nokogiri::HTML5` and `Nokogiri::XML` scope the selector to the node (`#c`
249
+ cannot be the `div`), and so does `Makiri::XML`, which lowers the selector to
250
+ an XPath from the node. Lexbor has no `:scope`; for a scoped match on HTML,
251
+ use XPath from the node (`xpath(".//div//p")`).
252
+ * The attribute case modifiers (`[a="x" i]`, `[a="x" s]`): HTML supports both,
253
+ through Lexbor's matcher. `Makiri::XML` accepts `s` (case-sensitive, which XML
254
+ values are anyway) and refuses `i` with `Makiri::CSS::SyntaxError`. Nokogiri
255
+ refuses both on either representation.
141
256
  * `#matches?` answers for a DETACHED node (`document.create_element("p")
142
257
  .matches?("p")` is true, on both representations). Nokogiri raises
143
258
  `NoMethodError` there - it implements `#matches?` as a search from
data/README.md CHANGED
@@ -23,11 +23,13 @@ XPath 1.0 evaluation in its own native engine, with no libxml2 dependency.
23
23
  * Native XML 1.0 parser
24
24
  * A strict, non-validating, fail-closed parser with its own node arena (not
25
25
  Lexbor's HTML DOM), queried through the same native XPath engine, with
26
- in-place tree edits (attributes, content, rename, remove).
26
+ in-place tree edits (attributes, content, remove).
27
27
  * Conformance is held by the W3C XML Conformance Test Suite, an XPath
28
28
  differential, and property-based testing vs Nokogiri (see below).
29
29
  * Bounded, fail-closed execution
30
30
  * XPath evaluation is bounded by per-evaluation limits on work, memory, and recursion.
31
+ * HTML parsing bounds the tree depth (`max_tree_depth:`, default 400) and the
32
+ `<option>`s per `<select>` (10,000).
31
33
  * Ownership and borrowing are kept explicit across layers, with owned/borrowed
32
34
  string types and verified text at engine boundaries.
33
35
  * Programmatic invalid input, limit violations, allocation failures, and unsupported constructs
@@ -65,6 +67,10 @@ link.parent.name # => "div"
65
67
  # Source location (reconstructed from the tokenizer, no Lexbor patches)
66
68
  doc.at_css("p").line # => 3
67
69
 
70
+ # Nesting deeper than 400 elements raises Makiri::Error (Nokogiri's default);
71
+ # max_tree_depth: raises the limit, and a negative value disables it
72
+ Makiri::HTML(deep_html, max_tree_depth: 2000)
73
+
68
74
  # Serialization
69
75
  doc.at_css("#main").to_html # => "<div id=\"main\" ...>...</div>"
70
76
  doc.at_css("#main").inner_html # => "\n <p class=\"lead\">Hello</p>\n..."
@@ -132,11 +138,10 @@ e = doc.at_xpath("//entry")
132
138
  e["id"] = "9" # add or replace an attribute (value escaped on output)
133
139
  e["dc:k"] = "v" # a prefixed name resolves against the in-scope xmlns
134
140
  e.content = "Bye" # replace an element's children with text
135
- e.name = "post" # rename in place (identity + namespace re-resolved)
136
141
  e.delete("id") # remove an attribute
137
142
  doc.at_xpath("//draft").remove
138
143
 
139
- doc.root.to_xml # => "<feed xmlns:dc=\"urn:dc\"><post dc:k=\"v\">Bye</post></feed>"
144
+ doc.root.to_xml # => "<feed xmlns:dc=\"urn:dc\"><entry dc:k=\"v\">Bye</entry></feed>"
140
145
  ```
141
146
 
142
147
  XML subtrees can be built using `Document#create_element` and other node factory methods,
data/Rakefile CHANGED
@@ -649,8 +649,20 @@ task :oom do
649
649
  # The hook is compiled in only under MAKIRI_ALLOC_INJECT=1 (zero overhead in
650
650
  # a normal build), so this needs its own rebuild; see
651
651
  # script/check_alloc_failures.rb for the protocol and the property gated.
652
- sh({ "MAKIRI_ALLOC_INJECT" => "1" }, "#{FileUtils::RUBY} -S rake clean compile")
653
- sh "#{FileUtils::RUBY} -Ilib script/check_alloc_failures.rb"
652
+ sanitize = ENV["MAKIRI_SANITIZE"].to_s
653
+ build_env = { "MAKIRI_ALLOC_INJECT" => "1" }
654
+ build_env["MAKIRI_SANITIZE"] = sanitize unless sanitize.empty?
655
+ sh(build_env, "#{FileUtils::RUBY} -S rake clean compile")
656
+
657
+ run_env = {}
658
+ unless sanitize.empty?
659
+ assert_sanitized!("oom", sanitize)
660
+ run_env = {
661
+ "ASAN_OPTIONS" => ASAN_ENV_OPTIONS,
662
+ "MAKIRI_SANITIZE" => sanitize,
663
+ }.merge(asan_preload_env(sanitize))
664
+ end
665
+ sh(run_env, "#{FileUtils::RUBY} -Ilib script/check_alloc_failures.rb")
654
666
  puts "(injection build left in place; run `rake clean compile` to restore a normal build)"
655
667
  end
656
668
 
@@ -795,10 +807,11 @@ namespace :fuzz do
795
807
  # (halt_on_error). The default (isolated) is the complete net:
796
808
  # it also survives + attributes a genuine segfault and catches a
797
809
  # hang via the per-query timeout, at much lower throughput.
810
+ # ISOLATED=0 same as FAST=1; ISOLATED=1 (like the default) keeps isolation.
798
811
  # SKIP_BUILD=1 reuse the current build instead of rebuilding (refuses to run
799
812
  # if it is not a sanitizer build, so you never fuzz a plain ext).
800
813
  # FUZZ_TIME=N seconds per surface (default 90).
801
- # FUZZ_ARGS=... run a single custom invocation instead of the three surfaces.
814
+ # FUZZ_ARGS=... run a single custom invocation instead of the five surfaces.
802
815
  desc "Run the fuzzer under AddressSanitizer (FAST=1 non-isolated, SKIP_BUILD=1 reuse build)"
803
816
  task :sanitize do
804
817
  sanitize = ENV["MAKIRI_SANITIZE"] || "address"
@@ -818,12 +831,15 @@ namespace :fuzz do
818
831
  if ENV["FUZZ_ARGS"]
819
832
  sh(env, "#{FileUtils::RUBY} -Ilib spec/fuzz/run.rb #{ENV['FUZZ_ARGS']}")
820
833
  else
821
- iso = %w[1 true yes].include?(ENV["ISOLATED"].to_s.downcase) ? "--isolated" : ""
834
+ truthy = ->(name) { %w[1 true yes].include?(ENV[name].to_s.downcase) }
835
+ explicit_isolated = ENV.key?("ISOLATED") && !ENV["ISOLATED"].to_s.strip.empty?
836
+ isolated = explicit_isolated ? truthy.call("ISOLATED") : !truthy.call("FAST")
837
+ iso = isolated ? "--isolated" : "--in-process"
822
838
  secs = ENV["FUZZ_TIME"] || "90"
823
- # Cover every surface under the sanitizer: the query engine (XPath/CSS over
824
- # parsed fixtures), the XML parser (hostile documents), and the XML mutation
825
- # surface (random edit sequences + invariants).
826
- ["", "--target xml", "--target mutate", "--target xmlcss"].each do |surface|
839
+ # Cover every surface under the sanitizer: the query engine (XPath over the
840
+ # default fixtures), the HTML CSS selector path, the XML parser (hostile
841
+ # documents), the XML mutation surface, and CSS over an XML document.
842
+ ["", "--target css", "--target xml", "--target mutate", "--target xmlcss"].each do |surface|
827
843
  sh(env, "#{FileUtils::RUBY} -Ilib spec/fuzz/run.rb #{surface} #{iso} --time #{secs}".squeeze(" ").strip)
828
844
  end
829
845
  end
@@ -841,7 +857,7 @@ namespace :fuzz do
841
857
  # library and headers (the crate's build.rs links the archive and generates
842
858
  # the layout from the headers), which `rake compile` produces - hence the
843
859
  # dependency, which is about Lexbor rather than about the bundle.
844
- FUZZ_TARGETS = %w[xml xpath xml_xpath css html html_xpath].freeze
860
+ FUZZ_TARGETS = %w[xml xpath xml_xpath css html html_xpath html_css].freeze
845
861
 
846
862
  # The local mode: TARGETS=css,html_xpath narrows the run to what you touched,
847
863
  # and the time is FUZZ_TIME seconds per target (default 60) or FUZZ_BUDGET
@@ -23,7 +23,7 @@ path = "src/lib.rs"
23
23
  # about an unexpected cfg name (and CI's `clippy -D warnings` turns that into an
24
24
  # error). The proof modules are compiled only under it.
25
25
  [lints.rust]
26
- unexpected_cfgs = { level = "warn", check-cfg = ['cfg(kani)'] }
26
+ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(kani)', 'cfg(makiri_asan)'] }
27
27
 
28
28
  # The panic gate. A panic now reaches Ruby as an exception (see the profile
29
29
  # below), but it is still an INTERNAL error nobody asked for - `rescue => e`
@@ -32,15 +32,15 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(kani)'] }
32
32
  # few sites that want one carry an `#[allow]` with a reason. Test code is
33
33
  # exempt in `lib.rs`.
34
34
  #
35
- # `expect_used` is deliberately NOT here: 223 of its hits are the
36
- # `.expect("#name")` of method registration, which cannot fail after a
37
- # successful `Init_makiri`, and allowing each would say nothing. Nor are
38
- # `indexing_slicing` and `arithmetic_side_effects` (469 hits, mostly the
35
+ # `expect_used` joined them once method registration stopped `.expect`ing each
36
+ # definition (223 hits, now `?`): what remains is a handful of invariants, each
37
+ # with an `#[allow(..., reason = ...)]`. Not here are `indexing_slicing` and `arithmetic_side_effects` (469 hits, mostly the
39
38
  # engine's own bounds-checked walks): gating those needs a different argument
40
39
  # than "a panic aborts", since release builds keep `overflow-checks` on
41
40
  # precisely so a wrapped size stops the process instead of answering wrong.
42
41
  [lints.clippy]
43
42
  unwrap_used = "warn"
43
+ expect_used = "warn"
44
44
  panic = "warn"
45
45
 
46
46
  [features]
@@ -88,13 +88,13 @@ rb-sys = { version = "0.9", optional = true }
88
88
  # an `extern "C"` boundary into an abort, because unwinding through C frames
89
89
  # built without unwind tables is undefined behaviour. So every callback catches
90
90
  # its own panic and the caller re-raises it past the C frames - `caught::
91
- # PanicLatch`, installed in all nine (the CSS traversal, both serializer sinks,
91
+ # PanicLatch`, installed in all eight (the CSS traversal, the serializer sink,
92
92
  # the tokenizer hook, the GVL trampoline, and `bridge::ruby::protect`, which
93
93
  # covers every `rb_protect`). The GC callbacks in `bridge/typed.rs` keep
94
94
  # aborting on purpose; see CLAUDE.md.
95
95
  #
96
96
  # Anything that must be released across a panic is therefore a `Drop`, not a
97
- # statement after the work. `lexbor::selectors::PanicReset` and
97
+ # statement after the work. `lexbor::selectors::Session` and
98
98
  # `post_parse::DocOwner` are the two that had to be converted.
99
99
  #
100
100
  # overflow-checks stays ON in release: a wrapped size is exactly the class of