makiri 0.12.0-aarch64-linux → 0.13.0-aarch64-linux

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '080648651b5e8e38a405373a252b0ded69f1e5a672c6e5031e0f31058b09fa80'
4
- data.tar.gz: c13624a3a5ed5e94e4d0adc761280e498fbf3e34891ba41d46cfeeed762bcf68
3
+ metadata.gz: c38568d5e92468d6f4da1275cf823cdc615d079c6a0293236ad5c85897647df1
4
+ data.tar.gz: 33701440893a286012dcdba48d78cd30d69af8d27fff9bc3691603e5e4151900
5
5
  SHA512:
6
- metadata.gz: ba444c67f4c84cbe0d3a9317ddcfbbf6854daf1c9fa0a020e16e319ac9d8e98e693fa0b245ba8f3abb3bf9e0f7837af77238c7c0d4557d9d268d1b0465bd24cd
7
- data.tar.gz: c822e0c488221872b85fc6d8390dea3ee1b72def3c35ae160750102a26115047a84ca0f06df2f0b3ee34b7b3dacc011edbb7c56de27cff93e96fb7e9f9132360
6
+ metadata.gz: '0586c38ca0fbbac3cc9da9d3147db3d58c2f1380991a06bf412f17d92029023fbfca96289c91b8bfc30a1a25e1a26a8df8e8e5ada81cbfaf62074eae396868d1'
7
+ data.tar.gz: 85f351046a3e65e9e0c8f8d725b5aa585555f6055798f4d80ace09fe16f8ef28b8f00124ac9ce7de3e5e1047ae0b01cf263636f712818d402600bc0e996b1e9f
@@ -0,0 +1,180 @@
1
+ name: CSS matcher
2
+
3
+ # The HTML CSS matcher (`lexbor::css_match`) on its own: it answers every
4
+ # `Node#css` / `#at_css` / `#matches?`, and its failure mode is not a crash
5
+ # but a wrong answer, which the general suites only catch by example. Two
6
+ # checks, both against the OLD engine - Lexbor's own `lxb_selectors`, kept as
7
+ # `lexbor::selectors` for exactly this:
8
+ #
9
+ # - the randomized differential tests in `lexbor::tests::css_match`, run long
10
+ # with a fresh seed (MAKIRI_CSS_DIFF_SEED; a failure prints it);
11
+ # - the cargo-fuzz targets `html_css` (memory safety, and the three entry
12
+ # points agreeing) and `html_css_diff` (agreement with Lexbor's engine),
13
+ # with the selector dictionary.
14
+ #
15
+ # A PR that touches the matcher, the Lexbor layer or the pin gets a short run;
16
+ # the nightly one is long. A disagreement on a shape where css_match departs
17
+ # from Lexbor on purpose is not a finding: the departures are listed in
18
+ # `css_match`'s module doc, and both checks leave them out.
19
+
20
+ on:
21
+ push:
22
+ branches: [main, master]
23
+ paths:
24
+ - "ext/makiri/rust/src/lexbor/**"
25
+ - "ext/makiri/rust/fuzz/**"
26
+ - "ext/makiri/rust/Cargo.toml"
27
+ - "vendor/lexbor"
28
+ - ".github/workflows/css-match.yml"
29
+ pull_request:
30
+ paths:
31
+ - "ext/makiri/rust/src/lexbor/**"
32
+ - "ext/makiri/rust/fuzz/**"
33
+ - "ext/makiri/rust/Cargo.toml"
34
+ - "vendor/lexbor"
35
+ - ".github/workflows/css-match.yml"
36
+ schedule:
37
+ - cron: "41 4 * * *"
38
+ workflow_dispatch:
39
+
40
+ env:
41
+ # Short on a push or PR, long on the nightly schedule.
42
+ LONG: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
43
+
44
+ jobs:
45
+ differential:
46
+ name: Randomized differential (vs Lexbor's engine)
47
+ runs-on: ubuntu-latest
48
+ timeout-minutes: 60
49
+ steps:
50
+ - name: Checkout (with vendored Lexbor submodule)
51
+ uses: actions/checkout@v6
52
+ with:
53
+ submodules: recursive
54
+
55
+ - name: Ensure cmake is available
56
+ uses: lukka/get-cmake@latest
57
+
58
+ - uses: dtolnay/rust-toolchain@stable
59
+
60
+ - name: Install libclang (bindgen, via rb-sys)
61
+ run: sudo apt-get update && sudo apt-get install -y libclang-dev
62
+
63
+ - name: Set up Ruby
64
+ uses: ruby/setup-ruby@v1
65
+ with:
66
+ ruby-version: "3.4"
67
+ bundler-cache: true
68
+
69
+ # build.rs links the vendored Lexbor archive and reads its headers.
70
+ - name: Compile the extension (builds Lexbor)
71
+ run: bundle exec rake compile
72
+
73
+ # Release: the sweep is the point, and debug is ~10x slower. The seed is
74
+ # fresh per run, so a long series covers new ground; it is printed here
75
+ # and in any failure, so a finding reproduces.
76
+ - name: Run the css_match tests with a fresh seed
77
+ working-directory: ext/makiri/rust
78
+ run: |
79
+ export MAKIRI_CSS_DIFF_SEED=$(printf '0x%x' $(( (GITHUB_RUN_ID * 2654435761 + GITHUB_RUN_ATTEMPT) & 0xffffffffffff )))
80
+ if [ "$LONG" = true ]; then
81
+ export MAKIRI_CSS_DIFF_ITERATIONS=1000000
82
+ else
83
+ export MAKIRI_CSS_DIFF_ITERATIONS=20000
84
+ fi
85
+ echo "MAKIRI_CSS_DIFF_SEED=$MAKIRI_CSS_DIFF_SEED MAKIRI_CSS_DIFF_ITERATIONS=$MAKIRI_CSS_DIFF_ITERATIONS"
86
+ cargo test --release --no-default-features --features lexbor -- \
87
+ lexbor::tests::css_match lexbor::tests::selector_cache
88
+
89
+ fuzz:
90
+ name: cargo-fuzz ${{ matrix.target }}
91
+ runs-on: ubuntu-latest
92
+ timeout-minutes: 120
93
+ strategy:
94
+ fail-fast: false
95
+ matrix:
96
+ target: [html_css, html_css_diff]
97
+ steps:
98
+ - name: Checkout (with vendored Lexbor submodule)
99
+ uses: actions/checkout@v6
100
+ with:
101
+ submodules: recursive
102
+
103
+ - name: Ensure cmake is available
104
+ uses: lukka/get-cmake@latest
105
+
106
+ # -Zsanitizer is unstable, and cargo-fuzz needs it.
107
+ - uses: dtolnay/rust-toolchain@nightly
108
+
109
+ - name: Install libclang (bindgen, via rb-sys)
110
+ run: sudo apt-get update && sudo apt-get install -y libclang-dev
111
+
112
+ - name: Set up Ruby
113
+ uses: ruby/setup-ruby@v1
114
+ with:
115
+ ruby-version: "3.4"
116
+ bundler-cache: true
117
+
118
+ - name: Compile the extension (builds Lexbor)
119
+ run: bundle exec rake compile
120
+
121
+ - name: Install cargo-fuzz
122
+ run: cargo install cargo-fuzz --locked
123
+
124
+ - name: Build the fuzz target
125
+ working-directory: ext/makiri/rust/fuzz
126
+ run: cargo fuzz build ${{ matrix.target }}
127
+
128
+ # As in libfuzzer.yml: nm -u reads 0 on Linux whether or not the binary
129
+ # is instrumented, so assert on the full symbol table.
130
+ - name: Assert the target is instrumented
131
+ working-directory: ext/makiri/rust/fuzz
132
+ run: |
133
+ bin=$(ls target/*/release/${{ matrix.target }})
134
+ reports=$(nm "$bin" | grep -c '__asan_report')
135
+ echo "$bin: __asan_report symbols = $reports"
136
+ test "$reports" -gt 0 || { echo "not ASan-instrumented"; exit 1; }
137
+
138
+ # Both targets read the same `selector NUL document` input, so each
139
+ # also starts from the other's corpus (read-only: new units go to the
140
+ # first directory). The cache prefix is libfuzzer.yml's, so the corpus
141
+ # accumulated there carries over.
142
+ - name: Restore the corpus
143
+ uses: actions/cache@v6
144
+ with:
145
+ path: ext/makiri/rust/fuzz/corpus/${{ matrix.target }}
146
+ key: cargo-fuzz-corpus-${{ matrix.target }}-${{ github.run_id }}
147
+ restore-keys: |
148
+ cargo-fuzz-corpus-${{ matrix.target }}-
149
+
150
+ - name: Restore the sibling corpus
151
+ uses: actions/cache/restore@v6
152
+ with:
153
+ path: ext/makiri/rust/fuzz/corpus/${{ matrix.target == 'html_css' && 'html_css_diff' || 'html_css' }}
154
+ key: cargo-fuzz-corpus-${{ matrix.target == 'html_css' && 'html_css_diff' || 'html_css' }}-${{ github.run_id }}
155
+ restore-keys: |
156
+ cargo-fuzz-corpus-${{ matrix.target == 'html_css' && 'html_css_diff' || 'html_css' }}-
157
+
158
+ # -timeout: html_css_diff runs Lexbor's exhaustively backtracking engine
159
+ # too; the harness skips inputs it cannot afford, and this is the stop
160
+ # for one it misjudged.
161
+ - name: Run cargo-fuzz ${{ matrix.target }}
162
+ working-directory: ext/makiri/rust/fuzz
163
+ run: |
164
+ sibling=${{ matrix.target == 'html_css' && 'html_css_diff' || 'html_css' }}
165
+ mkdir -p corpus/${{ matrix.target }} corpus/$sibling
166
+ if [ "$LONG" = true ]; then seconds=3600; else seconds=180; fi
167
+ cargo fuzz run ${{ matrix.target }} corpus/${{ matrix.target }} corpus/$sibling -- \
168
+ -max_total_time=$seconds \
169
+ -max_len=4096 \
170
+ -timeout=30 \
171
+ -dict=css.dict \
172
+ -print_final_stats=1
173
+
174
+ - name: Upload reproducers
175
+ if: failure()
176
+ uses: actions/upload-artifact@v4
177
+ with:
178
+ name: cargo-fuzz-artifacts-${{ matrix.target }}
179
+ path: ext/makiri/rust/fuzz/artifacts/${{ matrix.target }}
180
+ retention-days: 30
@@ -25,7 +25,9 @@ jobs:
25
25
  strategy:
26
26
  fail-fast: false
27
27
  matrix:
28
- target: [xml, xpath, xml_xpath, css, html, html_xpath, html_css]
28
+ # html_css and html_css_diff run in css-match.yml, with the selector
29
+ # dictionary and a longer budget.
30
+ target: [xml, xpath, xml_xpath, css, html, html_xpath]
29
31
 
30
32
  steps:
31
33
  - name: Checkout (with vendored Lexbor submodule)
data/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.13.0] - 2026-10-03
4
+
5
+ ### Added
6
+
7
+ * `Document#tree_version` (HTML and XML): an Integer that increases whenever
8
+ a child list in the document changes - nodes added, removed or replaced,
9
+ `inner_html=`, `content=`, and both documents when a node moves between
10
+ them. Attribute changes leave it as it is, so it can key a cache of child
11
+ lists.
12
+ * `Element#attribute_value_ns(ns, local)` and `#attribute_node_ns(ns, local)`
13
+ (HTML and XML): the attribute's value or Attr node by namespace and local
14
+ name, like the DOM's `getAttributeNS` / `getAttributeNodeNS`. `nil` or `""`
15
+ means no namespace.
16
+ * `Element#set_loose_dom_attribute(name, value)` now works on HTML nodes too.
17
+ Like the DOM's `setAttribute`, it sets an attribute in no namespace whose
18
+ name may contain a colon (`v-on:click`).
19
+
20
+ ### Fixed
21
+
22
+ * HTML documents keep namespace URIs as written. `create_element_ns`,
23
+ `set_attribute_ns` and `import_node` lower-cased them (`fooNamespace` read
24
+ back as `foonamespace`), and a differently cased XHTML namespace URI made
25
+ an HTML element.
26
+
27
+ ## [0.12.1] - 2026-10-02
28
+
29
+ ### Fixed
30
+
31
+ * Passing a string literal from a file without
32
+ `# frozen_string_literal: true` no longer warns "literal string will be
33
+ frozen in the future" on Ruby 3.4+ (with deprecation warnings enabled).
34
+ * A fragment parsed with a context element from another document
35
+ (`fragment(context:)`, `DocumentFragment.parse(context:)`) no longer keeps
36
+ a reference into that document, which could be read after that document was
37
+ freed.
38
+ * The 10,000-option limit per `<select>` now also counts the options a
39
+ fragment parsed in a select context receives (`inner_html=` and
40
+ `outer_html=` on or inside a select, `fragment(context: "select")`).
41
+ Before, they went uncounted, and inserting them took time quadratic in
42
+ their number.
43
+ * A String `context:` naming a node that is not an element (`"#text"`,
44
+ `"!--"`, ...) raises `ArgumentError`.
45
+
46
+ ### Security
47
+
48
+ * Hardening against Lexbor edge cases in fragment parsing, node copying,
49
+ element creation and serialization. Serialization is now always Makiri's
50
+ own walk, which uses no native recursion, so `to_html` / `inner_html` are
51
+ about 3% slower.
52
+
3
53
  ## [0.12.0] - 2026-10-01
4
54
 
5
55
  Most changes accept what the DOM allows where 0.11.0 raised. The XML
data/README.md CHANGED
@@ -50,7 +50,7 @@ doc = Makiri::HTML(<<~HTML)
50
50
  </body></html>
51
51
  HTML
52
52
 
53
- # CSS selectors (Lexbor's selector engine)
53
+ # CSS selectors (Lexbor's selector parser, Makiri's own matcher)
54
54
  doc.css("a").map { |a| a["href"] } # => ["/a", "/b"]
55
55
  doc.at_css("p.lead").text # => "Hello"
56
56
 
@@ -184,6 +184,27 @@ and unusually large documents can raise it with `max_bytes:`.
184
184
  Makiri::XML(huge_xml, max_bytes: 512 * 1024 * 1024) # also Makiri::XML::Document.parse(..., max_bytes:)
185
185
  ```
186
186
 
187
+ ### Node identity
188
+
189
+ Navigating to the same node always gives the same Ruby object, for as long as
190
+ its document is alive. The document keeps every wrapper it has handed out, so
191
+ instance variables, singleton methods and `freeze` set on a node survive
192
+ garbage collection and are there the next time the node is reached.
193
+
194
+ That makes a node a safe place to keep per-node state, such as a wrapper
195
+ object of your own:
196
+
197
+ ```ruby
198
+ el = doc.at_css("p")
199
+ el.instance_variable_set(:@wrapper, MyElement.new(el))
200
+ doc.at_css("p").instance_variable_get(:@wrapper) # => the same MyElement
201
+ ```
202
+
203
+ A node created by `clone_node` or `import_node` is a new node with no state.
204
+ `pointer_id` is unique only among live nodes: once a document is freed, a node
205
+ of another document may reuse its value. So key a pointer-based cache per
206
+ document, not across documents.
207
+
187
208
  ## Non-goals (v1.0)
188
209
 
189
210
  * XSLT, DTD / Schema / RelaxNG validation, XPointer, XInclude.
@@ -224,6 +245,7 @@ See also [`spec/conformance/README.md`](spec/conformance/README.md).
224
245
  | XPath 1.0 | XML | `Nokogiri::XML` — differential | `conformance:xpath_xml` |
225
246
  | Parsed tree (property-based) | XML | `Nokogiri::XML` — differential | `conformance:xml_pbt` |
226
247
  | CSS selectors | XML | `Nokogiri::XML` — differential | `conformance:css_xml` |
248
+ | XML Builder | Ruby DSL | `Nokogiri::XML::Builder` — differential | `conformance:builder` |
227
249
 
228
250
  ## Requirements
229
251
 
data/Rakefile CHANGED
@@ -857,7 +857,9 @@ namespace :fuzz do
857
857
  # library and headers (the crate's build.rs links the archive and generates
858
858
  # the layout from the headers), which `rake compile` produces - hence the
859
859
  # dependency, which is about Lexbor rather than about the bundle.
860
- FUZZ_TARGETS = %w[xml xpath xml_xpath css html html_xpath html_css].freeze
860
+ FUZZ_TARGETS = %w[xml xpath xml_xpath css html html_xpath html_css html_css_diff].freeze
861
+ # The targets whose input starts with a CSS selector take its dictionary.
862
+ FUZZ_CSS_DICT = %w[html_css html_css_diff].freeze
861
863
 
862
864
  # The local mode: TARGETS=css,html_xpath narrows the run to what you touched,
863
865
  # and the time is FUZZ_TIME seconds per target (default 60) or FUZZ_BUDGET
@@ -916,8 +918,9 @@ namespace :fuzz do
916
918
  env, sanitizer = libfuzzer_invocation
917
919
  Dir.chdir("ext/makiri/rust/fuzz") do
918
920
  targets.each do |target|
921
+ dict = FUZZ_CSS_DICT.include?(target) ? ["-dict=css.dict"] : []
919
922
  sh env, "cargo", "fuzz", "run", *sanitizer, target, "--",
920
- "-max_total_time=#{time}", "-max_len=4096"
923
+ "-max_total_time=#{time}", "-max_len=4096", *dict
921
924
  end
922
925
  end
923
926
  end
Binary file
Binary file
Binary file
Binary file
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Makiri
4
- VERSION = "0.12.0"
4
+ VERSION = "0.13.0"
5
5
  end
@@ -31,14 +31,14 @@ RUST = File.join(ROOT, "ext/makiri/rust/src")
31
31
  UNSAFE_ISLANDS = {
32
32
  "bridge/alloc.rs" => 4,
33
33
  "bridge/doc.rs" => 3,
34
- "bridge/fragment.rs" => 5,
34
+ "bridge/fragment.rs" => 6,
35
35
  "bridge/gvl.rs" => 5,
36
36
  "bridge/html.rs" => 17,
37
37
  "bridge/node_set.rs" => 8,
38
38
  "bridge/node_wrap.rs" => 1,
39
39
  "bridge/ruby.rs" => 18,
40
40
  "bridge/stack.rs" => 1,
41
- "bridge/string.rs" => 28,
41
+ "bridge/string.rs" => 29,
42
42
  "bridge/typed.rs" => 21,
43
43
  "bridge/wrapper.rs" => 19,
44
44
  "bridge/xml.rs" => 11,
@@ -55,15 +55,15 @@ UNSAFE_ISLANDS = {
55
55
  "lexbor/abi.rs" => 4,
56
56
  "lexbor/adapter/arena_bytes.rs" => 14,
57
57
  "lexbor/adapter/cross_import.rs" => 2,
58
- "lexbor/adapter/html/attrs.rs" => 20,
59
- "lexbor/adapter/html/build.rs" => 19,
60
- "lexbor/adapter/html/mod.rs" => 60,
58
+ "lexbor/adapter/html/attrs.rs" => 23,
59
+ "lexbor/adapter/html/build.rs" => 25,
60
+ "lexbor/adapter/html/mod.rs" => 63,
61
61
  "lexbor/adapter/html/mutate.rs" => 9,
62
- "lexbor/adapter/html/serialize.rs" => 9,
62
+ "lexbor/adapter/html/serialize.rs" => 18,
63
63
  "lexbor/adapter/post_parse.rs" => 13,
64
64
  "lexbor/adapter/source_loc.rs" => 2,
65
65
  "lexbor/adapter/text_index.rs" => 1,
66
- "lexbor/adapter/tree_guard.rs" => 8,
66
+ "lexbor/adapter/tree_guard.rs" => 9,
67
67
  "lexbor/chunks.rs" => 2,
68
68
  "lexbor/css_engine.rs" => 14,
69
69
  "lexbor/css_parser.rs" => 21,
@@ -74,7 +74,7 @@ UNSAFE_ISLANDS = {
74
74
  "lexbor/selectors.rs" => 9,
75
75
  "lexbor/serialize.rs" => 2,
76
76
  "lexbor/stylesheet.rs" => 7,
77
- "lexbor/tests.rs" => 14,
77
+ "lexbor/tests.rs" => 16,
78
78
  "lexbor/xpath.rs" => 9,
79
79
  "token.rs" => 1,
80
80
  }.freeze
@@ -462,7 +462,7 @@ end
462
462
  # is generated, so its signature is Lexbor's. A new hand declaration of a
463
463
  # header-declared function belongs in build.rs's allowlist instead; one with no
464
464
  # header belongs in build.rs's UNDECLARED_EXPORTS as well as here.
465
- LEXBOR_HAND_DECLS = 4
465
+ LEXBOR_HAND_DECLS = 5
466
466
  abi_decls = comments_removed(File.binread(File.join(RUST, "lexbor/abi.rs"))).scan(LEXBOR_DECL).length
467
467
  if abi_decls != LEXBOR_HAND_DECLS
468
468
  errors << "lexbor/abi.rs hand-declares #{abi_decls} Lexbor functions (expected " \
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: makiri
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.13.0
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - takahashim
@@ -64,6 +64,7 @@ extra_rdoc_files: []
64
64
  files:
65
65
  - ".github/workflows/ci.yml"
66
66
  - ".github/workflows/conformance.yml"
67
+ - ".github/workflows/css-match.yml"
67
68
  - ".github/workflows/libfuzzer.yml"
68
69
  - ".github/workflows/release.yml"
69
70
  - ".github/workflows/security.yml"