moxml 0.5.10 → 0.5.12
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 +85 -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: 7f702d6c51c5df49bdb2bf0ac5189d3fe0716d94093ced7decdb743dcf87fa5d
|
|
4
|
+
data.tar.gz: 56316b016c8865d6290033873477d9dcc92b04e1e16ae77716626398bd976ad3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a1e503460e2b5d645cd4147d4164d2a67c254e8e2174b145fbf4f17dab222d1eae03216b7a35dca18a800684c3c904daf9633140f262195f212d9dd235ccc675
|
|
7
|
+
data.tar.gz: 4cf7957e370339230fc4d184a7fd6b9312bd3f5e8f9e843984ed4e56967e33597d18abef6479de69cde47af781a65703427349cdaff16eaf317cdb39ba8cd135
|
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,16 @@ 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)
|
|
30
|
+
# leptris-ruby 1.9.32 (#89): traverse is subtree-bounded again —
|
|
31
|
+
# earlier 1.9.x walks followed the document chain and swept
|
|
32
|
+
# following siblings, so element materialize had to fall back
|
|
33
|
+
# to the generic wrapper walk.
|
|
34
|
+
TRAVERSE_SUBTREE_BOUNDED =
|
|
35
|
+
Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.32")
|
|
26
36
|
|
|
27
37
|
class << self
|
|
28
38
|
def attachments
|
|
@@ -42,8 +52,14 @@ module Moxml
|
|
|
42
52
|
# readonly: true (issue #133): the binding memoizes reads and
|
|
43
53
|
# refuses mutations — the parse-and-read lifecycle for
|
|
44
54
|
# multi-pass consumers (comparison, diff, signature).
|
|
55
|
+
# dtdattr: true opts into DTD ATTLIST default materialization
|
|
56
|
+
# (off by default since libleptris 1.9.8, matching libxml2).
|
|
45
57
|
native_doc = begin
|
|
46
|
-
::Leptris::XML::Document.parse(
|
|
58
|
+
::Leptris::XML::Document.parse(
|
|
59
|
+
processed,
|
|
60
|
+
readonly: options[:readonly] == true,
|
|
61
|
+
options: dtdattr_parse_options(options),
|
|
62
|
+
)
|
|
47
63
|
rescue ::Leptris::XML::ParseError => e
|
|
48
64
|
# libleptris has no recovery mode that survives unclosed
|
|
49
65
|
# tags; non-strict callers get an empty document, matching
|
|
@@ -61,6 +77,30 @@ module Moxml
|
|
|
61
77
|
doc
|
|
62
78
|
end
|
|
63
79
|
|
|
80
|
+
# nil when DTDATTR is unsupported or not requested — the
|
|
81
|
+
# binding treats a nil options hash as plain defaults.
|
|
82
|
+
def dtdattr_parse_options(options)
|
|
83
|
+
return nil unless DTDATTR_SUPPORTED && options[:dtdattr] == true
|
|
84
|
+
|
|
85
|
+
::Leptris::XML::ParseOptions.dtdattr
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Issue #134: deterministic release of the C tree. The binding
|
|
89
|
+
# clears its wrapper cache and raises UseAfterFreeError on
|
|
90
|
+
# later access; moxml-side attachments for the document are
|
|
91
|
+
# swept too (the context wrapper identity map self-cleans via
|
|
92
|
+
# its size valve).
|
|
93
|
+
DOCUMENT_ATTACHMENT_KEYS = %i[
|
|
94
|
+
entity_markers doc_pi_nodes declaration doctype
|
|
95
|
+
had_source_declaration document_text
|
|
96
|
+
].freeze
|
|
97
|
+
|
|
98
|
+
def free_document(native)
|
|
99
|
+
DOCUMENT_ATTACHMENT_KEYS.each { |key| attachments.delete(native, key) }
|
|
100
|
+
native.free
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
64
104
|
def create_document(_native_doc = nil)
|
|
65
105
|
::Leptris::XML::Document.create
|
|
66
106
|
end
|
|
@@ -248,24 +288,37 @@ module Moxml
|
|
|
248
288
|
# ER expansion); the bulk path has no marker handling.
|
|
249
289
|
return nil if doc.nil? || attachments.get(doc, :entity_markers)
|
|
250
290
|
|
|
251
|
-
|
|
291
|
+
# On bindings whose traverse follows the document chain
|
|
292
|
+
# (leptris 1.9.28–1.9.31), element materialize falls back to
|
|
293
|
+
# the generic wrapper walk — only document materialization
|
|
294
|
+
# can filter safely there (issue #140).
|
|
295
|
+
root = if native.is_a?(::Leptris::XML::Document)
|
|
296
|
+
doc.root
|
|
297
|
+
else
|
|
298
|
+
return nil unless TRAVERSE_SUBTREE_BOUNDED
|
|
299
|
+
|
|
300
|
+
native
|
|
301
|
+
end
|
|
252
302
|
return nil if root.nil?
|
|
253
303
|
|
|
254
304
|
depth_memo = {}.compare_by_identity
|
|
255
305
|
root.traverse do |node|
|
|
306
|
+
depth = material_depth_in_subtree(node, root, depth_memo)
|
|
307
|
+
next if depth.nil?
|
|
308
|
+
|
|
256
309
|
record = case node
|
|
257
310
|
when ::Leptris::XML::Element
|
|
258
|
-
element_material_record(node,
|
|
311
|
+
element_material_record(node, depth)
|
|
259
312
|
when ::Leptris::XML::CDATA
|
|
260
313
|
# CDATA < Text in the binding: this arm must come
|
|
261
314
|
# first or CDATA content reports as text.
|
|
262
|
-
text_material_record(:cdata, node.content,
|
|
315
|
+
text_material_record(:cdata, node.content, depth)
|
|
263
316
|
when ::Leptris::XML::Text
|
|
264
|
-
text_material_record(:text, node.content,
|
|
317
|
+
text_material_record(:text, node.content, depth)
|
|
265
318
|
when ::Leptris::XML::Comment
|
|
266
|
-
text_material_record(:comment, node.content,
|
|
319
|
+
text_material_record(:comment, node.content, depth)
|
|
267
320
|
when ::Leptris::XML::ProcessingInstruction
|
|
268
|
-
text_material_record(:processing_instruction, node.content,
|
|
321
|
+
text_material_record(:processing_instruction, node.content, depth)
|
|
269
322
|
.merge(qname: node.target)
|
|
270
323
|
end
|
|
271
324
|
yield(record) if record
|
|
@@ -273,7 +326,7 @@ module Moxml
|
|
|
273
326
|
true
|
|
274
327
|
end
|
|
275
328
|
|
|
276
|
-
def element_material_record(node,
|
|
329
|
+
def element_material_record(node, depth)
|
|
277
330
|
attributes = node.each_attribute.map do |attr|
|
|
278
331
|
# Local name + separate prefix, matching the generic
|
|
279
332
|
# path's resolver semantics (moxml's canonical shape).
|
|
@@ -296,11 +349,11 @@ module Moxml
|
|
|
296
349
|
end,
|
|
297
350
|
attributes: attributes,
|
|
298
351
|
text: nil,
|
|
299
|
-
depth:
|
|
352
|
+
depth: depth,
|
|
300
353
|
}
|
|
301
354
|
end
|
|
302
355
|
|
|
303
|
-
def text_material_record(kind, text,
|
|
356
|
+
def text_material_record(kind, text, depth)
|
|
304
357
|
{
|
|
305
358
|
kind: kind,
|
|
306
359
|
qname: nil,
|
|
@@ -309,18 +362,30 @@ module Moxml
|
|
|
309
362
|
namespaces: Materializer::EMPTY_ATTRIBUTES,
|
|
310
363
|
attributes: Materializer::EMPTY_ATTRIBUTES,
|
|
311
364
|
text: text,
|
|
312
|
-
depth:
|
|
365
|
+
depth: depth,
|
|
313
366
|
}
|
|
314
367
|
end
|
|
315
368
|
|
|
316
|
-
#
|
|
317
|
-
#
|
|
369
|
+
# Depth relative to the subtree root, or nil when the node
|
|
370
|
+
# lies outside it (traverse follows the document chain since
|
|
371
|
+
# leptris 1.9.28, so epilog siblings can appear in the
|
|
372
|
+
# stream). Binding wrappers are address-stable and #parent is
|
|
373
|
+
# memoized, so each edge resolves once through the
|
|
318
374
|
# identity-keyed memo.
|
|
319
|
-
def
|
|
320
|
-
memo[node] ||=
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
375
|
+
def material_depth_in_subtree(node, root, memo)
|
|
376
|
+
memo[node] ||= if node.equal?(root)
|
|
377
|
+
0
|
|
378
|
+
else
|
|
379
|
+
parent = node.parent
|
|
380
|
+
if parent.nil? || parent.is_a?(::Leptris::XML::Document)
|
|
381
|
+
nil
|
|
382
|
+
else
|
|
383
|
+
|
|
384
|
+
parent_depth = material_depth_in_subtree(parent, root, memo)
|
|
385
|
+
parent_depth.nil? ? nil : parent_depth + 1
|
|
386
|
+
|
|
387
|
+
end
|
|
388
|
+
end
|
|
324
389
|
end
|
|
325
390
|
|
|
326
391
|
def node_type(node)
|
|
@@ -1025,7 +1090,8 @@ module Moxml
|
|
|
1025
1090
|
dt = doc.doctype
|
|
1026
1091
|
return nil unless dt
|
|
1027
1092
|
|
|
1028
|
-
|
|
1093
|
+
subset = dt.internal_subset if dt.class.method_defined?(:internal_subset)
|
|
1094
|
+
XmlEmitter.doctype_xml(dt.root_name, dt.public_id, dt.system_id, subset)
|
|
1029
1095
|
end
|
|
1030
1096
|
|
|
1031
1097
|
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.12
|
|
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
|