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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0307f3c9e6060b1ca344c69633788790dc899121f213be74870be3cd2c2600ac
4
- data.tar.gz: 22135905e79bcb1099a04809bedb69c734b1893ee4fa4749761427641bfcf9b2
3
+ metadata.gz: 15a0dd834bc1c9637a8d2284173d4994b1717b1bd2d8d9461686a56c679942e6
4
+ data.tar.gz: 4635e66b998d8fa371dd1617d0f960fc31b8a886627095061f945aa8b29cae41
5
5
  SHA512:
6
- metadata.gz: 50413c5cddd8680fc5c6cf4bd67dcf75e54e0cbf202b45f1ff5b985648ff948286b92c0a4efc9ef725d9fd0af677e0239eefb7fec37f27e43ece9795240a5db7
7
- data.tar.gz: 696949b3fb51358b4841e2a2fdeb68824659a7ffefdbb6ac4a2006758006d649ddb64127728cd2b5b653cc54dd888e14566d012d5d6b6c382ce012f3b0ea0112
6
+ metadata.gz: 6eace14b6adb980bc4c14c6f628b231961f47fab55e05a0bf809591b4203a6581f0cb333dc358ec3bebeb1c239f7ecd65fc68ca34ee70fb1bf71fbf8c00f278c
7
+ data.tar.gz: f03e39569fd299ac6a7e8dc266d97a5499fea6f89b166f64e46944649286df8a6ceb4fd4732b6a67cac160f2318b0ed4c79fc8d782184b3a090b9c3feb65be1e
@@ -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(processed, readonly: options[:readonly] == true)
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
- root = native.is_a?(::Leptris::XML::Document) ? native.root : native
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, depth_memo)
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, node, depth_memo)
305
+ text_material_record(:cdata, node.content, depth)
263
306
  when ::Leptris::XML::Text
264
- text_material_record(:text, node.content, node, depth_memo)
307
+ text_material_record(:text, node.content, depth)
265
308
  when ::Leptris::XML::Comment
266
- text_material_record(:comment, node.content, node, depth_memo)
309
+ text_material_record(:comment, node.content, depth)
267
310
  when ::Leptris::XML::ProcessingInstruction
268
- text_material_record(:processing_instruction, node.content, node, depth_memo)
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, depth_memo)
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: material_depth(node, depth_memo),
342
+ depth: depth,
300
343
  }
301
344
  end
302
345
 
303
- def text_material_record(kind, text, node, depth_memo)
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: material_depth(node, depth_memo),
355
+ depth: depth,
313
356
  }
314
357
  end
315
358
 
316
- # Binding wrappers are address-stable and #parent is memoized,
317
- # so each edge is fetched once; depths resolve through the
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 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
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
- XmlEmitter.doctype_xml(dt.root_name, dt.public_id, dt.system_id)
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)
@@ -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.11"
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.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