leptris 1.9.35 → 1.9.36

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: 81d0ccc67839cd649fce63e998598f641e731492085d5fd09f5d6f5be226c743
4
- data.tar.gz: f6914705968260382baa876b12f864d712544919a1186ef90cc0ba76857a8f86
3
+ metadata.gz: 31fdbaa742e583a50409dea1a5bda76e454e3b54ec30429d74101d5438f91b6f
4
+ data.tar.gz: 6acd554905283d755febf5c6265ce2a97dd8f9ce1ef83c9804363ff2d5bf1000
5
5
  SHA512:
6
- metadata.gz: 70235b9639261a967137bc78694b0cbe447f8165c95819e7d28db24c4f37775fb67aca769e05462231ecd8bdf14f542a42cdd3b73159546c36232aaaf14fc848
7
- data.tar.gz: 053d1e08b23283d82d83c7276417623358e4f29207eaf77fec9f71d319c4dc48ac5f6ad8d5ff038190d5a29b4e9956202abde97b2bb708a256837cbc5b67c21f
6
+ metadata.gz: 7e4c9abd84b07a678394774470b1486ccb2e03146578cda7eded0da2aa42f0b33f3ae682ac5a1881d2661e4a90ddb3bd6e0b3098d411303268867a2bbc1ad0d5
7
+ data.tar.gz: 35a2088c31974f7dc19beaa0bd4ad5c73515be100b1c0d3b3fd3bc313fa10e7abc28202f3eb7bb3cce6035ad86b0c3f553bde0ca458e08a03110ba0d774ff07a
data/CHANGELOG.md CHANGED
@@ -5,6 +5,46 @@ 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.36] - 2026-08-29
9
+
10
+ ### Fixed
11
+
12
+ - **SAX attribute corruption (leptris-ruby#95)** — fixed fully in
13
+ the binding. The engine's streaming attribute buffer loses
14
+ leading attribute pairs when several nested ancestor levels each
15
+ carry attributes; all THREE streaming surfaces are affected (the
16
+ callback transport, the recorder's arena, and pull attrs — each
17
+ verified on the issue fixture, which also yields run-varying
18
+ uninitialized bytes), while the DOM parser reads the same bytes
19
+ correctly. `SAX::Parser` now delivers from a DOM parse by
20
+ default (`SAX::DomDispatch`): one parse, then a lean walk
21
+ reproducing the streaming call shapes exactly —
22
+ attach-only-overridden kinds, one-arg arity dispatch, QName
23
+ element names, UTF-8, PI-data normalization. The issue fixture
24
+ is a regression spec (all 7 image attributes intact; the entire
25
+ existing SAX suite passes byte-identically through the new
26
+ path). The engine transports remain available via
27
+ `Parser.new(handler, streaming: true)` — and return as the
28
+ default the moment the engine fix lands. Correctness-first tax,
29
+ measured on big.xml: all-events 375 ms (engine 112, nokogiri
30
+ 130); the bisection (not positional, not attribute length;
31
+ trigger = nested attr-carrying ancestors; all three surfaces
32
+ share the buffer) is filed upstream for the C fix.
33
+
34
+ ### Added
35
+
36
+ - **libleptris 1.9.10 lockstep**: pin bumped; audit 250/250 with
37
+ `leptris_sax_recorder_reset` attached. `SAX::Recorder#reset` —
38
+ reuse one recorder across documents (upstream #594's
39
+ one-document-per-instance finding): fresh parser state, record/
40
+ arena buffers retained.
41
+
42
+ ### Meta
43
+
44
+ - Memory battery (first measurement, 10 x big.xml held): leptris
45
+ **17.5 MB/doc vs nokogiri 31.3** (1.8x lighter); both free
46
+ cleanly.
47
+
8
48
  ## [1.9.35] - 2026-08-28
9
49
 
10
50
  ### Added
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.9"
11
+ LIBLEPTRIS_VERSION = "1.9.10"
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.35"
4
+ VERSION = "1.9.36"
5
5
  end
@@ -561,6 +561,11 @@ module Leptris
561
561
  [:leptris_sax_recorder, :pointer], :pointer
562
562
  attach_function :leptris_sax_recorder_arena,
563
563
  [:leptris_sax_recorder, :pointer], :pointer
564
+ # Reuse across documents (1.9.10, #594): swaps in fresh parser
565
+ # state, RETAINS the record/arena buffers — a
566
+ # one-document-per-parse loop stops paying new/free churn.
567
+ attach_function :leptris_sax_recorder_reset,
568
+ [:leptris_sax_recorder], :int
564
569
  attach_function :leptris_sax_recorder_free,
565
570
  [:leptris_sax_recorder], :void
566
571
  attach_function :leptris_sax_parser_free,
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ # DOM-backed SAX dispatch (leptris-ruby#95): the engine's streaming
4
+ # attribute buffer corrupts leading attribute pairs when several
5
+ # nested ancestor levels each carry attributes — the callback
6
+ # transport, the recorder's packed arena, AND the pull parser all
7
+ # deliver the loss on the issue fixture (verified), while the DOM
8
+ # parser reads the same bytes correctly. Until the engine fix
9
+ # lands, SAX::Parser delivers from a DOM parse: same call shapes as
10
+ # the streaming transports — attach-only-overridden kinds,
11
+ # one-argument start_element arity, QName element names ("p:a"),
12
+ # UTF-8 strings, PI-data normalization.
13
+ #
14
+ # The walk is lean by construction: elements wrap (identity, name
15
+ # and prefix memos); every other child kind is read straight from
16
+ # its pointer — content getters per node type, never a wrapper.
17
+ module Leptris::XML::SAX::DomDispatch
18
+ module_function
19
+
20
+ def parse(handler, dispatched, string)
21
+ doc = Leptris::XML::Document.parse(string)
22
+ begin
23
+ dispatch(handler, dispatched, doc)
24
+ ensure
25
+ doc.free
26
+ end
27
+ end
28
+
29
+ def dispatch(handler, dispatched, doc)
30
+ handler.start_document if dispatched[:start_document]
31
+ doc_node = doc.node
32
+ visit_children(doc_node.c_ptr, doc, handler, dispatched) if doc_node
33
+ handler.end_document if dispatched[:end_document]
34
+ self
35
+ end
36
+
37
+ NODE = Leptris::XML::FFI
38
+ ELEMENT = Leptris::XML::FFI::NODE_ELEMENT
39
+ TEXT = Leptris::XML::FFI::NODE_TEXT
40
+ COMMENT = Leptris::XML::FFI::NODE_COMMENT
41
+ CDATA = Leptris::XML::FFI::NODE_CDATA
42
+ PI = Leptris::XML::FFI::NODE_PI
43
+
44
+ def visit_children(c_ptr, doc, handler, dispatched)
45
+ wants_text_kinds = dispatched[:characters] ||
46
+ dispatched[:comment] ||
47
+ dispatched[:cdata] || dispatched[:pi]
48
+ Leptris::XML::FFI.fetch_children(c_ptr).each do |child_ptr|
49
+ case Leptris::XML::FFI.leptris_node_get_type(child_ptr)
50
+ when ELEMENT
51
+ visit_element(
52
+ Leptris::XML::Node.wrap(child_ptr, doc, node_type: ELEMENT),
53
+ handler, dispatched)
54
+ when TEXT
55
+ if dispatched[:characters]
56
+ handler.characters(utf8(NODE.leptris_text_node_get_content(child_ptr)))
57
+ end
58
+ when CDATA
59
+ if dispatched[:cdata]
60
+ handler.cdata_block(utf8(NODE.leptris_cdata_node_get_content(child_ptr)))
61
+ end
62
+ when COMMENT
63
+ if dispatched[:comment]
64
+ handler.comment(utf8(NODE.leptris_comment_node_get_content(child_ptr)))
65
+ end
66
+ when PI
67
+ if dispatched[:pi]
68
+ handler.processing_instruction(
69
+ utf8(NODE.leptris_pi_node_get_target(child_ptr)),
70
+ Leptris::XML::FFI.read_pi_data(
71
+ utf8(NODE.leptris_pi_node_get_data(child_ptr))))
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def visit_element(element, handler, dispatched)
78
+ qname = element.prefix ? "#{element.prefix}:#{element.name}" : element.name
79
+ if dispatched[:start_prefix]
80
+ element.namespace_definitions.each do |ns|
81
+ handler.start_prefix_mapping(ns.prefix.to_s, ns.href.to_s)
82
+ end
83
+ end
84
+ if dispatched[:start_element]
85
+ if dispatched[:start_element] == :one_arg
86
+ handler.start_element(qname)
87
+ else
88
+ handler.start_element(qname, attr_pairs(element.c_ptr))
89
+ end
90
+ end
91
+ visit_children(element.c_ptr, element.document, handler, dispatched)
92
+ handler.end_element(qname) if dispatched[:end_element]
93
+ if dispatched[:end_prefix]
94
+ element.namespace_definitions.reverse_each do |ns|
95
+ handler.end_prefix_mapping(ns.prefix.to_s)
96
+ end
97
+ end
98
+ end
99
+
100
+ # [name, value] pairs in source order, read straight from the
101
+ # attribute chain — no Attr objects, no hash.
102
+ def attr_pairs(c_ptr)
103
+ pairs = []
104
+ attr = Leptris::XML::FFI.leptris_element_first_attribute(c_ptr)
105
+ until attr.nil? || attr.null?
106
+ pairs << [utf8(Leptris::XML::FFI.leptris_attribute_get_name(attr)),
107
+ utf8(Leptris::XML::FFI.leptris_attribute_get_value(
108
+ c_ptr, attr))]
109
+ attr = Leptris::XML::FFI.leptris_attribute_next(attr)
110
+ end
111
+ pairs
112
+ end
113
+
114
+ def utf8(str)
115
+ str.nil? ? nil : str.force_encoding(Encoding::UTF_8)
116
+ end
117
+ end
@@ -8,9 +8,18 @@ class Leptris::XML::SAX::Parser
8
8
 
9
9
  attr_reader :document, :encoding
10
10
 
11
- def initialize(handler = Leptris::XML::SAX::Document.new, encoding = nil)
11
+ # streaming: false (default) delivers from a DOM parse —
12
+ # leptris-ruby#95: the engine's streaming SAX transports corrupt
13
+ # attribute state on nested attribute-heavy shapes while the DOM
14
+ # parser reads the same bytes correctly. streaming: true opts
15
+ # back into the engine transports (bulk/callback by override
16
+ # weight) once the engine fix lands, or for consumers that
17
+ # accept the risk.
18
+ def initialize(handler = Leptris::XML::SAX::Document.new,
19
+ encoding = nil, streaming: false)
12
20
  @document = handler
13
21
  @encoding = encoding
22
+ @streaming = streaming
14
23
  end
15
24
 
16
25
  # Swapping the handler invalidates the memoized callback struct so
@@ -41,6 +50,11 @@ class Leptris::XML::SAX::Parser
41
50
 
42
51
  def parse_memory(string)
43
52
  string = string.dup.force_encoding("UTF-8")
53
+ unless @streaming
54
+ Leptris::XML::SAX::DomDispatch.parse(
55
+ @document, dispatched_kinds, string)
56
+ return self
57
+ end
44
58
  if bulk_dispatch?
45
59
  parse_memory_bulk(string)
46
60
  else
@@ -133,6 +147,10 @@ class Leptris::XML::SAX::Parser
133
147
  end
134
148
 
135
149
  def parse_io(io)
150
+ unless @streaming
151
+ parse_memory(io.read)
152
+ return self
153
+ end
136
154
  handler_struct = self.handler_struct
137
155
  parser_ptr = Leptris::XML::FFI.leptris_sax_parser_create(
138
156
  handler_struct.pointer, nil)
@@ -159,7 +177,11 @@ class Leptris::XML::SAX::Parser
159
177
  end
160
178
 
161
179
  def parse_file(path)
162
- File.open(path, "r") { |f| parse_io(f) }
180
+ if @streaming
181
+ File.open(path, "r") { |f| parse_io(f) }
182
+ else
183
+ parse_memory(File.read(path))
184
+ end
163
185
  end
164
186
 
165
187
  private
@@ -69,6 +69,18 @@ class Leptris::XML::SAX::Recorder
69
69
  @handle = nil
70
70
  end
71
71
 
72
+ # Resets a finished recorder for the next document (libleptris
73
+ # 1.9.10, upstream #594): fresh parser state, record/arena
74
+ # buffers retained — a one-document-per-parse loop reuses ONE
75
+ # recorder without new/free churn. Records restart empty.
76
+ def reset
77
+ rc = Leptris::XML::FFI.leptris_sax_recorder_reset(@handle)
78
+ if rc != 0
79
+ raise Leptris::XML::Error, "leptris_sax_recorder_reset failed"
80
+ end
81
+ self
82
+ end
83
+
72
84
  # Feed one chunk; records/arena reset at feed entry, so drain
73
85
  # after every feed. Returns self.
74
86
  def feed(chunk, final: false)
@@ -8,6 +8,7 @@ module Leptris
8
8
  autoload :Document, "leptris/xml/sax/document"
9
9
  autoload :Parser, "leptris/xml/sax/parser"
10
10
  autoload :Recorder, "leptris/xml/sax/recorder"
11
+ autoload :DomDispatch, "leptris/xml/sax/dom_dispatch"
11
12
  end
12
13
  end
13
14
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.35
4
+ version: 1.9.36
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -98,6 +98,7 @@ files:
98
98
  - lib/leptris/xml/pull.rb
99
99
  - lib/leptris/xml/sax.rb
100
100
  - lib/leptris/xml/sax/document.rb
101
+ - lib/leptris/xml/sax/dom_dispatch.rb
101
102
  - lib/leptris/xml/sax/parser.rb
102
103
  - lib/leptris/xml/sax/recorder.rb
103
104
  - lib/leptris/xml/searchable.rb