leptris 1.9.312.0-aarch64-linux → 1.9.313.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: 9de71b8c23036b66ec08f42ba8d76658c4e0b434d1310edd0f25b22c67685cfa
4
- data.tar.gz: b6a93975b39dd5c601e021c4265ddc4ad50af9e4542c161d946d992110b02fbc
3
+ metadata.gz: 32b80b7b9678dfa98c186b9fa738538d14906537f79e5d327a72afe44bb81bdf
4
+ data.tar.gz: e2b05a7d9ad9ea690fa7467d8aa5b1e3c2b2f2e7ca5a50a1e82005cb97b03e8b
5
5
  SHA512:
6
- metadata.gz: fbd674dc47c7220e0405a473dce2d261370d4ef45a8cca93475212250ddf4859206884638901974124e8f23aff5ea78b44e5f4a8e6e078647f5279efe0ab1544
7
- data.tar.gz: 37082ea5af7b48056ea1d19cd2613d6f8d4696cce9dcb547ebc14c3ee30ef0af28d88a1d086d4857d07ef3431be03217b3ff0c9a0fa39511211890773525474c
6
+ metadata.gz: 9df504654282e9a307d343c8ef5ef71ee14e514a787e983e1b432218eb4dcf7f2e0902db9cef9a227ce95e762e543739a2f37f903a44f5283df46d61fc6c18f1
7
+ data.tar.gz: 8330930c0c63240f1ab4ba6bc1be6cf021b279477d4aca660f638f16736737eb08c7f951867fc7a26640066705f62dc15348e719aa6e7aee80d80b4f57d8daa5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,35 @@
1
+ ## [1.9.313.0] - 2026-10-07
2
+
3
+ ### Fixed — engine sync (libleptris 1.9.313)
4
+
5
+ - **Absorption-aware release at the C ABI (#1557, engine #1566)**:
6
+ the follow-up to the #386 mitigation — `leptris_document_free`
7
+ now consults the absorption state itself. The anchor dying while
8
+ `absorbed_handle > 0` detaches the source and defers its release
9
+ to the outstanding holder's free (`absorbed_deferred`); handles
10
+ released before the anchor keep the #1548 handle-only no-op. C
11
+ consumers no longer need to mirror the absorbed flag at their own
12
+ release sites; the binding's 1.9.311.2 mitigation stays as
13
+ defense-in-depth. Both release orderings spec-pinned.
14
+
15
+ ## [1.9.312.1] - 2026-10-07
16
+
17
+ ### Added
18
+
19
+ - **Document#build / Element#build — the builder face (#374 lever
20
+ 2)**: markup-accumulating construction. The block turns into
21
+ well-formed markup in Ruby (method_missing element dispatch
22
+ with attrs/text, `#text`/`#cdata`/`#comment`/`#raw`, one-pass
23
+ escaping of `& < > "`), and the flush performs ONE native
24
+ crossing for the whole subtree — parse + `root=` adoption for
25
+ document sinks (exactly one root, the parser enforces it;
26
+ returns the installed root wrapper), `append_markup` for
27
+ element sinks (returns the added NodeSet). Measured DSL-vs-DSL
28
+ under load (interleaved CPU-time medians, build(500)):
29
+ leptris builder 2.68 ms vs nokogiri's builder 5.29 ms —
30
+ **~2.0x** — and ~1.2x over our own object face (3.19 ms,
31
+ itself ~1.7x nokogiri after the 1.9.311.3 trim).
32
+
1
33
  ## [1.9.312.0] - 2026-10-07
2
34
 
3
35
  ### Added — engine sync (libleptris 1.9.312)
data/Rakefile CHANGED
@@ -8,7 +8,7 @@ RSpec::Core::RakeTask.new(:spec)
8
8
  # Pin for `rake compile` and the platform-gem builds. Keep in lockstep
9
9
  # with .github/workflows/build.yml (which calls `rake compile`) and the
10
10
  # CHANGELOG when libleptris releases.
11
- LIBLEPTRIS_VERSION = "1.9.312"
11
+ LIBLEPTRIS_VERSION = "1.9.313"
12
12
  # Vendored alongside libleptris for fn:normalize-unicode (TODO
13
13
  # .restructure/20): built per platform with a RELOCATABLE @rpath
14
14
  # install name, loaded by ffi.rb before libleptris so the
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.312.0"
4
+ VERSION = "1.9.313.0"
5
5
  end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Markup-accumulating subtree construction (#374 lever 2): the
4
+ # builder turns a Ruby block into well-formed markup in a plain
5
+ # String (Ruby-side cost only), and the flush performs ONE native
6
+ # crossing — parse + attach — for the whole subtree. The
7
+ # per-element FFI crossing that dominated the object-face build
8
+ # profile collapses to one per #build call.
9
+ #
10
+ # doc.build do |b|
11
+ # b.catalog(id: "c") do
12
+ # b.item(id: 7) { b.name "Item 7" }
13
+ # end
14
+ # end
15
+ #
16
+ # Element names arriving through method_missing are identifier-
17
+ # shaped by construction (Ruby method names cannot carry markup
18
+ # metacharacters); the explicit #element path checks NAME_RE.
19
+ # Text and attribute values escape in one pass per string.
20
+ class Leptris::XML::Builder
21
+ NAME_RE = /\A[a-zA-Z_][\w.-]*\z/
22
+
23
+ ESCAPE = {
24
+ "&" => "&amp;",
25
+ "<" => "&lt;",
26
+ ">" => "&gt;",
27
+ '"' => "&quot;",
28
+ }.freeze
29
+ ESCAPE_RE = /[&<>"]/
30
+ private_constant :ESCAPE, :ESCAPE_RE
31
+
32
+ # +sink+ receives the flushed markup (a Document or Element).
33
+ def initialize(sink)
34
+ @sink = sink
35
+ @buf = +""
36
+ @depth = 0
37
+ end
38
+
39
+ def element(name, attributes = nil)
40
+ name = name.to_s
41
+ unless name =~ NAME_RE
42
+ raise ArgumentError,
43
+ "element name must match #{NAME_RE.inspect}, got #{name.inspect}"
44
+ end
45
+ @buf << "<" << name
46
+ write_attributes(attributes) if attributes
47
+ if block_given?
48
+ @buf << ">"
49
+ @depth += 1
50
+ begin
51
+ yield self
52
+ ensure
53
+ @depth -= 1
54
+ end
55
+ @buf << "</" << name << ">"
56
+ else
57
+ @buf << "/>"
58
+ end
59
+ self
60
+ end
61
+
62
+ def text(content)
63
+ @buf << escape_text(content.to_s)
64
+ self
65
+ end
66
+
67
+ def cdata(content)
68
+ @buf << "<![CDATA[" << content.to_s << "]]>"
69
+ self
70
+ end
71
+
72
+ def comment(content)
73
+ @buf << "<!--" << escape_text(content.to_s) << "-->"
74
+ self
75
+ end
76
+
77
+ # Verbatim markup — the escape hatch for pre-serialized
78
+ # fragments; the caller owns well-formedness.
79
+ def raw(markup)
80
+ @buf << markup.to_s
81
+ self
82
+ end
83
+
84
+ def method_missing(name, *args, &block)
85
+ # Method names are identifier-shaped: they cannot carry <, >,
86
+ # &, quotes, or whitespace, so they need no NAME_RE round-trip
87
+ # (trailing = / ? / ! are legal Ruby but never legal XML
88
+ # name tails here — reject them explicitly).
89
+ return super if name.to_s.end_with?("=", "?", "!")
90
+ case args.length
91
+ when 0
92
+ element(name.to_s, &block)
93
+ when 1
94
+ arg = args[0]
95
+ if arg.is_a?(Hash)
96
+ element(name.to_s, arg, &block)
97
+ else
98
+ element(name.to_s) { |b| b.text(arg); block&.call(b) }
99
+ end
100
+ when 2
101
+ attrs, txt = args
102
+ return super unless attrs.is_a?(Hash)
103
+ element(name.to_s, attrs) { |b| b.text(txt); block&.call(b) }
104
+ else
105
+ super
106
+ end
107
+ end
108
+
109
+ def respond_to_missing?(name, include_private = false)
110
+ !name.to_s.end_with?("=", "?", "!") || super
111
+ end
112
+
113
+ # One native crossing for the accumulated subtree: the flush
114
+ # parses + attaches. Document sinks take exactly one root
115
+ # (set_root_ex adopts the parsed root); element sinks append
116
+ # every fragment child.
117
+ def flush
118
+ unless @depth.zero?
119
+ raise Leptris::XML::Error,
120
+ "unbalanced builder — unclosed elements remain (depth #{@depth})"
121
+ end
122
+ markup = @buf
123
+ return @sink if markup.empty?
124
+ case @sink
125
+ when Leptris::XML::Document
126
+ parsed = Leptris::XML::Document.parse(markup)
127
+ root = parsed.root
128
+ if root.nil?
129
+ raise Leptris::XML::Error,
130
+ "builder markup produced no root element"
131
+ end
132
+ @sink.root = root
133
+ @buf = +""
134
+ # The INSTALLED root — the same wrapper #root now yields
135
+ # (root= returns the parsed source, not the adoption copy).
136
+ @sink.root
137
+ when Leptris::XML::Element
138
+ added = @sink.add_child(markup)
139
+ @buf = +""
140
+ added
141
+ else
142
+ raise ArgumentError,
143
+ "builder sink must be a Document or Element, got #{@sink.class}"
144
+ end
145
+ end
146
+
147
+ private
148
+
149
+ def write_attributes(attributes)
150
+ case attributes
151
+ when Hash
152
+ attributes.each do |k, v|
153
+ @buf << " " << k.to_s << '="' << escape_attr(v) << '"'
154
+ end
155
+ when nil then # nothing
156
+ else
157
+ raise ArgumentError,
158
+ "attributes must be a Hash, got #{attributes.class}"
159
+ end
160
+ end
161
+
162
+ def escape_text(value)
163
+ value.gsub(ESCAPE_RE, ESCAPE)
164
+ end
165
+
166
+ def escape_attr(value)
167
+ value.to_s.gsub(ESCAPE_RE, ESCAPE)
168
+ end
169
+ end
@@ -232,6 +232,20 @@ class Leptris::XML::Document
232
232
  wrap(raw)
233
233
  end
234
234
 
235
+ # Markup-accumulating construction (#374 lever 2): the block
236
+ # accumulates well-formed markup in Ruby; the flush performs
237
+ # ONE native crossing for the whole subtree. Exactly one root
238
+ # element — the parsed root is adopted with set_root_ex.
239
+ # Returns the flush's value (the installed root element).
240
+ #
241
+ # doc = Leptris::XML::Document.create
242
+ # doc.build { |b| b.catalog(id: "c") { b.item { b.name "x" } } }
243
+ def build
244
+ builder = Leptris::XML::Builder.new(self)
245
+ yield builder
246
+ builder.flush
247
+ end
248
+
235
249
  # Deep copy of +node+ in a NEW document — the one authority
236
250
  # behind Node#dup / Element#dup and the indent-unit path.
237
251
  # Elements deep-copy through the C copier (every child kind and
@@ -665,6 +665,17 @@ class Leptris::XML::Element < Leptris::XML::Node
665
665
  Leptris::XML::Node.wrap_fresh(ptr, @document, Leptris::XML::FFI::NODE_ELEMENT)
666
666
  end
667
667
 
668
+ # Markup-accumulating child construction (#374 lever 2): the
669
+ # block accumulates well-formed child markup in Ruby; the flush
670
+ # appends every fragment child in ONE native crossing
671
+ # (append_markup). Returns the flush's value (the NodeSet of
672
+ # added children).
673
+ def build
674
+ builder = Leptris::XML::Builder.new(self)
675
+ yield builder
676
+ builder.flush
677
+ end
678
+
668
679
  def add_child(node_or_markup)
669
680
  case node_or_markup
670
681
  when Leptris::XML::Node
data/lib/leptris/xml.rb CHANGED
@@ -14,6 +14,7 @@ module Leptris
14
14
  autoload :Namespace, "leptris/xml/namespace"
15
15
  autoload :Document, "leptris/xml/document"
16
16
  autoload :DocumentFragment, "leptris/xml/document_fragment"
17
+ autoload :Builder, "leptris/xml/builder"
17
18
  autoload :DocType, "leptris/xml/doc_type"
18
19
  autoload :NodeSet, "leptris/xml/node_set"
19
20
  autoload :Searchable, "leptris/xml/searchable"
data/lib/libleptris.so CHANGED
Binary file
data/vendor-src/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Sources vendored in this gem (recompile rights; the ruby
2
2
  variant builds them at install via extconf.rb):
3
3
 
4
- - libleptris v1.9.312
4
+ - libleptris v1.9.313
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,27 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.313] - 2026-10-07
4
+
5
+ ### Fixed
6
+
7
+ - **Absorbed sources defer their release to outstanding handles
8
+ (#1557, PR #1566).** Follow-up to #1548's
9
+ `leptris_document_absorb`: the absorber's free cleared the
10
+ absorbed state before recursing, so a source handle that
11
+ OUTLIVED the absorber read the cleared flag and re-released the
12
+ already-destroyed pool — heap corruption (glibc abort /
13
+ `malloc_zone_error`), deterministic under exit-time finalizers
14
+ (leptris-ruby#386, 50-iteration repro). When the anchor dies
15
+ while `absorbed_handle > 0` it now detaches the source and marks
16
+ `absorbed_deferred`: the holder's `leptris_document_free`
17
+ performs the FULL release; handles released before the anchor
18
+ keep the #1548 handle-only no-op. Consumers no longer need to
19
+ mirror the absorbed flag at their own release sites. Gates: the
20
+ repro red under ASAN at leptris.c:937, green after; mutation
21
+ suite 27/27 ASAN + Release; full ctest 1866/1866; leaks 0.
22
+
23
+
24
+
3
25
  ## [1.9.312] - 2026-10-06
4
26
 
5
27
  ### Added
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.312
6
+ VERSION 1.9.313
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -933,9 +933,11 @@ LEPTRIS_API void leptris_document_free(struct leptris_document* doc) {
933
933
 
934
934
  /* #1548: an absorbed document's MEMORY belongs to its absorber
935
935
  * (it rides the absorber's child_docs chain). A caller free
936
- * releases only the outstanding handle. */
937
- if (doc->absorbed_by) {
938
- doc->absorbed_handle = 0;
936
+ * releases only the outstanding handle. #1557: unless the
937
+ * anchor died first and DEFERRED the release to this holder —
938
+ * then this free is the real one. */
939
+ if (doc->absorbed_by && !doc->absorbed_deferred) {
940
+ if (doc->absorbed_handle > 0) doc->absorbed_handle--;
939
941
  return;
940
942
  }
941
943
 
@@ -1021,12 +1023,24 @@ LEPTRIS_API void leptris_document_free(struct leptris_document* doc) {
1021
1023
  * doesn't try to walk the
1022
1024
  * adopted subtree. */
1023
1025
  ch->root = NULL;
1024
- /* #1548: the anchor's free is the REAL one — clear the
1025
- * absorbed state so the early handle-return above does
1026
- * not swallow it. */
1027
- ch->absorbed_by = NULL;
1028
- ch->absorbed_handle = 0;
1029
- leptris_document_free(ch);
1026
+ if (ch->absorbed_by && ch->absorbed_handle > 0) {
1027
+ /* #1557: a caller still holds this absorbed handle.
1028
+ * Freeing here would strand it on freed memory (its
1029
+ * later free read the cleared flag and re-released
1030
+ * the pool — the leptris-ruby#386 double free).
1031
+ * DEFER: detach from our chain and hand the release
1032
+ * to the holder's leptris_document_free, which sees
1033
+ * absorbed_deferred and runs the full release. */
1034
+ ch->absorbed_deferred = 1;
1035
+ ch->next_adopted = NULL;
1036
+ } else {
1037
+ /* #1548: the anchor's free is the REAL one — clear
1038
+ * the absorbed state so the early handle-return
1039
+ * above does not swallow it. */
1040
+ ch->absorbed_by = NULL;
1041
+ ch->absorbed_handle = 0;
1042
+ leptris_document_free(ch);
1043
+ }
1030
1044
  ch = next;
1031
1045
  }
1032
1046
  doc->child_docs = NULL;
@@ -195,6 +195,11 @@ struct leptris_document {
195
195
  * (the anchor frees the memory later). */
196
196
  struct leptris_document* absorbed_by;
197
197
  int absorbed_handle;
198
+ /* #1557: the absorber died while this handle was outstanding —
199
+ * the anchor deferred the pool release to THIS holder's free,
200
+ * so leptris_document_free must run the full release (not the
201
+ * handle-only early return). */
202
+ int absorbed_deferred;
198
203
  /* XML Declaration support */
199
204
  char* xml_version; /* "1.0", "1.1", etc. or NULL if not present */
200
205
  int standalone; /* -1=not set, 0=no, 1=yes */
@@ -670,6 +670,32 @@ TEST(CrossDocumentLeafAdoption, RootLevelLeafArgumentAdoptsByKind) {
670
670
  }
671
671
  }
672
672
 
673
+ // #1557: an absorbed source's handle may OUTLIVE the absorber. The
674
+ // anchor's free used to clear the absorbed state, so the holder's
675
+ // later free re-released the same pool — heap corruption. The anchor
676
+ // now defers the release to the outstanding handle.
677
+ TEST(AbsorbAdoption, SourceHandleMayOutliveAnchor) {
678
+ for (int i = 0; i < 50; i++) {
679
+ LeptrisDocument dest = Parse("<main><keep/></main>");
680
+ ASSERT_NE(dest, nullptr);
681
+ LeptrisDocument src = Parse("<scratch><a/></scratch>");
682
+ ASSERT_NE(src, nullptr);
683
+ ASSERT_EQ(leptris_document_absorb(dest, src), LEPTRIS_OK);
684
+ leptris_document_free(dest); /* anchor dies first */
685
+ leptris_document_free(src); /* holder releases after */
686
+ }
687
+ }
688
+
689
+ TEST(AbsorbAdoption, SourceHandleFreedBeforeAnchorStillNoOp) {
690
+ LeptrisDocument dest = Parse("<main><keep/></main>");
691
+ ASSERT_NE(dest, nullptr);
692
+ LeptrisDocument src = Parse("<scratch><a/></scratch>");
693
+ ASSERT_NE(src, nullptr);
694
+ ASSERT_EQ(leptris_document_absorb(dest, src), LEPTRIS_OK);
695
+ leptris_document_free(src); /* handle-only: memory stays with anchor */
696
+ leptris_document_free(dest); /* releases everything incl. the source */
697
+ }
698
+
673
699
  TEST(AbsorbAdoption, SpliceAfterAbsorbIsZeroCopy) {
674
700
  LeptrisDocument dst = Parse("<main><keep/></main>");
675
701
  ASSERT_NE(dst, nullptr);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json",
3
3
  "name": "leptris",
4
- "version": "1.9.312",
4
+ "version": "1.9.313",
5
5
  "description": "High-performance XML parser and XPath 1.0 engine in C (libleptris)",
6
6
  "homepage": "https://github.com/leptris/leptris",
7
7
  "license": "MIT",
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.312.0
4
+ version: 1.9.313.0
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-06 00:00:00.000000000 Z
11
+ date: 2026-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -144,6 +144,7 @@ files:
144
144
  - lib/leptris/version.rb
145
145
  - lib/leptris/xml.rb
146
146
  - lib/leptris/xml/attr.rb
147
+ - lib/leptris/xml/builder.rb
147
148
  - lib/leptris/xml/c_string_array.rb
148
149
  - lib/leptris/xml/cdata.rb
149
150
  - lib/leptris/xml/comment.rb