leptris 1.9.24-aarch64-linux → 1.9.26-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: 1be15b3c37fddd0b1f5c30c79b08adae540825a34605ffa2d44f8c9800dc1cd5
4
- data.tar.gz: e008a194237148e03af56e37032cd3e3e30c1762680d3013b5cb3053af4a7c21
3
+ metadata.gz: 2ff1e0252cab3e0bf0f626b2561c127325c2540332d1049e0eaec864f5065d29
4
+ data.tar.gz: 2bc04e7940829b977148af31a56e8044496c1138773858a25e69c0d216b81fee
5
5
  SHA512:
6
- metadata.gz: '04893f33c1d19df655b5ae1e5aeb15cd753988e1f7f7872f5984fa6a011863c59c860919186d7f688dbc200e3d2cc76c5266917ff0293f7ad125d8d1913daa6e'
7
- data.tar.gz: 4698e55f88198a6058302c6df78062c57771e809b49b99fbdee63e21fe7b348f0bf707650e9690b46e6c975b474d1daa51ba5782dd3074655c62be67f24df621
6
+ metadata.gz: 902dbd67e428e7efd945017cc2a72e6d80c79250b19eabaeeceeab711c7d00669dbaecf3696778bce1780fe1f613042201eb12042370a767b2705ebc392d06b0
7
+ data.tar.gz: 5eb92eb2921f786ad97dbebe5131e508d7ae16d205f073fed6010ea91cdbefd0db933457ed0d628f18b54e557ea8c8c2a67f1df4c9bf5c9c3f511700ec53b01d
data/CHANGELOG.md CHANGED
@@ -5,6 +5,72 @@ All notable changes to Leptris will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.9.26] - 2026-08-28
9
+
10
+ ### Added
11
+
12
+ - **libleptris 1.9.7 lockstep** (upstream #602/#604): Rakefile pin
13
+ bumped; symbol audit 247/247 with the five new functions.
14
+ - **`Pull::Parser#each_batch(max = 256)`** — the #589 batch
15
+ transport: ONE C call stages up to max events (scratch-staged,
16
+ layout-offset drain, no per-event dispatch). Streaming big.xml
17
+ (250k events): 408 ms -> **210 ms per parse (-48%)**; the cursor
18
+ path also drops to 347 ms (Events are now constructed
19
+ positionally — the keyword-init Struct cost ~220 ns/event; a
20
+ minor API change: `Pull::Event` no longer takes keyword args,
21
+ readers unchanged). Per the engine's attr-mirror protocol,
22
+ attributes are captured for each batch's LAST start_element —
23
+ use `#each` when every start's attrs must be present. Attr
24
+ capture on both paths is now one count query + one flat copy
25
+ (`leptris_pull_attrs`) instead of 2N per-index dispatches.
26
+ - **`Document#node` / `Document#children`** (#580, our upstream
27
+ ask): document-level PIs/comments are tree children behind a
28
+ stable singleton navigation head — `children` is
29
+ [prolog…, root, epilog…] in document order (the libxml2/Nokogiri
30
+ model), and `/comment()` / `//processing-instruction()` see the
31
+ document-level nodes. The dedicated memoized
32
+ `#processing_instructions` / `#comments` readers are unchanged.
33
+ - **`NodeSet#xpath` through the union entry** (`leptris_xpath_
34
+ eval_nodeset`, #589): one C call for N contexts — results
35
+ de-duplicated and document-ordered, which the per-member Ruby
36
+ loop it replaces could not guarantee.
37
+
38
+ ### Meta
39
+
40
+ - Engine swap 1.9.4 -> 1.9.7 regression battery (interleaved
41
+ best-of-3): cold walk 4,637 vs 4,652 µs, DOM parse 72 vs 73 µs,
42
+ SAX text-only 22.7 vs 22.6 ms — parity across the board. The
43
+ v1.9.6 XPath fixes (`/descendant::` root inclusion, $var head
44
+ dropping) are covered by the existing suite plus the new specs.
45
+
46
+ ## [1.9.25] - 2026-08-28
47
+
48
+ ### Fixed
49
+
50
+ - **Cold full-tree walk regression (1.9.1 → 1.9.24, ~1.4-1.5x)
51
+ recovered to parity**: the batch-fetch era (1.9.11-1.9.17) bought
52
+ its warm-path wins with per-call machinery the cold branch paid
53
+ on every children() — reproduced, bisected across every published
54
+ release, and profiled end to end this round. Raw cold walk of a
55
+ ~4,200-node catalog (interleaved best-of-5): 1.9.24 5,441 µs ->
56
+ **4,519 µs (-17%)**, vs 1.9.1's 4,369 µs = parity. Through moxml
57
+ the same shape improves 20.1 ms -> 16.5 ms (-18%).
58
+ - `FFI.fetch_children` (and the XPath result batch) now use
59
+ thread-local scratch buffers that grow to the largest family
60
+ seen and are reused — no MemoryPointer allocation/free per
61
+ call, and materially less GC. The count query is paid only when
62
+ the buffer fills exactly (possible truncation): a family that
63
+ fits costs ONE dispatch, not two.
64
+ - `Node.wrap`'s miss path resolves the wrapper cache and pointer
65
+ address once instead of twice — a cold walk wraps every node
66
+ exactly once, so the cache is 100% misses and every redundant
67
+ resolution was pure cost. Identity semantics unchanged.
68
+ - Kept: the per-field version-stamped memo stores on writable
69
+ documents (ADR-0003's writable extension — the residual ~3% vs
70
+ 1.9.1 is that feature working) and the batch architecture
71
+ itself (never more FFI dispatches than the linked-list walk,
72
+ and 2 vs 701 for the 700-child root).
73
+
8
74
  ## [1.9.24] - 2026-08-27
9
75
 
10
76
  ### Changed
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.4"
11
+ LIBLEPTRIS_VERSION = "1.9.7"
12
12
 
13
13
  CMAKE_FLAGS = %w[
14
14
  -DCMAKE_BUILD_TYPE=Release
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.24"
4
+ VERSION = "1.9.26"
5
5
  end
@@ -151,6 +151,26 @@ class Leptris::XML::Document
151
151
  Leptris::XML::Node.wrap(ptr, self)
152
152
  end
153
153
 
154
+ # The document node — navigation head over the whole tree chain
155
+ # [prolog comments/PIs, root element, epilog comments/PIs] in
156
+ # document order (libleptris 1.9.7, upstream #580: the libxml2
157
+ # model; XPath /comment() and //processing-instruction() see the
158
+ # document-level nodes). A stable, document-owned singleton —
159
+ # Node.wrap's cache keeps the returned wrapper identical.
160
+ def node
161
+ raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
162
+ @node ||= Leptris::XML::Node.wrap(
163
+ Leptris::XML::FFI.leptris_document_node(@c_ptr), self)
164
+ end
165
+
166
+ # The document's children, via the document node: prolog
167
+ # comments/PIs, the root element, epilog comments/PIs, in
168
+ # document order (Nokogiri-parity shape).
169
+ def children
170
+ doc_node = node
171
+ doc_node ? doc_node.children : Leptris::XML::NodeSet.new(self, [])
172
+ end
173
+
154
174
  # Attach +element+ as the document's root element. The element must
155
175
  # have been created against this document and must not already have
156
176
  # a parent. Any previous root is left detached (still owned by the
@@ -143,6 +143,11 @@ module Leptris
143
143
  [:leptris_document, :leptris_element], :leptris_status
144
144
  attach_function :leptris_document_root,
145
145
  [:leptris_document], :leptris_element
146
+ # The document node — navigation head over the whole tree
147
+ # chain [prolog comments/PIs, root, epilog...] (libleptris
148
+ # 1.9.7, upstream #580; the libxml2 model).
149
+ attach_function :leptris_document_node,
150
+ [:leptris_document], :leptris_node_ref
146
151
  # Document-level PIs (v1.6.0): not tree nodes — enumerate via
147
152
  # these accessors only.
148
153
  attach_function :leptris_document_pi_count,
@@ -430,6 +435,14 @@ module Leptris
430
435
  [:leptris_document, :leptris_element, :string], :leptris_xpath_result
431
436
  attach_function :leptris_xpath_eval_with_vars,
432
437
  [:leptris_document, :string, :leptris_xpath_var_set], :leptris_xpath_result
438
+ # Union semantics (NodeSet#xpath): N contexts, ONE call — the
439
+ # engine merges the per-context results into a de-duplicated,
440
+ # document-ordered nodeset (libleptris 1.9.7, upstream #589).
441
+ attach_function :leptris_xpath_eval_nodeset,
442
+ [:leptris_document, :pointer, :size_t, :string], :leptris_xpath_result
443
+ attach_function :leptris_xpath_compiled_eval_nodeset,
444
+ [:leptris_document, :pointer, :size_t, :leptris_xpath_compiled],
445
+ :leptris_xpath_result
433
446
  attach_function :leptris_xpath_eval_with_vars_context,
434
447
  [:leptris_document, :leptris_element, :string, :leptris_xpath_var_set],
435
448
  :leptris_xpath_result
@@ -552,12 +565,16 @@ module Leptris
552
565
  [:string], :leptris_pull_parser
553
566
  attach_function :leptris_pull_next,
554
567
  [:leptris_pull_parser], :pointer
568
+ attach_function :leptris_pull_next_batch,
569
+ [:leptris_pull_parser, :pointer, :size_t], :size_t
555
570
  attach_function :leptris_pull_attr_count,
556
571
  [:leptris_pull_parser], :size_t
557
572
  attach_function :leptris_pull_attr_name,
558
573
  [:leptris_pull_parser, :size_t], :string
559
574
  attach_function :leptris_pull_attr_value,
560
575
  [:leptris_pull_parser, :size_t], :string
576
+ attach_function :leptris_pull_attrs,
577
+ [:leptris_pull_parser, :pointer, :size_t], :size_t
561
578
  attach_function :leptris_pull_free,
562
579
  [:leptris_pull_parser], :void
563
580
 
@@ -817,16 +834,33 @@ module Leptris
817
834
  end
818
835
  end
819
836
 
820
- # All-kind child handle batch (libleptris 1.7.0): count-only
821
- # query sizes the buffer, one fetch copies every child kind,
822
- # one bulk read materializes the pointers.
837
+ # All-kind child handle batch (libleptris 1.7.0). Opportunistic
838
+ # single dispatch: the C call copies up to the capacity handed
839
+ # it and returns what fit, so the count query is paid only when
840
+ # the buffer fills exactly (a possible truncation) — families
841
+ # that fit the scratch buffer cost ONE dispatch, not two.
842
+ FETCH_INITIAL = 32
843
+ private_constant :FETCH_INITIAL
844
+
823
845
  def self.fetch_children(c_ptr)
824
- count = leptris_node_children(c_ptr, nil, 0)
825
- return [] if count.zero?
826
- with_pointer_buffer(count) do |buffer|
827
- copied = leptris_node_children(c_ptr, buffer, count)
828
- buffer.get_array_of_pointer(0, copied)
846
+ scratch = (Thread.current[:leptris_scratch] ||= {})
847
+ buffer = scratch[:pointers]
848
+ if buffer.nil?
849
+ buffer = scratch[:pointers] =
850
+ ::FFI::MemoryPointer.new(:pointer, FETCH_INITIAL)
851
+ end
852
+ cap = buffer.size / PTR_BYTES
853
+ copied = leptris_node_children(c_ptr, buffer, cap)
854
+ if copied == cap
855
+ total = leptris_node_children(c_ptr, nil, 0)
856
+ if total > cap
857
+ buffer.free
858
+ buffer = scratch[:pointers] =
859
+ ::FFI::MemoryPointer.new(:pointer, total)
860
+ copied = leptris_node_children(c_ptr, buffer, total)
861
+ end
829
862
  end
863
+ buffer.get_array_of_pointer(0, copied)
830
864
  end
831
865
 
832
866
  # XPath result-set batch (leptris_xpath_result_get_nodes_ex):
@@ -840,18 +874,13 @@ module Leptris
840
874
 
841
875
  def self.fetch_result_nodes(result_ptr, count)
842
876
  return [[], nil] if count.zero?
843
- with_pointer_buffer(count) do |buffer|
844
- kinds = ::FFI::MemoryPointer.new(:int, count)
845
- begin
846
- copied = leptris_xpath_result_get_nodes_ex(
847
- result_ptr, buffer, kinds, count)
848
- hints = kinds.get_array_of_int(0, copied)
849
- .map { |k| XPATH_KIND_HINT[k] }
850
- [buffer.get_array_of_pointer(0, copied), hints]
851
- ensure
852
- kinds.free
853
- end
854
- end
877
+ buffer = scratch_pointers(count)
878
+ kinds = scratch_ints(count)
879
+ copied = leptris_xpath_result_get_nodes_ex(
880
+ result_ptr, buffer, kinds, count)
881
+ hints = kinds.get_array_of_int(0, copied)
882
+ .map { |k| XPATH_KIND_HINT[k] }
883
+ [buffer.get_array_of_pointer(0, copied), hints]
855
884
  end
856
885
 
857
886
  # Fragment parse with the status out-param read: returns
@@ -863,16 +892,52 @@ module Leptris
863
892
  [raw, status.read_int]
864
893
  end
865
894
 
866
- # The one buffer-allocation point for handle-array fetches:
867
- # everything MemoryPointer-related above this line is either
868
- # an attached C call or another seam helper.
869
- def self.with_pointer_buffer(count)
870
- buffer = ::FFI::MemoryPointer.new(:pointer, count)
871
- begin
872
- yield buffer
873
- ensure
874
- buffer.free
895
+ # Thread-local scratch buffers for batch handle fetches (round
896
+ # XX): the per-call MemoryPointer new/free was pure cold-path
897
+ # overhead — two allocations and a GC visit per children() /
898
+ # result-set batch, ~15% of a cold full-tree walk between
899
+ # 1.9.1 and 1.9.24. The buffers grow to the largest request
900
+ # seen (bounded by the widest element or result set) and are
901
+ # reused. Safe: no Ruby reentrancy during the C call, and
902
+ # per-thread storage keeps concurrent fetches independent.
903
+ # Never freed — a few KB pinned per thread.
904
+ PTR_BYTES = ::FFI.type_size(:pointer)
905
+ private_constant :PTR_BYTES
906
+ INT_BYTES = ::FFI.type_size(:int)
907
+ private_constant :INT_BYTES
908
+
909
+ def self.scratch_pointers(count)
910
+ scratch = (Thread.current[:leptris_scratch] ||= {})
911
+ ptr = scratch[:pointers]
912
+ if ptr.nil? || ptr.size / PTR_BYTES < count
913
+ ptr&.free
914
+ ptr = scratch[:pointers] = ::FFI::MemoryPointer.new(:pointer, count)
915
+ end
916
+ ptr
917
+ end
918
+
919
+ def self.scratch_ints(count)
920
+ scratch = (Thread.current[:leptris_scratch] ||= {})
921
+ ptr = scratch[:ints]
922
+ if ptr.nil? || ptr.size / INT_BYTES < count
923
+ ptr&.free
924
+ ptr = scratch[:ints] = ::FFI::MemoryPointer.new(:int, count)
925
+ end
926
+ ptr
927
+ end
928
+
929
+ # Pull-event staging block for leptris_pull_next_batch (the
930
+ # #589 bulk transport): LeptrisPullEvent records, stride from
931
+ # the struct layout that mirrors the ABI.
932
+ def self.scratch_events(count)
933
+ need = count * PullEventStruct.size
934
+ scratch = (Thread.current[:leptris_scratch] ||= {})
935
+ ptr = scratch[:pull_events]
936
+ if ptr.nil? || ptr.size < need
937
+ ptr&.free
938
+ ptr = scratch[:pull_events] = ::FFI::MemoryPointer.new(need)
875
939
  end
940
+ ptr
876
941
  end
877
942
  end
878
943
  end
@@ -19,9 +19,15 @@ class Leptris::XML::Node
19
19
  # Per-document weak-ref cache. Returns the existing wrapper when the
20
20
  # same c_ptr is wrapped twice (common in children/sibling walks,
21
21
  # repeated xpath queries, traverse-then-access patterns). The cache
22
- # dies with the document so no stale entries.
23
- if document && (cached = document.wrapper_cache[c_ptr.address])
24
- return cached
22
+ # dies with the document so no stale entries. The miss path
23
+ # resolves the cache and address once — a cold walk wraps every
24
+ # node exactly once and pays both only on the store.
25
+ if document
26
+ cache = document.wrapper_cache
27
+ address = c_ptr.address
28
+ if (cached = cache[address])
29
+ return cached
30
+ end
25
31
  end
26
32
 
27
33
  node_type ||= Leptris::XML::FFI.leptris_node_get_type(c_ptr)
@@ -41,7 +47,7 @@ class Leptris::XML::Node
41
47
  new(c_ptr, document, parent: parent, node_type: node_type)
42
48
  end
43
49
 
44
- document.wrapper_cache[c_ptr.address] = node if document
50
+ cache[address] = node if document
45
51
  node
46
52
  end
47
53
 
@@ -127,24 +127,23 @@ class Leptris::XML::NodeSet
127
127
  end
128
128
  alias_method :text, :inner_text
129
129
 
130
- # Union semantics: evaluate the expression against each element
131
- # member (one C eval per member — the engine has no multi-context
132
- # eval yet; leptris/leptris ask pending) and accumulate into ONE
133
- # NodeSet, instead of merging NodeSets per member.
130
+ # Union semantics: ONE engine call evaluates the expression
131
+ # against every element member and merges the results into a
132
+ # de-duplicated, document-ordered nodeset (libleptris 1.9.7,
133
+ # upstream #589 — the per-member Ruby loop this replaces could
134
+ # neither dedup nor order).
134
135
  def xpath(*paths)
135
136
  handler, _ns, _vars = parse_search_args(paths)
136
137
  raise ArgumentError, "custom XPath handlers not supported" if handler
137
138
  expr = paths.join(" | ")
138
- accumulated = []
139
- each do |node|
140
- next unless node.is_a?(Leptris::XML::Element)
141
- result_ptr = Leptris::XML::FFI.leptris_xpath_eval(
142
- @document.c_ptr, node.c_ptr, expr)
143
- next if result_ptr.null?
144
- accumulated.concat(
145
- Leptris::XML::NodeSet.from_result(@document, result_ptr).to_a)
146
- end
147
- Leptris::XML::NodeSet.new(@document, accumulated)
139
+ contexts = to_a.select { |node| node.is_a?(Leptris::XML::Element) }
140
+ return Leptris::XML::NodeSet.new(@document, []) if contexts.empty?
141
+ buffer = Leptris::XML::FFI.scratch_pointers(contexts.size)
142
+ buffer.write_array_of_pointer(contexts.map(&:c_ptr))
143
+ result_ptr = Leptris::XML::FFI.leptris_xpath_eval_nodeset(
144
+ @document.c_ptr, buffer, contexts.size, expr)
145
+ return Leptris::XML::NodeSet.new(@document, []) if result_ptr.null?
146
+ Leptris::XML::NodeSet.from_result(@document, result_ptr)
148
147
  end
149
148
 
150
149
  def inspect
@@ -14,7 +14,10 @@ require "ffi"
14
14
  # end
15
15
  #
16
16
  module Leptris::XML::Pull
17
- Event = Struct.new(:type, :name, :text, :attrs, keyword_init: true)
17
+ # Constructed positionally by the parser: the keyword-init
18
+ # variant cost ~220ns/event in batch drains (round XXI). Readers
19
+ # (event.type, event.attrs, ...) are unchanged.
20
+ Event = Struct.new(:type, :name, :text, :attrs)
18
21
 
19
22
  TYPES = {
20
23
  Leptris::XML::FFI::PULL_START_ELEMENT => :start_element,
@@ -60,6 +63,49 @@ module Leptris::XML::Pull
60
63
  self
61
64
  end
62
65
 
66
+ # Bulk-delivers events (libleptris 1.9.7, upstream #589): ONE C
67
+ # call stages up to +max_count+ events — the per-event dispatch
68
+ # that made streaming ~145x slower than a DOM parse collapses to
69
+ # amortized noise. Per the engine's protocol, attributes are
70
+ # captured only for the LAST start_element of each batch
71
+ # (earlier starts in the same batch carry nil attrs — segment
72
+ # boundaries fall where they fall); use #each when every
73
+ # start's attrs must be captured.
74
+ def each_batch(max_count = 256)
75
+ return enum_for(:each_batch, max_count) unless block_given?
76
+ stride = Leptris::XML::FFI::PullEventStruct.size
77
+ type_off = 0
78
+ name_off = Leptris::XML::FFI::PullEventStruct.offset_of(:name)
79
+ text_off = Leptris::XML::FFI::PullEventStruct.offset_of(:text)
80
+ buffer = Leptris::XML::FFI.scratch_events(max_count)
81
+ while (n = Leptris::XML::FFI.leptris_pull_next_batch(
82
+ @handle, buffer, max_count)) > 0
83
+ events = Array.new(n) do |i|
84
+ base = i * stride
85
+ name_ptr = buffer.get_pointer(base + name_off)
86
+ text_ptr = buffer.get_pointer(base + text_off)
87
+ Event.new(
88
+ TYPES[buffer.get_int(base + type_off)],
89
+ name_ptr.null? ? nil :
90
+ name_ptr.read_string.force_encoding(Encoding::UTF_8),
91
+ text_ptr.null? ? nil :
92
+ text_ptr.read_string.force_encoding(Encoding::UTF_8),
93
+ nil
94
+ )
95
+ end
96
+ # The attr mirror holds the batch's most recent start.
97
+ last_start = nil
98
+ events.each_with_index do |event, i|
99
+ last_start = i if event.type == :start_element
100
+ end
101
+ events[last_start].attrs = capture_attrs if last_start
102
+ events.each { |event| yield event }
103
+ tail = events.last.type
104
+ break if tail == :end_document || tail == :error
105
+ end
106
+ self
107
+ end
108
+
63
109
  # Hot-loop offsets: derived from PullEventStruct's layout (the
64
110
  # struct stays the single source of truth for the ABI), read via
65
111
  # get_int/get_pointer so the per-event struct-wrapper allocation
@@ -80,23 +126,32 @@ module Leptris::XML::Pull
80
126
  text_ptr = raw.get_pointer(TEXT_OFFSET)
81
127
  attrs = type == :start_element ? capture_attrs : nil
82
128
  Event.new(
83
- type: type,
84
- name: name_ptr.null? ? nil : name_ptr.read_string.force_encoding(Encoding::UTF_8),
85
- text: text_ptr.null? ? nil : text_ptr.read_string.force_encoding(Encoding::UTF_8),
86
- attrs: attrs
129
+ type,
130
+ name_ptr.null? ? nil : name_ptr.read_string.force_encoding(Encoding::UTF_8),
131
+ text_ptr.null? ? nil : text_ptr.read_string.force_encoding(Encoding::UTF_8),
132
+ attrs
87
133
  )
88
134
  end
89
135
 
90
136
  private
91
137
 
138
+ # One count-only query plus one flat copy (leptris_pull_attrs,
139
+ # 1.9.7) — the char** wire format the SAX callbacks already use —
140
+ # replaces the 2N per-index dispatches.
92
141
  def capture_attrs
93
- count = Leptris::XML::FFI.leptris_pull_attr_count(@handle)
94
- return nil if count.zero?
142
+ total = Leptris::XML::FFI.leptris_pull_attrs(@handle, nil, 0)
143
+ return nil if total.zero?
144
+ buffer = Leptris::XML::FFI.scratch_pointers(2 * total)
145
+ copied = Leptris::XML::FFI.leptris_pull_attrs(@handle, buffer, total)
146
+ ptr_size = ::FFI.type_size(:pointer)
95
147
  hash = {}
96
148
  i = 0
97
- while i < count
98
- hash[Leptris::XML::FFI.leptris_pull_attr_name(@handle, i)] =
99
- Leptris::XML::FFI.leptris_pull_attr_value(@handle, i)
149
+ while i < copied
150
+ name = buffer.get_pointer(2 * i * ptr_size).read_string
151
+ .force_encoding(Encoding::UTF_8)
152
+ value = buffer.get_pointer((2 * i + 1) * ptr_size).read_string
153
+ .force_encoding(Encoding::UTF_8)
154
+ hash[name] = value
100
155
  i += 1
101
156
  end
102
157
  hash
data/lib/libleptris.so CHANGED
Binary file
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.24
4
+ version: 1.9.26
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-08-27 00:00:00.000000000 Z
11
+ date: 2026-08-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi