leptris 1.9.1 → 1.9.2

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: ed2f1d5755923f389734df36cc9c40c513535e58e7abe5a40aa87dd93f858788
4
- data.tar.gz: eb4e9c9c3b12a7584572746053c9aaf318ee5481fa002909d8502d5565bcf543
3
+ metadata.gz: 6d6cc4a584231a903d60b34a8b3e438c729d804743f66ada5b9944dac2c2662e
4
+ data.tar.gz: 2cfcc126bf8e78363d6614e06ddfed0d72bd653eac0087f80781e237a7c1f85a
5
5
  SHA512:
6
- metadata.gz: 35a1a34584014746679c39098f60c6009871007e3e5016562b669437e6f00d84412c7a26f286034f628421f72846c810b1b3568faacbbae86fa3e6c47b6db20f
7
- data.tar.gz: 5e4785ee975db46c2f7798a4effd7c3d2be729e94ded98db78e3cdd830891b0704170e104d9b2287ff1e4e8a98833917a70a7ec4d5591615931020cf67f18e54
6
+ metadata.gz: 6fe8c57e921c84a0c31552c255151e1f0a756236282b7d8ac184a7f54da5dcac465747d33acc77fbd38d47506fcfc7d89961711c7ae3daf45aa08cbc9ef337fe
7
+ data.tar.gz: ea5561b2e483f0dc17c9942d60763565873f3d983b2f188e71153afd60e83909ce53a40a61d3c73e15be13592e0c00f2752aaad9cca8365e3e68d5b2bd5455a8
data/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ 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.2] - 2026-08-25
9
+
10
+ ### Changed
11
+
12
+ Architecture deepening — all C ABI and buffer knowledge now lives
13
+ behind the FFI seam (`lib/leptris/xml/ffi.rb`); no public API change.
14
+
15
+ - `PullEventStruct` (FFI::Struct) replaces the pull parser's
16
+ hand-rolled ABI offsets (`get_int(0)` / `get_pointer(8)` /
17
+ `get_pointer(16)`); struct-layout changes become a one-line
18
+ layout edit instead of silent offset drift.
19
+ - `FFI.with_ns_set(hash)` concentrates the namespace-binding
20
+ lifecycle (flatten → CStringArray wire format → build → yield →
21
+ free) that ad-hoc and compiled XPath each hand-wrote.
22
+ - `FFI.serialize_into_string` / `FFI.fetch_children` /
23
+ `FFI.fetch_result_nodes` / `FFI.parse_fragment_with_status` own
24
+ the size-query/allocate/fill/read buffer protocols; Serialization,
25
+ NodeSet and DocumentFragment now contain no `MemoryPointer` code.
26
+ - `Node#children` and `DocumentFragment#children` fetch all child
27
+ handles in one batched call (the libleptris 1.7.0 surface, now
28
+ attached) instead of the first_child + N next_sibling walk —
29
+ N+1 FFI round trips collapse to 2 dispatches plus N wraps.
30
+
8
31
  ## [1.9.1] - 2026-08-24
9
32
 
10
33
  ### Fixed
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.1"
4
+ VERSION = "1.9.2"
5
5
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ffi"
4
-
5
3
  # Wraps the synthetic "#document-fragment" element returned by
6
4
  # leptris_parse_fragment. Children of this fragment are the parsed nodes;
7
5
  # the fragment itself isn't part of any document tree but borrows its
@@ -15,23 +13,18 @@ class Leptris::XML::DocumentFragment
15
13
  end
16
14
 
17
15
  def self.parse(xml, document)
18
- status_ptr = ::FFI::MemoryPointer.new(:int)
19
- raw = Leptris::XML::FFI.leptris_parse_fragment(
20
- xml.to_s, xml.to_s.bytesize, document.c_ptr, status_ptr)
16
+ raw, status = Leptris::XML::FFI.parse_fragment_with_status(
17
+ xml.to_s, document.c_ptr)
21
18
  if raw.null?
22
19
  raise Leptris::XML::ParseError,
23
- "leptris_parse_fragment failed (status=#{status_ptr.read_int})"
20
+ "leptris_parse_fragment failed (status=#{status})"
24
21
  end
25
22
  new(document, raw)
26
23
  end
27
24
 
28
25
  def children
29
- count = Leptris::XML::FFI.leptris_element_child_count(@c_ptr)
30
- nodes = []
31
- ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
32
- until ptr.nil? || ptr.null?
33
- nodes << Leptris::XML::Node.wrap(ptr, @document)
34
- ptr = Leptris::XML::FFI.leptris_node_next_sibling(ptr)
26
+ nodes = Leptris::XML::FFI.fetch_children(@c_ptr).map do |ptr|
27
+ Leptris::XML::Node.wrap(ptr, @document)
35
28
  end
36
29
  Leptris::XML::NodeSet.new(@document, nodes)
37
30
  end
@@ -80,6 +80,17 @@ module Leptris
80
80
  :recover, :int
81
81
  end
82
82
 
83
+ # struct LeptrisPullEvent (leptris/sax.h): FFI::Struct derives
84
+ # the field offsets from the layout, so ABI changes are a
85
+ # one-line edit here instead of silent offset drift in callers.
86
+ class PullEventStruct < ::FFI::Struct
87
+ layout \
88
+ :type, :int,
89
+ :name, :pointer,
90
+ :text, :pointer,
91
+ :text_len, :size_t
92
+ end
93
+
83
94
  attach_function :leptris_version, [], :string
84
95
  attach_function :leptris_version_components, [:pointer, :pointer, :pointer], :void
85
96
 
@@ -158,6 +169,10 @@ module Leptris
158
169
  [:leptris_node_ref], :leptris_node_ref
159
170
  attach_function :leptris_node_child_count,
160
171
  [:leptris_node_ref], :size_t
172
+ # libleptris 1.7.0: all-kind child batch — out_nodes=NULL is a
173
+ # count-only query returning the TOTAL across every child kind.
174
+ attach_function :leptris_node_children,
175
+ [:leptris_node_ref, :pointer, :size_t], :size_t
161
176
  attach_function :leptris_node_as_element,
162
177
  [:leptris_node_ref], :leptris_element
163
178
  attach_function :leptris_element_as_node,
@@ -623,6 +638,90 @@ module Leptris
623
638
  detail = leptris_last_error.to_s
624
639
  detail.empty? ? base : "#{base} (#{detail})"
625
640
  end
