moxml 0.5.10 → 0.5.11
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/lib/moxml/adapter/base.rb +8 -0
- data/lib/moxml/adapter/leptris.rb +75 -19
- data/lib/moxml/document.rb +11 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_emitter.rb +6 -2
- data/spec/moxml/adapter/leptris_spec.rb +25 -0
- data/spec/moxml/document_free_spec.rb +22 -0
- data/spec/moxml/materializer_spec.rb +11 -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: 15a0dd834bc1c9637a8d2284173d4994b1717b1bd2d8d9461686a56c679942e6
|
|
4
|
+
data.tar.gz: 4635e66b998d8fa371dd1617d0f960fc31b8a886627095061f945aa8b29cae41
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6eace14b6adb980bc4c14c6f628b231961f47fab55e05a0bf809591b4203a6581f0cb333dc358ec3bebeb1c239f7ecd65fc68ca34ee70fb1bf71fbf8c00f278c
|
|
7
|
+
data.tar.gz: f03e39569fd299ac6a7e8dc266d97a5499fea6f89b166f64e46944649286df8a6ceb4fd4732b6a67cac160f2318b0ed4c79fc8d782184b3a090b9c3feb65be1e
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -198,6 +198,14 @@ namespace_validation_mode: :strict)
|
|
|
198
198
|
false
|
|
199
199
|
end
|
|
200
200
|
|
|
201
|
+
# Deterministic native-memory release for adapters backed by
|
|
202
|
+
# C trees (issue #134). GC-managed engines no-op; released
|
|
203
|
+
# documents raise the engine's use-after-free error on
|
|
204
|
+
# further access.
|
|
205
|
+
def free_document(_native)
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
|
|
201
209
|
# Check if the native document has an XML declaration
|
|
202
210
|
# @param native_doc the native document object
|
|
203
211
|
# @param wrapper [Moxml::Document] the wrapper with has_xml_declaration flag
|
|
@@ -23,6 +23,10 @@ module Moxml
|
|
|
23
23
|
# the libxml2-model document node — prolog/epilog parts in
|
|
24
24
|
# document order. Older bindings keep the flat pre-root PI path.
|
|
25
25
|
DOC_NODE_SUPPORTED = ::Leptris::XML::Document.method_defined?(:node)
|
|
26
|
+
# libleptris 1.9.8 (leptris-ruby 1.9.30): plain parse excludes
|
|
27
|
+
# DTD ATTLIST defaults, matching libxml2/Nokogiri semantics;
|
|
28
|
+
# ParseOptions::DTDATTR opts in (leptris/leptris#606).
|
|
29
|
+
DTDATTR_SUPPORTED = ::Leptris::XML::ParseOptions.const_defined?(:DTDATTR)
|
|
26
30
|
|
|
27
31
|
class << self
|
|
28
32
|
def attachments
|
|
@@ -42,8 +46,14 @@ module Moxml
|
|
|
42
46
|
# readonly: true (issue #133): the binding memoizes reads and
|
|
43
47
|
# refuses mutations — the parse-and-read lifecycle for
|
|
44
48
|
# multi-pass consumers (comparison, diff, signature).
|
|
49
|
+
# dtdattr: true opts into DTD ATTLIST default materialization
|
|
50
|
+
# (off by default since libleptris 1.9.8, matching libxml2).
|
|
45
51
|
native_doc = begin
|
|
46
|
-
::Leptris::XML::Document.parse(
|
|
52
|
+
::Leptris::XML::Document.parse(
|
|
53
|
+
processed,
|
|
54
|
+
readonly: options[:readonly] == true,
|
|
55
|
+
options: dtdattr_parse_options(options),
|
|
56
|
+
)
|
|
47
57
|
rescue ::Leptris::XML::ParseError => e
|
|
48
58
|
# libleptris has no recovery mode that survives unclosed
|
|
49
59
|
# tags; non-strict callers get an empty document, matching
|
|
@@ -61,6 +71,30 @@ module Moxml
|
|
|
61
71
|
doc
|
|
62
72
|
end
|
|
63
73
|
|
|
74
|
+
# nil when DTDATTR is unsupported or not requested — the
|
|
75
|
+
# binding treats a nil options hash as plain defaults.
|
|
76
|
+
def dtdattr_parse_options(options)
|
|
77
|
+
return nil unless DTDATTR_SUPPORTED && options[:dtdattr] == true
|
|
78
|
+
|
|
79
|
+
::Leptris::XML::ParseOptions.dtdattr
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Issue #134: deterministic release of the C tree. The binding
|
|
83
|
+
# clears its wrapper cache and raises UseAfterFreeError on
|
|
84
|
+
# later access; moxml-side attachments for the document are
|
|
85
|
+
# swept too (the context wrapper identity map self-cleans via
|
|
86
|
+
# its size valve).
|
|
87
|
+
DOCUMENT_ATTACHMENT_KEYS = %i[
|
|
88
|
+
entity_markers doc_pi_nodes declaration doctype
|
|
89
|
+
had_source_declaration document_text
|
|
90
|
+
].freeze
|
|
91
|
+
|
|
92
|
+
def free_document(native)
|
|
93
|
+
DOCUMENT_ATTACHMENT_KEYS.each { |key| attachments.delete(native, key) }
|
|
94
|
+
native.free
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
64
98
|
def create_document(_native_doc = nil)
|
|
65
99
|
::Leptris::XML::Document.create
|
|
66
100
|
end
|
|
@@ -248,24 +282,33 @@ module Moxml
|
|
|
248
282
|
# ER expansion); the bulk path has no marker handling.
|
|
249
283
|
return nil if doc.nil? || attachments.get(doc, :entity_markers)
|
|
250
284
|
|
|
251
|
-
|
|
285
|
+
# leptris 1.9.28+'s traverse follows the document chain; from
|
|
286
|
+
# an arbitrary element it can visit following siblings. Use
|
|
287
|
+
# the bulk path only for document materialization, and filter
|
|
288
|
+
# the stream to the document element's subtree (issue #140).
|
|
289
|
+
return nil unless native.is_a?(::Leptris::XML::Document)
|
|
290
|
+
|
|
291
|
+
root = doc.root
|
|
252
292
|
return nil if root.nil?
|
|
253
293
|
|
|
254
294
|
depth_memo = {}.compare_by_identity
|
|
255
295
|
root.traverse do |node|
|
|
296
|
+
depth = material_depth_in_subtree(node, root, depth_memo)
|
|
297
|
+
next if depth.nil?
|
|
298
|
+
|
|
256
299
|
record = case node
|
|
257
300
|
when ::Leptris::XML::Element
|
|
258
|
-
element_material_record(node,
|
|
301
|
+
element_material_record(node, depth)
|
|
259
302
|
when ::Leptris::XML::CDATA
|
|
260
303
|
# CDATA < Text in the binding: this arm must come
|
|
261
304
|
# first or CDATA content reports as text.
|
|
262
|
-
text_material_record(:cdata, node.content,
|
|
305
|
+
text_material_record(:cdata, node.content, depth)
|
|
263
306
|
when ::Leptris::XML::Text
|
|
264
|
-
text_material_record(:text, node.content,
|
|
307
|
+
text_material_record(:text, node.content, depth)
|
|
265
308
|
when ::Leptris::XML::Comment
|
|
266
|
-
text_material_record(:comment, node.content,
|
|
309
|
+
text_material_record(:comment, node.content, depth)
|
|
267
310
|
when ::Leptris::XML::ProcessingInstruction
|
|
268
|
-
text_material_record(:processing_instruction, node.content,
|
|
311
|
+
text_material_record(:processing_instruction, node.content, depth)
|
|
269
312
|
.merge(qname: node.target)
|
|
270
313
|
end
|
|
271
314
|
yield(record) if record
|
|
@@ -273,7 +316,7 @@ module Moxml
|
|
|
273
316
|
true
|
|
274
317
|
end
|
|
275
318
|
|
|
276
|
-
def element_material_record(node,
|
|
319
|
+
def element_material_record(node, depth)
|
|
277
320
|
attributes = node.each_attribute.map do |attr|
|
|
278
321
|
# Local name + separate prefix, matching the generic
|
|
279
322
|
# path's resolver semantics (moxml's canonical shape).
|
|
@@ -296,11 +339,11 @@ module Moxml
|
|
|
296
339
|
end,
|
|
297
340
|
attributes: attributes,
|
|
298
341
|
text: nil,
|
|
299
|
-
depth:
|
|
342
|
+
depth: depth,
|
|
300
343
|
}
|
|
301
344
|
end
|
|
302
345
|
|
|
303
|
-
def text_material_record(kind, text,
|
|
346
|
+
def text_material_record(kind, text, depth)
|
|
304
347
|
{
|
|
305
348
|
kind: kind,
|
|
306
349
|
qname: nil,
|
|
@@ -309,18 +352,30 @@ module Moxml
|
|
|
309
352
|
namespaces: Materializer::EMPTY_ATTRIBUTES,
|
|
310
353
|
attributes: Materializer::EMPTY_ATTRIBUTES,
|
|
311
354
|
text: text,
|
|
312
|
-
depth:
|
|
355
|
+
depth: depth,
|
|
313
356
|
}
|
|
314
357
|
end
|
|
315
358
|
|
|
316
|
-
#
|
|
317
|
-
#
|
|
359
|
+
# Depth relative to the subtree root, or nil when the node
|
|
360
|
+
# lies outside it (traverse follows the document chain since
|
|
361
|
+
# leptris 1.9.28, so epilog siblings can appear in the
|
|
362
|
+
# stream). Binding wrappers are address-stable and #parent is
|
|
363
|
+
# memoized, so each edge resolves once through the
|
|
318
364
|
# identity-keyed memo.
|
|
319
|
-
def
|
|
320
|
-
memo[node] ||=
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
365
|
+
def material_depth_in_subtree(node, root, memo)
|
|
366
|
+
memo[node] ||= if node.equal?(root)
|
|
367
|
+
0
|
|
368
|
+
else
|
|
369
|
+
parent = node.parent
|
|
370
|
+
if parent.nil? || parent.is_a?(::Leptris::XML::Document)
|
|
371
|
+
nil
|
|
372
|
+
else
|
|
373
|
+
|
|
374
|
+
parent_depth = material_depth_in_subtree(parent, root, memo)
|
|
375
|
+
parent_depth.nil? ? nil : parent_depth + 1
|
|
376
|
+
|
|
377
|
+
end
|
|
378
|
+
end
|
|
324
379
|
end
|
|
325
380
|
|
|
326
381
|
def node_type(node)
|
|
@@ -1025,7 +1080,8 @@ module Moxml
|
|
|
1025
1080
|
dt = doc.doctype
|
|
1026
1081
|
return nil unless dt
|
|
1027
1082
|
|
|
1028
|
-
|
|
1083
|
+
subset = dt.internal_subset if dt.class.method_defined?(:internal_subset)
|
|
1084
|
+
XmlEmitter.doctype_xml(dt.root_name, dt.public_id, dt.system_id, subset)
|
|
1029
1085
|
end
|
|
1030
1086
|
|
|
1031
1087
|
def sax_parse(xml, handler)
|
data/lib/moxml/document.rb
CHANGED
|
@@ -31,6 +31,17 @@ module Moxml
|
|
|
31
31
|
root&.materialize(&block)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# Deterministically release the adapter's native memory for this
|
|
35
|
+
# document (issue #134) — batch workloads parsing thousands of
|
|
36
|
+
# documents otherwise hold C trees until GC finalizers run.
|
|
37
|
+
# GC-managed engines no-op. Further access raises the engine's
|
|
38
|
+
# use-after-free error; ordinary garbage-collected documents keep
|
|
39
|
+
# working via the finalizer either way.
|
|
40
|
+
def free
|
|
41
|
+
adapter.free_document(@native)
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
34
45
|
def create_element(name)
|
|
35
46
|
Element.new(adapter.create_element(name, owner_doc: @native), context)
|
|
36
47
|
end
|
data/lib/moxml/version.rb
CHANGED
data/lib/moxml/xml_emitter.rb
CHANGED
|
@@ -56,8 +56,9 @@ module Moxml
|
|
|
56
56
|
output << "?>"
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
# @return [String] a DOCTYPE with its external identifier
|
|
60
|
-
|
|
59
|
+
# @return [String] a DOCTYPE with its external identifier and
|
|
60
|
+
# optional internal subset
|
|
61
|
+
def doctype_xml(name, public_id, system_id, internal_subset = nil)
|
|
61
62
|
output = "<!DOCTYPE #{name}"
|
|
62
63
|
if public_id && !public_id.to_s.empty?
|
|
63
64
|
output << %( PUBLIC "#{public_id}")
|
|
@@ -65,6 +66,9 @@ module Moxml
|
|
|
65
66
|
elsif system_id && !system_id.to_s.empty?
|
|
66
67
|
output << %( SYSTEM "#{system_id}")
|
|
67
68
|
end
|
|
69
|
+
if internal_subset && !internal_subset.empty?
|
|
70
|
+
output << " [" << internal_subset << "]"
|
|
71
|
+
end
|
|
68
72
|
output << ">"
|
|
69
73
|
end
|
|
70
74
|
end
|
|
@@ -153,6 +153,31 @@ RSpec.describe Moxml::Adapter::Leptris do
|
|
|
153
153
|
end
|
|
154
154
|
end
|
|
155
155
|
|
|
156
|
+
describe "DTD ATTLIST defaults" do
|
|
157
|
+
# libleptris 1.9.8: plain parse excludes ATTLIST defaults,
|
|
158
|
+
# matching libxml2/Nokogiri/REXML; dtdattr: true opts in.
|
|
159
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
160
|
+
let(:dtd_xml) do
|
|
161
|
+
%(<?xml version="1.0"?><!DOCTYPE doc [<!ATTLIST e9 attr CDATA "default">]><doc><e9/></doc>)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it "excludes ATTLIST defaults on plain parse" do
|
|
165
|
+
skip "requires leptris with no-DTDATTR semantics" unless Moxml::Adapter::Leptris::DTDATTR_SUPPORTED
|
|
166
|
+
|
|
167
|
+
doc = ctx.parse(dtd_xml)
|
|
168
|
+
expect(doc.at_xpath("//e9")["attr"]).to be_nil
|
|
169
|
+
expect(doc.to_xml).not_to include("attr=")
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
it "materializes ATTLIST defaults with dtdattr: true" do
|
|
173
|
+
skip "requires leptris with ParseOptions::DTDATTR" unless Moxml::Adapter::Leptris::DTDATTR_SUPPORTED
|
|
174
|
+
|
|
175
|
+
doc = ctx.parse(dtd_xml, dtdattr: true)
|
|
176
|
+
expect(doc.at_xpath("//e9")["attr"]).to eq("default")
|
|
177
|
+
expect(doc.to_xml).to include(%(attr="default"))
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
156
181
|
describe "entity-marker tracking" do
|
|
157
182
|
let(:ctx) { Moxml.new(:leptris) }
|
|
158
183
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "Document#free" do
|
|
6
|
+
let(:xml) { '<r><a x="1">t</a></r>' }
|
|
7
|
+
|
|
8
|
+
it "frees native memory deterministically (issue #134)" do
|
|
9
|
+
doc = Moxml.new(:leptris).parse(xml)
|
|
10
|
+
expect(doc.root.name).to eq("r")
|
|
11
|
+
|
|
12
|
+
expect(doc.free).to be_nil
|
|
13
|
+
expect { doc.root }.to raise_error(Leptris::XML::UseAfterFreeError)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "is a no-op on GC-managed engines" do
|
|
17
|
+
doc = Moxml.new(:nokogiri).parse(xml)
|
|
18
|
+
|
|
19
|
+
expect(doc.free).to be_nil
|
|
20
|
+
expect(doc.root.name).to eq("r")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -60,6 +60,17 @@ RSpec.describe Moxml::Materializer do
|
|
|
60
60
|
)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
it "scopes the document stream to the root subtree (issue #140)" do
|
|
64
|
+
scoped = %(<?xml version="1.0"?><!-- prolog c --><?pi b?><root>t</root><!-- epilog c --><?pi2 a?>)
|
|
65
|
+
leptris = Moxml.new(:leptris).materialize(scoped).to_a
|
|
66
|
+
nokogiri = Moxml.new(:nokogiri).materialize(scoped).to_a
|
|
67
|
+
|
|
68
|
+
expect(leptris).to eq(nokogiri)
|
|
69
|
+
expect(leptris.map { |r| [r[:kind], r[:depth]] }).to eq(
|
|
70
|
+
[[:text, 1], [:element, 0]],
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
63
74
|
it "materializes from any element subtree" do
|
|
64
75
|
doc = Moxml.new(:nokogiri).parse(xml)
|
|
65
76
|
title = doc.at_xpath("//*[local-name()='title']")
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: moxml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -447,6 +447,7 @@ files:
|
|
|
447
447
|
- spec/moxml/declaration_spec.rb
|
|
448
448
|
- spec/moxml/doctype_spec.rb
|
|
449
449
|
- spec/moxml/document_builder_spec.rb
|
|
450
|
+
- spec/moxml/document_free_spec.rb
|
|
450
451
|
- spec/moxml/document_spec.rb
|
|
451
452
|
- spec/moxml/element_spec.rb
|
|
452
453
|
- spec/moxml/entity_preservation_spec.rb
|