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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0307f3c9e6060b1ca344c69633788790dc899121f213be74870be3cd2c2600ac
4
- data.tar.gz: 22135905e79bcb1099a04809bedb69c734b1893ee4fa4749761427641bfcf9b2
3
+ metadata.gz: 7f702d6c51c5df49bdb2bf0ac5189d3fe0716d94093ced7decdb743dcf87fa5d
4
+ data.tar.gz: 56316b016c8865d6290033873477d9dcc92b04e1e16ae77716626398bd976ad3
5
5
  SHA512:
6
- metadata.gz: 50413c5cddd8680fc5c6cf4bd67dcf75e54e0cbf202b45f1ff5b985648ff948286b92c0a4efc9ef725d9fd0af677e0239eefb7fec37f27e43ece9795240a5db7
7
- data.tar.gz: 696949b3fb51358b4841e2a2fdeb68824659a7ffefdbb6ac4a2006758006d649ddb64127728cd2b5b653cc54dd888e14566d012d5d6b6c382ce012f3b0ea0112
6
+ metadata.gz: a1e503460e2b5d645cd4147d4164d2a67c254e8e2174b145fbf4f17dab222d1eae03216b7a35dca18a800684c3c904daf9633140f262195f212d9dd235ccc675
7
+ data.tar.gz: 4cf7957e370339230fc4d184a7fd6b9312bd3f5e8f9e843984ed4e56967e33597d18abef6479de69cde47af781a65703427349cdaff16eaf317cdb39ba8cd135
@@ -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(processed, readonly: options[:readonly] == true)
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
- root = native.is_a?(::Leptris::XML::Document) ? native.root : native
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, depth_memo)
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, node, depth_memo)
315
+ text_material_record(:cdata, node.content, depth)
263
316
  when ::Leptris::XML::Text
264
- text_material_record(:text, node.content, node, depth_memo)
317
+ text_material_record(:text, node.content, depth)
265
318
  when ::Leptris::XML::Comment
266
- text_material_record(:comment, node.content, node, depth_memo)
319
+ text_material_record(:comment, node.content, depth)
267
320
  when ::Leptris::XML::ProcessingInstruction
268
- text_material_record(:processing_instruction, node.content, node, depth_memo)
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, depth_memo)
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: material_depth(node, depth_memo),
352
+ depth: depth,
300
353
  }
301
354
  end
302
355
 
303
- def text_material_record(kind, text, node, depth_memo)
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: material_depth(node, depth_memo),
365
+ depth: depth,
313
366
  }
314
367
  end
315
368
 
316
- # Binding wrappers are address-stable and #parent is memoized,
317
- # so each edge is fetched once; depths resolve through the
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 material_depth(node, memo)
320
- memo[node] ||= begin
321
- parent = node.parent
322
- parent.nil? || parent.is_a?(::Leptris::XML::Document) ? 0 : material_depth(parent, memo) + 1
323
- end
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
- XmlEmitter.doctype_xml(dt.root_name, dt.public_id, dt.system_id)
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)
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.10"
4
+ VERSION = "0.5.12"
5
5
  end
@@ -56,8 +56,9 @@ module Moxml
56
56
  output << "?>"
57
57
  end
58
58
 
59
- # @return [String] a DOCTYPE with its external identifier
60
- def doctype_xml(name, public_id, system_id)
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.10
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