641
+
642
+ # Namespace-binding lifecycle, written once: flatten the
643
+ # prefix/URI hash into the alternating CStringArray wire
644
+ # format, build the caller-owned set, yield it, free it under
645
+ # all outcomes. Every eval variant uses this; none of them
646
+ # knows how a set is born or dies.
647
+ def self.with_ns_set(hash)
648
+ flat = hash.flat_map { |prefix, uri| [prefix.to_s, uri.to_s] }
649
+ buffer, _anchors = Leptris::XML::CStringArray.to_c(flat)
650
+ set = leptris_xpath_ns_set_new_from_pairs(buffer, flat.length / 2)
651
+ if set.null?
652
+ raise Leptris::XML::Error,
653
+ "leptris_xpath_ns_set_new_from_pairs failed"
654
+ end
655
+ begin
656
+ yield set
657
+ ensure
658
+ leptris_xpath_ns_set_free(set)
659
+ end
660
+ end
661
+
662
+ # Caller-buffer serialization cycle: size query (buf=NULL),
663
+ # allocate exactly, fill, read. Since libleptris 1.9.0 the
664
+ # pair reuses one serialization through the per-document
665
+ # cache. +ffi_function+ is leptris_document_serialize_into or
666
+ # leptris_element_serialize_into. The buffer holds the
667
+ # serialization + NUL and XML output is NUL-free, so the
668
+ # bounded read is exact.
669
+ def self.serialize_into_string(ffi_function, c_ptr, options)
670
+ need = ffi_function.call(c_ptr, nil, 0, nil, options)
671
+ return "" if need.zero?
672
+ buffer = ::FFI::MemoryPointer.new(:char, need)
673
+ begin
674
+ ffi_function.call(c_ptr, buffer, need, nil, options)
675
+ buffer.read_string
676
+ ensure
677
+ buffer.free
678
+ end
679
+ end
680
+
681
+ # All-kind child handle batch (libleptris 1.7.0): count-only
682
+ # query sizes the buffer, one fetch copies every child kind,
683
+ # one bulk read materializes the pointers.
684
+ def self.fetch_children(c_ptr)
685
+ count = leptris_node_children(c_ptr, nil, 0)
686
+ return [] if count.zero?
687
+ with_pointer_buffer(count) do |buffer|
688
+ copied = leptris_node_children(c_ptr, buffer, count)
689
+ buffer.get_array_of_pointer(0, copied)
690
+ end
691
+ end
692
+
693
+ # XPath result-set batch (leptris_xpath_result_get_nodes_ex):
694
+ # copies all node kinds into a caller buffer and hands back
695
+ # the pointer array.
696
+ def self.fetch_result_nodes(result_ptr, count)
697
+ return [] if count.zero?
698
+ with_pointer_buffer(count) do |buffer|
699
+ copied = leptris_xpath_result_get_nodes_ex(
700
+ result_ptr, buffer, nil, count)
701
+ buffer.get_array_of_pointer(0, copied)
702
+ end
703
+ end
704
+
705
+ # Fragment parse with the status out-param read: returns
706
+ # [document-or-fragment pointer, status].
707
+ def self.parse_fragment_with_status(xml, document_ptr)
708
+ status = ::FFI::MemoryPointer.new(:int)
709
+ raw = leptris_parse_fragment(
710
+ xml, xml.bytesize, document_ptr, status)
711
+ [raw, status.read_int]
712
+ end
713
+
714
+ # The one buffer-allocation point for handle-array fetches:
715
+ # everything MemoryPointer-related above this line is either
716
+ # an attached C call or another seam helper.
717
+ def self.with_pointer_buffer(count)
718
+ buffer = ::FFI::MemoryPointer.new(:pointer, count)
719
+ begin
720
+ yield buffer
721
+ ensure
722
+ buffer.free
723
+ end
724
+ end
626
725
  end
627
726
  end
628
727
  end
@@ -107,14 +107,12 @@ class Leptris::XML::Node
107
107
  end
108
108
 
109
109
  def children
110
- # Immutable in readonly mode: the walk (first_child + one
111
- # next_sibling per child) plus wrapper construction is paid once.
110
+ # Immutable in readonly mode: the batch fetch plus wrapper
111
+ # construction is paid once.
112
112
  return @children if readonly_cached?(:@children)
113
- nodes = []
114
- ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
115
- until ptr.nil? || ptr.null?
116
- nodes << Leptris::XML::Node.wrap(ptr, @document, parent: as_element_or_self)
117
- ptr = Leptris::XML::FFI.leptris_node_next_sibling(ptr)
113
+ parent = as_element_or_self
114
+ nodes = Leptris::XML::FFI.fetch_children(@c_ptr).map do |ptr|
115
+ Leptris::XML::Node.wrap(ptr, @document, parent: parent)
118
116
  end
119
117
  result = Leptris::XML::NodeSet.new(@document, nodes)
120
118
  @children = result if @document&.readonly?
@@ -63,30 +63,24 @@ class Leptris::XML::NodeSet
63
63
  def each
64
64
  return enum_for(:each) unless block_given?
65
65
  if @result_ptr
66
- # Batch-fetch all node pointers in one FFI call
67
- # (leptris_xpath_result_get_nodes_ex copies every node kind, not
68
- # just elements) and wrap each. Wrappers are cached per-Document
69
- # via Node.wrap, so re-iteration of the same NodeSet hits it.
66
+ # Batch-fetch all node pointers through the FFI seam
67
+ # (leptris_xpath_result_get_nodes_ex copies every node kind,
68
+ # not just elements) and wrap each. Wrappers are cached
69
+ # per-Document via Node.wrap, so re-iteration of the same
70
+ # NodeSet hits it.
70
71
  n = length
71
72
  if n > 0
72
- buf = ::FFI::MemoryPointer.new(:pointer, n)
73
- begin
74
- copied = Leptris::XML::FFI.leptris_xpath_result_get_nodes_ex(
75
- @result_ptr, buf, nil, n)
76
- copied.times do |i|
77
- ptr = buf.get_pointer(i * ::FFI.type_size(:pointer))
78
- next if ptr.null?
79
- yield Leptris::XML::Node.wrap(ptr, @document)
80
- end
81
- # The batch accessor under-copies mixed-kind results (upstream
82
- # leptris#477); fall back to per-index fetch for the remainder.
83
- (copied...n).each do |i|
84
- ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, i)
85
- next if ptr.null?
86
- yield Leptris::XML::Node.wrap(ptr, @document)
87
- end
88
- ensure
89
- buf.free
73
+ pointers = Leptris::XML::FFI.fetch_result_nodes(@result_ptr, n)
74
+ pointers.each do |ptr|
75
+ next if ptr.null?
76
+ yield Leptris::XML::Node.wrap(ptr, @document)
77
+ end
78
+ # The batch accessor under-copies mixed-kind results (upstream
79
+ # leptris#477); fall back to per-index fetch for the remainder.
80
+ (pointers.length...n).each do |i|
81
+ ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, i)
82
+ next if ptr.null?
83
+ yield Leptris::XML::Node.wrap(ptr, @document)
90
84
  end
91
85
  end
92
86
  else
@@ -67,22 +67,17 @@ module Leptris::XML::Pull
67
67
  def next_event
68
68
  raw = Leptris::XML::FFI.leptris_pull_next(@handle)
69
69
  return nil if raw.null?
70
- type_code = raw.get_int(0)
71
- type = TYPES[type_code]
72
- name = read_string_at(raw, 8)
73
- text_ptr = raw.get_pointer(16)
74
- text = text_ptr.null? ? nil : text_ptr.read_string
70
+ event = Leptris::XML::FFI::PullEventStruct.new(raw)
71
+ type = TYPES[event[:type]]
72
+ name = read_owned(event[:name])
73
+ text = read_owned(event[:text])
75
74
  attrs = type == :start_element ? capture_attrs : nil
76
75
  Event.new(type: type, name: name, text: text, attrs: attrs)
77
76
  end
78
77
 
79
78
  private
80
79
 
81
- # struct LeptrisPullEvent { int type; const char* name; const
82
- # char* text; size_t text_len; } — pointer-sized fields follow the
83
- # 4-byte enum on 64-bit ABIs (name at 8, text at 16).
84
- def read_string_at(raw, offset)
85
- ptr = raw.get_pointer(offset)
80
+ def read_owned(ptr)
86
81
  ptr.null? ? nil : ptr.read_string
87
82
  end
88
83
 
@@ -55,20 +55,11 @@ module Leptris::XML::Searchable
55
55
 
56
56
  protected
57
57
 
58
- # Builds a caller-owned namespace binding set in one FFI call
59
- # (ns_set_new_from_pairs takes a flat alternating prefix/URI array,
60
- # the same wire format CStringArray owns), evaluates via
61
- # leptris_xpath_eval_ns, and frees the set regardless of outcome.
58
+ # Evaluates against a caller-owned namespace binding set; the
59
+ # build/teardown lifecycle lives at the FFI seam (FFI.with_ns_set).
62
60
  def xpath_eval_with_namespaces(doc_ptr, context_ptr, expr, ns)
63
- flat = ns.flat_map { |prefix, uri| [prefix.to_s, uri.to_s] }
64
- buffer, _anchors = Leptris::XML::CStringArray.to_c(flat)
65
- set = Leptris::XML::FFI.leptris_xpath_ns_set_new_from_pairs(
66
- buffer, flat.length / 2)
67
- raise Leptris::XML::Error, "leptris_xpath_ns_set_new_from_pairs failed" if set.null?
68
- begin
61
+ Leptris::XML::FFI.with_ns_set(ns) do |set|
69
62
  Leptris::XML::FFI.leptris_xpath_eval_ns(doc_ptr, context_ptr, expr, set)
70
- ensure
71
- Leptris::XML::FFI.leptris_xpath_ns_set_free(set)
72
63
  end
73
64
  end
74
65
 
@@ -18,12 +18,9 @@ module Leptris::XML::Serialization
18
18
  end
19
19
  private_constant :DEFAULT_OPTIONS
20
20
 
21
- # +ffi_function+ is a bound _serialize_into function taking
22
- # (c_ptr, buf, capacity, out_len, opts): size query with buf=NULL,
23
- # then one fill into a caller buffer. Since libleptris 1.9.0 the
24
- # pair reuses a single serialization through a per-document cache
25
- # (leptris#541) — no C-side result-string allocation, and repeated
26
- # serializations of an unmutated document skip the serialize pass.
21
+ # +ffi_function+ is a bound _serialize_into function. The buffer
22
+ # cycle lives at the FFI seam (FFI.serialize_into_string); this
23
+ # module owns only options selection and construction.
27
24
  def self.to_xml(ffi_function, c_ptr, indent: 0, no_decl: false, encoding: nil)
28
25
  opts =
29
26
  if indent.to_i.zero? && !no_decl && encoding.nil?
@@ -33,18 +30,7 @@ module Leptris::XML::Serialization
33
30
  indent: indent, no_decl: no_decl, encoding: encoding)
34
31
  opts
35
32
  end
36
- need = ffi_function.call(c_ptr, nil, 0, nil, opts.pointer)
37
- return "" if need.zero?
38
- buf = ::FFI::MemoryPointer.new(:char, need)
39
- begin
40
- ffi_function.call(c_ptr, buf, need, nil, opts.pointer)
41
- # The buffer holds exactly the serialization + NUL; XML output
42
- # is NUL-free, so a bounded read_string is exact. (Avoids the
43
- # out_len param — no portable size_t read exists on Pointer.)
44
- buf.read_string
45
- ensure
46
- buf.free
47
- end
33
+ Leptris::XML::FFI.serialize_into_string(ffi_function, c_ptr, opts.pointer)
48
34
  end
49
35
 
50
36
  # +ffi_function+ is a bound FFI function taking
@@ -40,16 +40,9 @@ class Leptris::XML::XPath
40
40
  document = context.document
41
41
  result_ptr =
42
42
  if ns && !ns.empty?
43
- flat = ns.flat_map { |prefix, uri| [prefix.to_s, uri.to_s] }
44
- buffer, _anchors = Leptris::XML::CStringArray.to_c(flat)
45
- set = Leptris::XML::FFI.leptris_xpath_ns_set_new_from_pairs(
46
- buffer, flat.length / 2)
47
- raise Leptris::XML::Error, "leptris_xpath_ns_set_new_from_pairs failed" if set.null?
48
- begin
43
+ Leptris::XML::FFI.with_ns_set(ns) do |set|
49
44
  Leptris::XML::FFI.leptris_xpath_compiled_eval_ns(
50
45
  @handle, document.c_ptr, context_ptr(context), set)
51
- ensure
52
- Leptris::XML::FFI.leptris_xpath_ns_set_free(set)
53
46
  end
54
47
  else
55
48
  Leptris::XML::FFI.leptris_xpath_compiled_eval(
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.1
4
+ version: 1.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-24 00:00:00.000000000 Z
11
+ date: 2026-08-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi