moxml 0.5.94 → 0.5.96

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: e54a93041f955c95affa2211704839bfaf47dafc0908cd73b58012946ffe99f1
4
- data.tar.gz: 4d28d6376f61ae7441e13075ac5e9433bd18b6c393cf95afe363a639a4b8d251
3
+ metadata.gz: 011415dfeb5b083b1cb88c8a7b4619ed3cc896c51b78500fe849c02bbab055a4
4
+ data.tar.gz: a8b99c779efcca214047df53e1891b07961f0d3ab154e3f3538e83bfdbfb64f5
5
5
  SHA512:
6
- metadata.gz: 3e13a34f69a9e91b18d7fc9cef1e30c40ced24c8c73b322459ecba15af568439b002db8eec51097af2188571d026bfae7861646cc33077127c1fbd570b179863
7
- data.tar.gz: a873c43e7ee3ccffc1997bf103263b7848d8ed17c61e25322a8d550bdf62a46d2f1e9e4a93f6e2832484a6df9d93350fd53d833a0b48ce04e1247583fc447d50
6
+ metadata.gz: d1eb8163f7e725c42bba78d058abcece733204a414df60278d4fbfc477808160e8800eaa9fd5de5c011c1ce849d3f9e630eb667ebe15808cc6a614120a92fcd5
7
+ data.tar.gz: 5d4108fc8881b6c8073dd8de95c0023b3aaeca379e258fa5b8345a6c6016f0ee814b9d2c6b047ea1d91c724391e4fc8b6127090781c33665dcba59c536f8bb52
@@ -80,6 +80,24 @@ adapter = element.context.config.adapter
80
80
  element = Moxml::Element.new(native_node, context)
81
81
  ----
82
82
 
83
+ === Parse bookkeeping opt-outs (leptris)
84
+
85
+ Data-pipeline parses that never read diagnostics or source positions can
86
+ skip that bookkeeping. Both are opt-in per call; the default parse is
87
+ byte-identical either way.
88
+
89
+ [source,ruby]
90
+ ----
91
+ # Duplicate attributes admit silently: queries answer first-wins, but
92
+ # the duplicate stays in the tree, so the serialized form can carry it.
93
+ doc = Moxml.new.parse(xml, skip_dup_detection: true)
94
+
95
+ # Source columns degrade; lines still resolve.
96
+ doc = Moxml.new.parse(xml, skip_source_positions: true)
97
+ ----
98
+
99
+ Without the flags, a duplicate attribute records a recover diagnostic in
100
+ `doc.parse_diagnostics` and parses with the first value.
83
101
 
84
102
  See also:
85
103
 
@@ -856,6 +856,17 @@ module Moxml
856
856
  # Nokogiri semantics: blanks kept, no ATTLIST defaults).
857
857
  # noblanks forwards only once the engine flag is libxml2-safe
858
858
  # (see ENGINE_NOBLANKS_SAFE); otherwise it is moxml-side.
859
+ #
860
+ # The SKIP_ bookkeeping opt-outs (leptris-ruby#352, engine
861
+ # 1.9.272): duplicate attributes admit silently (first wins,
862
+ # no recover diagnostic) and source columns degrade (lines
863
+ # still resolve). Caller opt-in only — never defaults. The
864
+ # constant probe is the dependency-boundary check (the gem
865
+ # is zero-dep; user-resolved binding versions predate them).
866
+ NATIVE_PARSE_SKIPS =
867
+ ::Leptris::XML::ParseOptions.const_defined?(:SKIP_DUP_DETECTION) &&
868
+ ::Leptris::XML::ParseOptions.const_defined?(:SKIP_SOURCE_POSITIONS)
869
+
859
870
  def parse_flags(options, context: nil)
860
871
  flags = 0
861
872
  flags |= ::Leptris::XML::ParseOptions::DTDATTR if options[:dtdattr] == true
@@ -863,6 +874,11 @@ module Moxml
863
874
  (context&.config&.entity_mode == :keep ||
864
875
  options[:keep_entity_refs] == true)
865
876
  flags |= ::Leptris::XML::ParseOptions::NOBLANKS if options[:noblanks] == true && ENGINE_NOBLANKS_SAFE
877
+ if NATIVE_PARSE_SKIPS
878
+ po = ::Leptris::XML::ParseOptions
879
+ flags |= po::SKIP_DUP_DETECTION if options[:skip_dup_detection] == true
880
+ flags |= po::SKIP_SOURCE_POSITIONS if options[:skip_source_positions] == true
881
+ end
866
882
  flags.zero? ? nil : ::Leptris::XML::ParseOptions.new(flags)
867
883
  end
868
884
 
@@ -59,6 +59,7 @@ module Moxml
59
59
  # (Document/C14n), which is circular while THIS adapter file
60
60
  # is still loading.
61
61
  def native_inclusive10(native)
62
+ return nil if native.nil?
62
63
  return nil unless native_c14n_byte_safe?
63
64
 
64
65
  native.canonicalize(::Nokogiri::XML::XML_C14N_1_0)
@@ -25,12 +25,12 @@ module Moxml
25
25
  def parse(xml, options = {}, _context = nil)
26
26
  xml = "" if xml.nil?
27
27
 
28
- # Handle frozen strings by creating a mutable copy
29
- processed_xml = if xml.frozen?
30
- xml.dup.force_encoding("UTF-8").encode("UTF-8")
31
- else
32
- xml.force_encoding("UTF-8").encode("UTF-8")
33
- end
28
+ # Transcode from the input's own encoding tag; encode returns
29
+ # a fresh string (or self, already UTF-8) — the caller's
30
+ # buffer is never retagged in place. The old non-frozen
31
+ # branch force_encoding!-mutated the CALLER's string,
32
+ # corrupting it (invalid UTF-8 tag) for every later parse.
33
+ processed_xml = xml.encode("UTF-8")
34
34
 
35
35
  # Preprocess entities to avoid double-escaping on output
36
36
  processed_xml = preprocess_entities(processed_xml)
data/lib/moxml/entity.rb CHANGED
@@ -112,6 +112,7 @@ module Moxml
112
112
  else
113
113
  xml.encode("UTF-8")
114
114
  end
115
+ str = rewrite_declared_encoding(str)
115
116
  # Fast path: no `&` means no entity references to mark — skip
116
117
  # the regex scan and string allocation entirely. The vast
117
118
  # majority of XML payloads contain no entity references.
@@ -139,6 +140,24 @@ module Moxml
139
140
  preprocess_with_marker_flag(xml)[0]
140
141
  end
141
142
 
143
+ # Engines honor the DECLARED encoding and re-decode the bytes they
144
+ # are handed — moxml always feeds UTF-8 (see the normalization
145
+ # above), so a declaration naming another encoding must be
146
+ # rewritten to match, or the already-transcoded bytes decode a
147
+ # second time into mojibake ("café" -> "café"). The anchored match
148
+ # fails on the first two bytes for documents without a declaration.
149
+ DECLARED_ENCODING_RE = /\A(<\?xml[^>]*?encoding=)['"][^'"]*['"]/
150
+ private_constant :DECLARED_ENCODING_RE
151
+
152
+ def rewrite_declared_encoding(str)
153
+ # The passthrough contract (Entity spec: plain text returns the
154
+ # SAME object) requires no allocation when no declaration names
155
+ # another encoding — both gates are allocation-free.
156
+ return str unless str.start_with?("<?xml") && str.include?("encoding=")
157
+
158
+ str.sub(DECLARED_ENCODING_RE) { "#{Regexp.last_match(1)}\"UTF-8\"" }
159
+ end
160
+
142
161
  # Resolve numeric (&#NN; / &#xNN;) and the five standard named
143
162
  # (&amp; &lt; &gt; &quot; &apos;) XML entity references in one
144
163
  # single pass. The resulting characters are data; no further
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.94"
4
+ VERSION = "0.5.96"
5
5
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ DUP_XML = %(<r><e id="1">text</e><e id="2" id="3"/></r>)
6
+ POS_XML = %(<r><e id="1">text</e></r>)
7
+
8
+ RSpec.describe "Moxml leptris parse bookkeeping opt-outs" do
9
+ # leptris-ruby#352 (binding 1.9.273.1, engine 1.9.272 Door A):
10
+ # SKIP_DUP_DETECTION admits duplicate attributes silently (first
11
+ # wins, no recover diagnostic); SKIP_SOURCE_POSITIONS degrades
12
+ # source columns (lines still resolve). Caller opt-in only.
13
+ let(:ctx) { Moxml.new(:leptris) }
14
+ let(:adapter) { ctx.config.adapter }
15
+
16
+ before do
17
+ skip "binding without the SKIP_ parse flags" unless
18
+ Leptris::XML::ParseOptions.const_defined?(:SKIP_DUP_DETECTION)
19
+ end
20
+
21
+ it "drops the duplicate-attribute diagnostic when requested" do
22
+ doc = ctx.parse(DUP_XML, skip_dup_detection: true)
23
+ expect(adapter.parse_diagnostics(doc.native)).to eq([])
24
+ end
25
+
26
+ it "keeps the duplicate-attribute diagnostic by default" do
27
+ doc = ctx.parse(DUP_XML)
28
+ expect(adapter.parse_diagnostics(doc.native).map { |d| d[:kind] }).to include(:recover)
29
+ end
30
+
31
+ it "first attribute wins either way" do
32
+ expect(ctx.parse(DUP_XML, skip_dup_detection: true).root.children[1][:id])
33
+ .to eq(ctx.parse(DUP_XML).root.children[1][:id])
34
+ end
35
+
36
+ it "keeps lines but degrades columns for skipped positions" do
37
+ full = ctx.parse(POS_XML).root.children[0].source_position
38
+ skipped = ctx.parse(POS_XML, skip_source_positions: true)
39
+ .root.children[0].source_position
40
+ expect(skipped[:line]).to eq(full[:line])
41
+ expect(skipped[:col_start]).to be < full[:col_start]
42
+ end
43
+
44
+ it "keeps the duplicate in the tree — queries still answer first-wins" do
45
+ # The engine admits the duplicate silently: reads see the first
46
+ # value, but the second attribute stays in the tree, so the
47
+ # serialized form can carry it. Callers opting in trade the
48
+ # well-formed-output guarantee for the bookkeeping speed.
49
+ fast = ctx.parse(DUP_XML, skip_dup_detection: true)
50
+ expect(fast.root.children[1][:id]).to eq("2")
51
+ expect(fast.to_xml).to include('id="2" id="3"')
52
+ end
53
+
54
+ it "still raises on fatal errors with skips on" do
55
+ expect { ctx.parse("<r><e></r>", skip_dup_detection: true) }
56
+ .to raise_error(Moxml::ParseError)
57
+ end
58
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "moxml/c14n"
5
+
6
+ # TODO.c14n/13: encoding edge cases. moxml normalizes every parse to
7
+ # UTF-8 bytes and rewrites the declaration to match — engines honor
8
+ # the DECLARED encoding and would otherwise re-decode the already
9
+ # transcoded bytes into mojibake.
10
+ CAF_E_UTF8 = [60, 114, 62, 99, 97, 102, 195, 169, 60, 47, 114, 62].freeze
11
+ LATIN1_XML = (+"<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?><r>caf\xE9</r>").force_encoding("ISO-8859-1")
12
+ BOM_XML = (+"\xEF\xBB\xBF<r/>").force_encoding("UTF-8")
13
+ NFC_XML = (+"<r>caf\xC3\xA9</r>").force_encoding("UTF-8")
14
+
15
+ # oga's parse layer leaks the declaration as a PI node and the BOM as
16
+ # text (pre-existing, documented in the W3C examples spec).
17
+ OGA_LEAK = %i[oga].freeze
18
+
19
+ RSpec.describe "Moxml character encoding at the parse boundary" do
20
+ Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
21
+ context "with the #{adapter_name} adapter" do
22
+ let(:ctx) { Moxml.new(adapter_name) }
23
+
24
+ unless OGA_LEAK.include?(adapter_name)
25
+ it "transcodes declared Latin-1 to UTF-8 bytes" do
26
+ doc = ctx.parse(LATIN1_XML.dup)
27
+ expect(Moxml::C14n.canonicalize(doc).bytes).to eq(CAF_E_UTF8)
28
+ end
29
+
30
+ it "strips a leading UTF-8 BOM" do
31
+ doc = ctx.parse(BOM_XML.dup)
32
+ expect(Moxml::C14n.canonicalize(doc)).to eq("<r></r>")
33
+ end
34
+ end
35
+
36
+ it "passes NFC content through without normalization" do
37
+ doc = ctx.parse(NFC_XML.dup)
38
+ expect(Moxml::C14n.canonicalize(doc).bytes).to eq(CAF_E_UTF8)
39
+ end
40
+
41
+ it "does not mutate the caller's input string" do
42
+ input = LATIN1_XML.dup
43
+ expect(input).not_to be_frozen
44
+ ctx.parse(input)
45
+ expect(input.encoding).to eq(Encoding::ISO_8859_1)
46
+ expect(input.valid_encoding?).to be(true)
47
+ expect(input.bytes).to eq(LATIN1_XML.bytes)
48
+ end
49
+
50
+ it "serializes transcoded documents as UTF-8 declarations" do
51
+ doc = ctx.parse(LATIN1_XML.dup)
52
+ expect(doc.to_xml).to include("UTF-8")
53
+ expect(doc.to_xml).not_to include("ISO-8859-1")
54
+ end
55
+ end
56
+ end
57
+ end
@@ -2,6 +2,10 @@
2
2
 
3
3
  require "spec_helper"
4
4
 
5
+ # The per-item target class for the typed plan: Struct gives the
6
+ # accessors StructPlan assigns the cast slots to.
7
+ TypedItem = Struct.new(:price, :qty, :ok, :label)
8
+
5
9
  # Typed plan scalars (leptris plan ABI, #1269a consumer face):
6
10
  # [slot, type] declarations cast in C — Integer/Float/TrueClass/
7
11
  # FalseClass members with no Ruby String materialized; unparseable
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.94
4
+ version: 0.5.96
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -438,6 +438,7 @@ files:
438
438
  - spec/moxml/adapter/entity_restoration_spec.rb
439
439
  - spec/moxml/adapter/headed_ox_spec.rb
440
440
  - spec/moxml/adapter/leptris_doc_parts_spec.rb
441
+ - spec/moxml/adapter/leptris_parse_skips_spec.rb
441
442
  - spec/moxml/adapter/leptris_record_driver_spec.rb
442
443
  - spec/moxml/adapter/leptris_sax_dup_attr_spec.rb
443
444
  - spec/moxml/adapter/leptris_sax_records_spec.rb
@@ -460,6 +461,7 @@ files:
460
461
  - spec/moxml/c14n/adapter_delegation_spec.rb
461
462
  - spec/moxml/c14n/api_spec.rb
462
463
  - spec/moxml/c14n/c14n_spec.rb
464
+ - spec/moxml/c14n/character_encoding_spec.rb
463
465
  - spec/moxml/c14n/comments_pis_spec.rb
464
466
  - spec/moxml/c14n/inclusive10_spec.rb
465
467
  - spec/moxml/c14n/namespace_edge_cases_spec.rb