makiri 0.3.0-aarch64-linux → 0.4.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.
@@ -93,3 +93,22 @@ ignore_paths:
93
93
  - path: ext/makiri/glue/ruby_node_set.c
94
94
  rule: node_raw_boundary
95
95
  reason: NodeSet.new stores representation-opaque pointers; mkr_node_raw takes a seed node's pointer only after same-document validation guarantees its kind matches the set.
96
+
97
+ # --- parser-TU reader discipline (raw_scan_call / raw_cursor_member) ---
98
+ allowlist:
99
+ - path: ext/makiri/xpath/mkr_xpath_funcs_body.h
100
+ rule: raw_scan_call
101
+ max: 3
102
+ reason: "mkr_lookup_function's three strcmp over compile-time function-name tables: both sides are NUL-terminated (table literals / owned AST text), and the lookup signature takes bare const char* from the evaluator."
103
+
104
+ # --- verified_text_forge: the two Ruby-free test entry points. Neither can use
105
+ # mkr_verified_text_from_view (bridge = Ruby boundary), so each mints the
106
+ # token itself and must supply the contract by construction. ---
107
+ - path: ext/makiri/xpath/mkr_xpath_xml_selftest.c
108
+ rule: verified_text_forge
109
+ max: 2
110
+ reason: "Selftest expressions are compile-time string literals: NUL-terminated, NUL-free, valid UTF-8 by construction."
111
+ - path: ext/makiri/fuzz/xpath_fuzz.c
112
+ rule: verified_text_forge
113
+ max: 1
114
+ reason: "The libFuzzer harness mints over an owned mkr_strndup copy, which supplies NUL-termination and no-interior-NUL; UTF-8 validity is deliberately left to the lexer's strict decoder (its rejection path is fuzz-target behavior)."
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Malloc-leak gate for the C extension (macOS only; run via `rake leaks`).
4
+ #
5
+ # ASan runs everywhere with detect_leaks=0 (Ruby and Lexbor are uninstrumented,
6
+ # so LeakSanitizer drowns in their noise) - which means plain leaks were never
7
+ # machine-checked. This gate fills that hole with macOS's `leaks` tool: it runs
8
+ # script/leaks_harness.rb (the public surface in a loop, INCLUDING rescued
9
+ # failure paths) under MallocStackLogging, then scans the leak report for
10
+ # allocation stacks that (a) pass through makiri.bundle and (b) repeat in
11
+ # proportion to the loop count - i.e. a leak per call, which is what a missing
12
+ # free on some path looks like. One-time Init allocations and Ruby's own
13
+ # at-exit/unscannable noise stay at 1-2 instances and are ignored.
14
+ #
15
+ # Found (and since fixed) by this harness: the transient fragment document
16
+ # leaked by every inner_html=/outer_html=, and the partially-built step leaked
17
+ # by every failing XPath parse.
18
+ #
19
+ # ruby script/check_leaks.rb # threshold = ITERATIONS / 4
20
+ # LEAKS_ITERATIONS=200 rake leaks # more iterations, sharper signal
21
+
22
+ require "rbconfig"
23
+ require "tempfile"
24
+
25
+ abort "check_leaks: the `leaks` tool is macOS-only" unless RUBY_PLATFORM.include?("darwin")
26
+
27
+ iterations = Integer(ENV.fetch("LEAKS_ITERATIONS", "120"))
28
+ threshold = [iterations / 4, 10].max
29
+ harness = File.expand_path("leaks_harness.rb", __dir__)
30
+ lib = File.expand_path("../lib", __dir__)
31
+
32
+ out = Tempfile.create("makiri-leaks")
33
+ ok = system({ "MallocStackLogging" => "1", "LEAKS_ITERATIONS" => iterations.to_s },
34
+ "leaks", "--atExit", "--",
35
+ RbConfig.ruby, "-I#{lib}", harness,
36
+ out: out.path, err: out.path)
37
+ report = File.read(out.path)
38
+ # `leaks` exits non-zero whenever ANY leak exists (Ruby itself always reports
39
+ # some at-exit noise), so the exit status is not the verdict - the scan below
40
+ # is. But the harness itself must have completed.
41
+ abort "check_leaks: harness did not complete:\n#{report[-2000..]}" unless report.include?("leaks harness done")
42
+ abort "check_leaks: no leak report produced (leaks tool failed?)" unless report.include?("STACK OF") || ok
43
+
44
+ offenders = report.split(/\n(?=STACK OF )/).filter_map do |stanza|
45
+ next unless stanza.include?("makiri.bundle")
46
+
47
+ instances = stanza[/STACK OF (\d+) INSTANCES?/, 1].to_i
48
+ next if instances < threshold
49
+
50
+ frames = stanza.lines.grep(/makiri\.bundle/).first(4).map(&:strip)
51
+ [instances, frames]
52
+ end
53
+
54
+ if offenders.empty?
55
+ puts "check_leaks: OK - no repeated (>= #{threshold}x) leak stacks through makiri.bundle " \
56
+ "(#{iterations} iterations)"
57
+ else
58
+ puts "check_leaks: FAILED - #{offenders.size} repeated leak stack(s) through makiri.bundle:"
59
+ offenders.each do |instances, frames|
60
+ puts " #{instances}x:"
61
+ frames.each { |f| puts " #{f}" }
62
+ end
63
+ exit 1
64
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Exercise script for the malloc-leak gate (script/check_leaks.rb runs this
4
+ # under macOS `leaks --atExit`). It loops the public surface - parsing, queries,
5
+ # serialization, mutation, fragments, the Builder, XPathContext - and, crucially,
6
+ # RESCUED FAILURE paths (raises that skip cleanup are where leaks hide; both
7
+ # leaks this gate was built on lived there or in a transient-document path).
8
+ #
9
+ # A per-call leak shows up as a leak stack with ~ITERATIONS instances, which the
10
+ # driver flags; one-time/Init allocations stay at 1-2 instances and pass.
11
+
12
+ require "makiri"
13
+
14
+ ITERATIONS = Integer(ENV.fetch("LEAKS_ITERATIONS", "120"))
15
+
16
+ HTML = "<div id=m class='a b'><ul><li class=item>x</li><li>y<svg><path/></svg></li></ul><p>t&amp;</p></div>"
17
+ XML = %(<r xmlns:p="urn:p" xmlns="urn:d"><a id="1">t</a><p:b/><!--c--><![CDATA[z]]></r>)
18
+
19
+ handler = Class.new { def my_fn(nodes) = nodes.length.to_s }.new
20
+
21
+ ITERATIONS.times do |i|
22
+ # --- HTML: parse / query / serialize / mutate / fragments ---
23
+ d = Makiri::HTML(HTML)
24
+ d.css("li.item"); d.at_css("#m"); d.at_css("li").matches?("li")
25
+ begin d.css("li[") rescue Makiri::CSS::SyntaxError; end # selector syntax error (engine reset path)
26
+ d.xpath("//li"); d.at_xpath("//p"); d.xpath("count(//li)")
27
+ begin d.xpath("//li[") rescue Makiri::XPath::SyntaxError; end # parse failure (partial-AST/step cleanup)
28
+ begin d.xpath("//li", handler) rescue nil; end
29
+ d.xpath("//*[local-name()='path']")
30
+ d.to_html; d.at_css("ul").inner_html; d.to_html(pretty: true); d.text
31
+ e = d.at_css("li"); e["k#{i}"] = "v"; e.name = "li2"; e.content = "c"
32
+ e.set_attribute_ns("urn:x", "x:y", "1"); e.remove_attribute_ns("urn:x", "y")
33
+ d.at_css("ul").inner_html = "<li>new</li>" # transient fragment document path
34
+ frag = d.fragment("<b>f</b>"); d.at_css("p") << frag
35
+ Makiri::DocumentFragment.parse("<tr><td>1</td></tr>", context: "tbody")
36
+ d.at_css("p").clone_node(true); d.dup
37
+ begin d.fragment("x", context: "no-such-tag") rescue ArgumentError; end
38
+
39
+ # --- XML: parse (ok + failures) / query / serialize / mutate / Builder ---
40
+ x = Makiri::XML(XML)
41
+ begin Makiri::XML("<r>\xC3</r>".b) rescue Makiri::XML::SyntaxError; end
42
+ begin Makiri::XML("<r><a></r>") rescue Makiri::XML::SyntaxError; end
43
+ begin Makiri::XML("<r/>", max_bytes: 1) rescue Makiri::XML::LimitExceeded; end
44
+ begin Makiri::XML(%(<?xml version="1.1"?><r/>)) rescue Makiri::XML::SyntaxError; end
45
+ x.xpath("//d:a", "d" => "urn:d"); x.at_xpath("//p:b", "p" => "urn:p")
46
+ begin x.xpath("//unbound:a") rescue Makiri::Error; end
47
+ x.css("a"); x.at_css("p|b", "p" => "urn:p")
48
+ begin x.css("a[") rescue Makiri::CSS::SyntaxError; end
49
+ x.to_xml; x.to_xml(pretty: true); x.root.canonicalize
50
+ el = x.create_element("n", "t", "k" => "v"); x.root.add_child(el)
51
+ begin x.root.add_child(x.create_element("zz:q")) rescue Makiri::Error; end
52
+ x.root.children.first.replace(x.create_element("rep")); x.fragment("<f1/><f2/>")
53
+ Makiri::XML::Builder.new { |b| b.root("xmlns:d" => "urn:d") { b.item("a"); b["d"].q } }.to_xml
54
+
55
+ # --- XPathContext: AST cache / registrations / failing evaluate ---
56
+ ctx = Makiri::XPathContext.new(x)
57
+ ctx.register_namespace("d", "urn:d"); ctx.register_variable("v", "1")
58
+ ctx.evaluate("//d:a[@id=$v]"); ctx.evaluate("//d:a[@id=$v]")
59
+ begin ctx.evaluate("//(") rescue Makiri::XPath::SyntaxError; end
60
+ end
61
+
62
+ GC.start
63
+ GC.start
64
+ puts "leaks harness done (#{ITERATIONS} iterations)"
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.3.0
4
+ version: 0.4.0
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - takahashim
@@ -64,8 +64,10 @@ extra_rdoc_files: []
64
64
  files:
65
65
  - ".github/workflows/ci.yml"
66
66
  - ".github/workflows/conformance.yml"
67
+ - ".github/workflows/libfuzzer.yml"
67
68
  - ".github/workflows/release.yml"
68
69
  - ".github/workflows/security.yml"
70
+ - ".github/workflows/valgrind.yml"
69
71
  - ".gitmodules"
70
72
  - CHANGELOG.md
71
73
  - LICENSE
@@ -95,13 +97,17 @@ files:
95
97
  - lib/makiri/text.rb
96
98
  - lib/makiri/version.rb
97
99
  - lib/makiri/xml.rb
100
+ - lib/makiri/xml/builder.rb
98
101
  - lib/makiri/xml/document.rb
99
102
  - lib/makiri/xml/node_methods.rb
100
103
  - lib/makiri/xpath.rb
101
104
  - lib/makiri/xpath_context.rb
102
105
  - script/build_native_gem.rb
106
+ - script/check_alloc_failures.rb
103
107
  - script/check_c_safety.rb
104
108
  - script/check_c_safety_allowlist.yml
109
+ - script/check_leaks.rb
110
+ - script/leaks_harness.rb
105
111
  - sig/makiri.rbs
106
112
  homepage: https://github.com/takahashim/makiri
107
113
  licenses: