leptris 1.9.21 → 1.9.23
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 +4 -4
- data/CHANGELOG.md +41 -0
- data/Rakefile +1 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/ffi.rb +54 -0
- data/lib/leptris/xml/iterparse.rb +89 -14
- data/lib/leptris/xml/sax/recorder.rb +156 -0
- data/lib/leptris/xml/sax.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ec6d834f30c917e77000bf9784e85ecc59b8752c246f28bb6932bd003f0d45f9
|
|
4
|
+
data.tar.gz: a0cd90f99e074b719d9538dc3c1f245e85fc3185fbe693c5b4dab3bee405c77b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d4de54e8c4cb0b5d3951a7c7ac63fc4ce4d97335597d923eb424f445cbaaa790fb27220563d6bbd65ca7490522f2a42423da5e28b199b9e16fcfeb638b148dfb
|
|
7
|
+
data.tar.gz: 797164d946ad51fde25139e6cbdcc45d9c86a18f5062b09a93c63e098f443ad4b48c1c1f8cce6d26b7f2583a7decd774954dbe4dbe77e4839ad3f5c9d9e3eab9
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ 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.22] - 2026-08-27
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **libleptris 1.9.4 lockstep** (leptris/leptris#585, #586): Rakefile
|
|
13
|
+
pin bumped; symbol audit 242/242 attached-vs-exported with the ten
|
|
14
|
+
new functions.
|
|
15
|
+
- **`SAX::Recorder`**: public chunked-event API over the C recorder —
|
|
16
|
+
`Recorder.parse(xml_or_io)` / `Recorder#feed` / `#each_event`
|
|
17
|
+
yield `kind, name, text, attrs, line, column` with UTF-8 strings,
|
|
18
|
+
drained as one bulk read per chunk (records via `read_bytes` +
|
|
19
|
+
`unpack` against the packed string arena — no per-event FFI
|
|
20
|
+
struct). NOT wired into `SAX::Parser#parse_memory`: measured
|
|
21
|
+
parity-to-−30% vs the callback path (per-parse recorder setup
|
|
22
|
+
scales with input size; findings and measurements filed upstream
|
|
23
|
+
as leptris/leptris#594). It remains the right shape for hosts
|
|
24
|
+
that drain bulk events per chunk.
|
|
25
|
+
- **Iterparse v2** (#586): `Iterparse.parse` / `.parse_file` take
|
|
26
|
+
`mode:` — `:top_level` (v1: the root's completed children) or
|
|
27
|
+
`:full_document` (every element, post-order, child before parent).
|
|
28
|
+
`#namespace_uri(prefix)` / `#namespace_count` resolve prefixes on
|
|
29
|
+
the last yielded element's in-scope snapshot (call inside the run
|
|
30
|
+
block); `#error` reports truncated/malformed input.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **Iterparse lifecycle**: `#run` no longer frees in `ensure` — the
|
|
35
|
+
block form of `.parse`/`.parse_file` frees on return (Pull's
|
|
36
|
+
pattern), the no-block form returns a live iterator for explicit
|
|
37
|
+
`#free`. This fixes the `enum_for` path, which previously freed
|
|
38
|
+
the handle before the enumerator drove it, and lets `#error`
|
|
39
|
+
stay readable after the run (`#free` snapshots the terminal
|
|
40
|
+
message). Full-document mode measures +36% CPU over top-level —
|
|
41
|
+
proportional to yielding every element instead of root children.
|
|
42
|
+
|
|
43
|
+
### Meta
|
|
44
|
+
|
|
45
|
+
- Head-to-head vs published 1.9.21 (engine 1.9.0 vs 1.9.4): SAX
|
|
46
|
+
callback parsing and iterparse at parity across two passes; no
|
|
47
|
+
regression from the engine bump.
|
|
48
|
+
|
|
8
49
|
## [1.9.21] - 2026-08-27
|
|
9
50
|
|
|
10
51
|
### 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.
|
|
11
|
+
LIBLEPTRIS_VERSION = "1.9.4"
|
|
12
12
|
|
|
13
13
|
CMAKE_FLAGS = %w[
|
|
14
14
|
-DCMAKE_BUILD_TYPE=Release
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -43,6 +43,7 @@ module Leptris
|
|
|
43
43
|
typedef :pointer, :leptris_xpath_ns_set
|
|
44
44
|
typedef :pointer, :leptris_xpath_compiled
|
|
45
45
|
typedef :pointer, :leptris_xslt
|
|
46
|
+
typedef :pointer, :leptris_sax_recorder
|
|
46
47
|
typedef :pointer, :leptris_pull_parser
|
|
47
48
|
typedef :pointer, :leptris_iterparse
|
|
48
49
|
typedef :pointer, :leptris_xpath_var_set
|
|
@@ -81,6 +82,33 @@ module Leptris
|
|
|
81
82
|
:recover, :int
|
|
82
83
|
end
|
|
83
84
|
|
|
85
|
+
# LeptrisSaxEventRecord (types.h, issue #585): fixed layout the
|
|
86
|
+
# header explicitly freezes for FFI mirrors. Strings are NOT
|
|
87
|
+
# inline — name/text/attrs slice the recorder's arena.
|
|
88
|
+
class SaxEventRecord < ::FFI::Struct
|
|
89
|
+
layout :kind, :uint8,
|
|
90
|
+
:reserved, [:uint8, 7],
|
|
91
|
+
:name_off, :uint32, :name_len, :uint32,
|
|
92
|
+
:text_off, :uint32, :text_len, :uint32,
|
|
93
|
+
:attrs_off, :uint32, :attr_count, :uint32,
|
|
94
|
+
:line, :uint32, :column, :uint32
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
SAX_EVENT_START_DOCUMENT = 0
|
|
98
|
+
SAX_EVENT_END_DOCUMENT = 1
|
|
99
|
+
SAX_EVENT_START_ELEMENT = 2
|
|
100
|
+
SAX_EVENT_END_ELEMENT = 3
|
|
101
|
+
SAX_EVENT_CHARACTERS = 4
|
|
102
|
+
SAX_EVENT_COMMENT = 5
|
|
103
|
+
SAX_EVENT_CDATA = 6
|
|
104
|
+
SAX_EVENT_PI = 7
|
|
105
|
+
SAX_EVENT_START_PREFIX = 8
|
|
106
|
+
SAX_EVENT_END_PREFIX = 9
|
|
107
|
+
SAX_EVENT_ERROR = 10
|
|
108
|
+
|
|
109
|
+
ITERPARSE_TOP_LEVEL = 0
|
|
110
|
+
ITERPARSE_FULL_DOCUMENT = 1
|
|
111
|
+
|
|
84
112
|
# struct LeptrisPullEvent (leptris/sax.h): FFI::Struct derives
|
|
85
113
|
# the field offsets from the layout, so ABI changes are a
|
|
86
114
|
# one-line edit here instead of silent offset drift in callers.
|
|
@@ -499,6 +527,19 @@ module Leptris
|
|
|
499
527
|
[:pointer, :pointer], :leptris_sax_parser
|
|
500
528
|
attach_function :leptris_sax_parser_feed,
|
|
501
529
|
[:leptris_sax_parser, :string, :size_t, :int], :int
|
|
530
|
+
# Chunked event recorder (libleptris 1.9.4, #585): buffers
|
|
531
|
+
# events C-side (fixed records + packed arena); the host
|
|
532
|
+
# drains in bulk — callbacks become O(chunks), not O(events).
|
|
533
|
+
attach_function :leptris_sax_recorder_new,
|
|
534
|
+
[], :leptris_sax_recorder
|
|
535
|
+
attach_function :leptris_sax_recorder_feed,
|
|
536
|
+
[:leptris_sax_recorder, :string, :size_t, :int], :int
|
|
537
|
+
attach_function :leptris_sax_recorder_records,
|
|
538
|
+
[:leptris_sax_recorder, :pointer], :pointer
|
|
539
|
+
attach_function :leptris_sax_recorder_arena,
|
|
540
|
+
[:leptris_sax_recorder, :pointer], :pointer
|
|
541
|
+
attach_function :leptris_sax_recorder_free,
|
|
542
|
+
[:leptris_sax_recorder], :void
|
|
502
543
|
attach_function :leptris_sax_parser_free,
|
|
503
544
|
[:leptris_sax_parser], :void
|
|
504
545
|
attach_function :leptris_sax_parser_set_streaming,
|
|
@@ -528,6 +569,19 @@ module Leptris
|
|
|
528
569
|
[:string], :leptris_iterparse
|
|
529
570
|
attach_function :leptris_iterparse_next,
|
|
530
571
|
[:leptris_iterparse], :leptris_element
|
|
572
|
+
# iterparse v2 (libleptris 1.9.4, #586): explicit yield mode
|
|
573
|
+
# (top-level children or full-document post-order), namespace
|
|
574
|
+
# resolution on the last yielded element, error channel.
|
|
575
|
+
attach_function :leptris_iterparse_new_ex,
|
|
576
|
+
[:string, :size_t, :int], :leptris_iterparse
|
|
577
|
+
attach_function :leptris_iterparse_new_file_ex,
|
|
578
|
+
[:string, :int], :leptris_iterparse
|
|
579
|
+
attach_function :leptris_iterparse_ns_count,
|
|
580
|
+
[:leptris_iterparse], :size_t
|
|
581
|
+
attach_function :leptris_iterparse_ns_uri,
|
|
582
|
+
[:leptris_iterparse, :string], :string
|
|
583
|
+
attach_function :leptris_iterparse_error,
|
|
584
|
+
[:leptris_iterparse], :string
|
|
531
585
|
attach_function :leptris_iterparse_free,
|
|
532
586
|
[:leptris_iterparse], :void
|
|
533
587
|
|
|
@@ -12,34 +12,78 @@ require "ffi"
|
|
|
12
12
|
# process(element) # subtree valid until the next iteration
|
|
13
13
|
# end
|
|
14
14
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
15
|
+
# v2 (libleptris 1.9.4, #586): an explicit yield mode —
|
|
16
|
+
# :top_level (v1: the root's completed children) or
|
|
17
|
+
# :full_document (every element in document-order completion —
|
|
18
|
+
# post-order, child before parent, subtree attached) — plus
|
|
19
|
+
# namespace resolution on the last yielded element and an error
|
|
20
|
+
# channel for truncated input:
|
|
21
|
+
#
|
|
22
|
+
# it = Leptris::XML::Iterparse.parse(xml, mode: :full_document)
|
|
23
|
+
# it.run do |el|
|
|
24
|
+
# it.namespace_uri("p") # in scope on el, the last yielded
|
|
25
|
+
# end
|
|
26
|
+
# it.free
|
|
27
|
+
#
|
|
28
|
+
# Leptris::XML::Iterparse.parse(xml) { |el| ... }.error
|
|
29
|
+
# # => nil, or the parse failure (readable after the run: the
|
|
30
|
+
# # block form frees the iterator on return, snapshotting it)
|
|
31
|
+
#
|
|
32
|
+
# The yielded elements have no parent Document; #document returns
|
|
33
|
+
# nil and they must not outlive the iteration.
|
|
20
34
|
class Leptris::XML::Iterparse
|
|
35
|
+
MODES = {
|
|
36
|
+
top_level: Leptris::XML::FFI::ITERPARSE_TOP_LEVEL,
|
|
37
|
+
full_document: Leptris::XML::FFI::ITERPARSE_FULL_DOCUMENT,
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
21
40
|
# NOTE: an IO argument is read fully into memory before the C
|
|
22
41
|
# iterator starts — the bounded-memory path is parse_file (C-side
|
|
23
42
|
# file streaming). The C API takes one (xml, len) buffer.
|
|
24
|
-
def self.parse(xml_or_io, &block)
|
|
43
|
+
def self.parse(xml_or_io, mode: :top_level, &block)
|
|
25
44
|
xml = xml_or_io.respond_to?(:read) ? xml_or_io.read : xml_or_io.to_s
|
|
26
|
-
iterator = new(Leptris::XML::FFI.
|
|
27
|
-
|
|
45
|
+
iterator = new(Leptris::XML::FFI.leptris_iterparse_new_ex(
|
|
46
|
+
xml, xml.bytesize, mode_code(mode)))
|
|
47
|
+
return iterator unless block
|
|
48
|
+
begin
|
|
49
|
+
iterator.run(&block)
|
|
50
|
+
ensure
|
|
51
|
+
iterator.free
|
|
52
|
+
end
|
|
53
|
+
iterator
|
|
28
54
|
end
|
|
29
55
|
|
|
30
|
-
def self.parse_file(path, &block)
|
|
31
|
-
iterator = new(Leptris::XML::FFI.
|
|
32
|
-
|
|
56
|
+
def self.parse_file(path, mode: :top_level, &block)
|
|
57
|
+
iterator = new(Leptris::XML::FFI.leptris_iterparse_new_file_ex(
|
|
58
|
+
path, mode_code(mode)))
|
|
59
|
+
return iterator unless block
|
|
60
|
+
begin
|
|
61
|
+
iterator.run(&block)
|
|
62
|
+
ensure
|
|
63
|
+
iterator.free
|
|
64
|
+
end
|
|
65
|
+
iterator
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.mode_code(mode)
|
|
69
|
+
MODES.fetch(mode) do
|
|
70
|
+
raise ArgumentError,
|
|
71
|
+
"mode must be :top_level or :full_document, got #{mode.inspect}"
|
|
72
|
+
end
|
|
33
73
|
end
|
|
74
|
+
private_class_method :mode_code
|
|
34
75
|
|
|
35
76
|
def initialize(handle)
|
|
36
77
|
raise Leptris::XML::ParseError, "leptris_iterparse_new failed" if handle.null?
|
|
37
78
|
@handle = handle
|
|
38
79
|
end
|
|
39
80
|
|
|
81
|
+
# Yields completed elements until the document is exhausted or the
|
|
82
|
+
# parse fails (see #error). Does not free the iterator — the block
|
|
83
|
+
# form of .parse/.parse_file does, or call #free explicitly.
|
|
40
84
|
def run
|
|
41
85
|
return enum_for(:run) unless block_given?
|
|
42
|
-
|
|
86
|
+
while @handle
|
|
43
87
|
ptr = Leptris::XML::FFI.leptris_iterparse_next(@handle)
|
|
44
88
|
break if ptr.null?
|
|
45
89
|
# document: nil — the subtree belongs to the iterator's pool,
|
|
@@ -48,13 +92,44 @@ class Leptris::XML::Iterparse
|
|
|
48
92
|
yield Leptris::XML::Node.wrap(ptr, nil)
|
|
49
93
|
end
|
|
50
94
|
self
|
|
51
|
-
ensure
|
|
52
|
-
free
|
|
53
95
|
end
|
|
54
96
|
|
|
97
|
+
# The URI bound to +prefix+ ("" for the default namespace) in the
|
|
98
|
+
# last yielded element's in-scope namespace snapshot, or nil when
|
|
99
|
+
# unbound (v2, #586). Valid only while the iterator is live —
|
|
100
|
+
# call it inside the #run block; nil after #free.
|
|
101
|
+
def namespace_uri(prefix)
|
|
102
|
+
return nil if @handle.nil?
|
|
103
|
+
Leptris::XML::FFI.leptris_iterparse_ns_uri(@handle, prefix.to_s)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Number of namespace bindings in the last yielded element's
|
|
107
|
+
# scope. Valid only while the iterator is live; 0 after #free.
|
|
108
|
+
def namespace_count
|
|
109
|
+
return 0 if @handle.nil?
|
|
110
|
+
Leptris::XML::FFI.leptris_iterparse_ns_count(@handle)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The parse error for malformed/truncated input, or nil when the
|
|
114
|
+
# document is well-formed and exhausted. Yielded elements stop at
|
|
115
|
+
# the failure point; #free snapshots the message so this stays
|
|
116
|
+
# readable after the block form of .parse returns.
|
|
117
|
+
def error
|
|
118
|
+
@handle ? live_error : @last_error
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Releases the C iterator. Safe to call twice.
|
|
55
122
|
def free
|
|
56
123
|
return if @handle.nil?
|
|
124
|
+
@last_error = live_error
|
|
57
125
|
Leptris::XML::FFI.leptris_iterparse_free(@handle)
|
|
58
126
|
@handle = nil
|
|
59
127
|
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def live_error
|
|
132
|
+
msg = Leptris::XML::FFI.leptris_iterparse_error(@handle)
|
|
133
|
+
msg.nil? || msg.empty? ? nil : msg
|
|
134
|
+
end
|
|
60
135
|
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
# Chunked event recorder (libleptris 1.9.4, issue #585): a SAX
|
|
6
|
+
# transport that buffers events C-side — fixed LeptrisSaxEventRecord
|
|
7
|
+
# entries plus a packed string arena — and drains them in one bulk
|
|
8
|
+
# read per fed chunk. The handler receives the identical calls the
|
|
9
|
+
# callback API makes; only the transport changes, and the callback
|
|
10
|
+
# count becomes O(chunks), not O(events): through FFI, per-event
|
|
11
|
+
# C-to-Ruby dispatch cost more than the parse itself.
|
|
12
|
+
#
|
|
13
|
+
# Leptris::XML::SAX::Recorder.parse(xml) do |kind, name, text, attrs|
|
|
14
|
+
# # kind: :start_element, :characters, ... (see KINDS)
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
class Leptris::XML::SAX::Recorder
|
|
18
|
+
KINDS = {
|
|
19
|
+
Leptris::XML::FFI::SAX_EVENT_START_DOCUMENT => :start_document,
|
|
20
|
+
Leptris::XML::FFI::SAX_EVENT_END_DOCUMENT => :end_document,
|
|
21
|
+
Leptris::XML::FFI::SAX_EVENT_START_ELEMENT => :start_element,
|
|
22
|
+
Leptris::XML::FFI::SAX_EVENT_END_ELEMENT => :end_element,
|
|
23
|
+
Leptris::XML::FFI::SAX_EVENT_CHARACTERS => :characters,
|
|
24
|
+
Leptris::XML::FFI::SAX_EVENT_COMMENT => :comment,
|
|
25
|
+
Leptris::XML::FFI::SAX_EVENT_CDATA => :cdata,
|
|
26
|
+
Leptris::XML::FFI::SAX_EVENT_PI => :pi,
|
|
27
|
+
Leptris::XML::FFI::SAX_EVENT_START_PREFIX => :start_prefix,
|
|
28
|
+
Leptris::XML::FFI::SAX_EVENT_END_PREFIX => :end_prefix,
|
|
29
|
+
Leptris::XML::FFI::SAX_EVENT_ERROR => :error,
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
def self.open
|
|
33
|
+
raw = Leptris::XML::FFI.leptris_sax_recorder_new
|
|
34
|
+
if raw.null?
|
|
35
|
+
raise Leptris::XML::Error, "leptris_sax_recorder_new failed"
|
|
36
|
+
end
|
|
37
|
+
new(raw)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# One-shot parse over a complete document, yielding Record structs.
|
|
41
|
+
def self.parse(xml_or_io)
|
|
42
|
+
recorder = open
|
|
43
|
+
begin
|
|
44
|
+
if xml_or_io.respond_to?(:read)
|
|
45
|
+
recorder.feed_stream(xml_or_io) { |*args| yield(*args) }
|
|
46
|
+
else
|
|
47
|
+
recorder.feed(xml_or_io.to_s, final: true)
|
|
48
|
+
recorder.each_event { |*args| yield(*args) }
|
|
49
|
+
end
|
|
50
|
+
ensure
|
|
51
|
+
recorder.free
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def initialize(handle)
|
|
56
|
+
@handle = handle
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def free
|
|
60
|
+
return if @handle.nil?
|
|
61
|
+
Leptris::XML::FFI.leptris_sax_recorder_free(@handle)
|
|
62
|
+
@handle = nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Feed one chunk; records/arena reset at feed entry, so drain
|
|
66
|
+
# after every feed. Returns self.
|
|
67
|
+
def feed(chunk, final: false)
|
|
68
|
+
chunk = chunk.dup.force_encoding(Encoding::UTF_8)
|
|
69
|
+
rc = Leptris::XML::FFI.leptris_sax_recorder_feed(
|
|
70
|
+
@handle, chunk, chunk.bytesize, final ? 1 : 0)
|
|
71
|
+
rc
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Feed an IO in chunks, draining events after each chunk.
|
|
75
|
+
def feed_stream(io, chunk_size = 65_536, &block)
|
|
76
|
+
while (chunk = io.read(chunk_size))
|
|
77
|
+
feed(chunk)
|
|
78
|
+
each_event(&block)
|
|
79
|
+
end
|
|
80
|
+
feed("", final: true)
|
|
81
|
+
each_event(&block)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Drains the current chunk's records: bulk-reads the record array
|
|
85
|
+
# and the packed arena (two FFI calls), then slices every string
|
|
86
|
+
# from the arena in Ruby — no per-string FFI.
|
|
87
|
+
def each_event
|
|
88
|
+
count_ptr = ::FFI::MemoryPointer.new(:size_t)
|
|
89
|
+
len_ptr = ::FFI::MemoryPointer.new(:size_t)
|
|
90
|
+
begin
|
|
91
|
+
records_ptr = Leptris::XML::FFI.leptris_sax_recorder_records(
|
|
92
|
+
@handle, count_ptr)
|
|
93
|
+
return self if records_ptr.null? || count_ptr.read_uint64.zero?
|
|
94
|
+
|
|
95
|
+
arena_ptr = Leptris::XML::FFI.leptris_sax_recorder_arena(
|
|
96
|
+
@handle, len_ptr)
|
|
97
|
+
arena = arena_ptr.read_bytes(len_ptr.read_uint64)
|
|
98
|
+
count = count_ptr.read_uint64
|
|
99
|
+
|
|
100
|
+
# The whole record block in ONE read: read_bytes + String#unpack
|
|
101
|
+
# (C-fast) — two FFI calls per chunk total (records + arena),
|
|
102
|
+
# the true bulk discipline. Template per record: kind byte,
|
|
103
|
+
# 7 pad, then the eight uint32 fields (little-endian hosts —
|
|
104
|
+
# the layout the header freezes for FFI mirrors).
|
|
105
|
+
stride = Leptris::XML::FFI::SaxEventRecord.size
|
|
106
|
+
fields = records_ptr.read_bytes(count * stride)
|
|
107
|
+
.unpack("C x7 V8" * count)
|
|
108
|
+
|
|
109
|
+
i = 0
|
|
110
|
+
while i < count
|
|
111
|
+
kind = KINDS[fields[i * 9]]
|
|
112
|
+
if kind
|
|
113
|
+
base = i * 9 + 1
|
|
114
|
+
yield kind,
|
|
115
|
+
slice(arena, fields[base], fields[base + 1]),
|
|
116
|
+
slice(arena, fields[base + 2], fields[base + 3]),
|
|
117
|
+
attrs_from(arena, fields[base + 4], fields[base + 5]),
|
|
118
|
+
fields[base + 6], fields[base + 7]
|
|
119
|
+
end
|
|
120
|
+
i += 1
|
|
121
|
+
end
|
|
122
|
+
ensure
|
|
123
|
+
count_ptr.free
|
|
124
|
+
len_ptr.free
|
|
125
|
+
end
|
|
126
|
+
self
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# Zero-length slices are empty strings, not nil — the default
|
|
132
|
+
# namespace prefix is legitimately "" (START_PREFIX: name may be
|
|
133
|
+
# ""), matching the callback transport's delivery.
|
|
134
|
+
def slice(arena, off, len)
|
|
135
|
+
arena.byteslice(off, len).force_encoding(Encoding::UTF_8)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# attrs_off addresses name\0value\0... pairs, attr_count pairs;
|
|
139
|
+
# nil when the element carries none.
|
|
140
|
+
def attrs_from(arena, off, count)
|
|
141
|
+
return nil if count.zero?
|
|
142
|
+
hash = {}
|
|
143
|
+
pos = off
|
|
144
|
+
count.times do
|
|
145
|
+
name_end = arena.index("\0", pos)
|
|
146
|
+
name = arena.byteslice(pos, name_end - pos)
|
|
147
|
+
pos = name_end + 1
|
|
148
|
+
value_end = arena.index("\0", pos)
|
|
149
|
+
value = arena.byteslice(pos, value_end - pos)
|
|
150
|
+
pos = value_end + 1
|
|
151
|
+
hash[name.force_encoding(Encoding::UTF_8)] =
|
|
152
|
+
value.force_encoding(Encoding::UTF_8)
|
|
153
|
+
end
|
|
154
|
+
hash
|
|
155
|
+
end
|
|
156
|
+
end
|
data/lib/leptris/xml/sax.rb
CHANGED
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.
|
|
4
|
+
version: 1.9.23
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -99,6 +99,7 @@ files:
|
|
|
99
99
|
- lib/leptris/xml/sax.rb
|
|
100
100
|
- lib/leptris/xml/sax/document.rb
|
|
101
101
|
- lib/leptris/xml/sax/parser.rb
|
|
102
|
+
- lib/leptris/xml/sax/recorder.rb
|
|
102
103
|
- lib/leptris/xml/searchable.rb
|
|
103
104
|
- lib/leptris/xml/serialization.rb
|
|
104
105
|
- lib/leptris/xml/text.rb
|