moxml 0.5.95 → 0.5.97
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/Gemfile +1 -1
- data/lib/moxml/adapter/ox.rb +17 -6
- data/lib/moxml/adapter/rexml.rb +6 -6
- data/lib/moxml/entity.rb +19 -0
- data/lib/moxml/version.rb +1 -1
- data/spec/moxml/adapter/ox_spec.rb +30 -0
- data/spec/moxml/c14n/character_encoding_spec.rb +57 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ec3d9011525010958b918f5c9665d47f54c202c96d0e3917c65d90ced667233b
|
|
4
|
+
data.tar.gz: 4d7d535d9960f935a6419006c586db0b49981de555cfa362eee641c548c51c1b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8e39fcd9157a62fc686befcae1e4ca077e757c6b9bfc399b590f02377199be55e45f281f9cd50dcac5eaff9d32abd8aa2d40de7fc3250b3d02dd69aadd1dfefe
|
|
7
|
+
data.tar.gz: d947c13ae056a34a65d91d27648b54b89d4bca1c0d7a56712a77c560e7ef89cf798538af3871298ddcf5146379cc57442a7f02d359013228057a707765003f11
|
data/Gemfile
CHANGED
data/lib/moxml/adapter/ox.rb
CHANGED
|
@@ -779,41 +779,52 @@ module Moxml
|
|
|
779
779
|
if node.is_a?(::Ox::Element) && (doc = document(node))
|
|
780
780
|
return true if attachments.get(doc, :has_entity_refs)
|
|
781
781
|
return true if attachments.get(doc, :has_cdata_end_markers)
|
|
782
|
+
return true if attachments.get(doc, :has_patched_text)
|
|
782
783
|
return false if attachments.key?(doc, :has_entity_refs) &&
|
|
783
|
-
attachments.key?(doc, :has_cdata_end_markers)
|
|
784
|
+
attachments.key?(doc, :has_cdata_end_markers) &&
|
|
785
|
+
attachments.key?(doc, :has_patched_text)
|
|
784
786
|
|
|
785
787
|
scan_root = doc
|
|
786
788
|
end
|
|
787
789
|
|
|
788
790
|
# One merged tree scan instead of two; flags cache on the
|
|
789
791
|
# document either way.
|
|
790
|
-
has_er, has_cdata = tree_scan_custom_needs(scan_root)
|
|
792
|
+
has_er, has_cdata, has_patched = tree_scan_custom_needs(scan_root)
|
|
791
793
|
if scan_root.is_a?(::Ox::Document)
|
|
792
794
|
attachments.set(scan_root, :has_entity_refs, has_er)
|
|
793
795
|
attachments.set(scan_root, :has_cdata_end_markers, has_cdata)
|
|
796
|
+
attachments.set(scan_root, :has_patched_text, has_patched)
|
|
794
797
|
end
|
|
795
798
|
|
|
796
|
-
has_er || has_cdata
|
|
799
|
+
has_er || has_cdata || has_patched
|
|
797
800
|
end
|
|
798
801
|
|
|
799
|
-
# Single walk collecting
|
|
802
|
+
# Single walk collecting the custom-serialize triggers.
|
|
800
803
|
def tree_scan_custom_needs(node)
|
|
801
804
|
has_er = false
|
|
802
805
|
has_cdata = false
|
|
806
|
+
# Text enumeration hands out CustomizedOx::Text wrappers
|
|
807
|
+
# (node.rb patches String children so they can carry a
|
|
808
|
+
# parent); dup/append of an enumerated wrapper plants the
|
|
809
|
+
# subclass instance in the raw tree, and ::Ox.dump rejects
|
|
810
|
+
# classes it does not know (moxml#301).
|
|
811
|
+
has_patched = false
|
|
803
812
|
stack = [node]
|
|
804
813
|
until stack.empty?
|
|
805
814
|
current = stack.pop
|
|
806
815
|
case current
|
|
807
816
|
when ::Moxml::Adapter::CustomizedOx::EntityReference
|
|
808
817
|
has_er = true
|
|
818
|
+
when ::Moxml::Adapter::CustomizedOx::Text
|
|
819
|
+
has_patched = true
|
|
809
820
|
when ::Ox::CData
|
|
810
821
|
has_cdata = true if current.value&.include?("]]>")
|
|
811
822
|
when ::Ox::Element, ::Ox::Document
|
|
812
823
|
current.nodes&.each { |child| stack << child }
|
|
813
824
|
end
|
|
814
|
-
return [true, has_cdata] if has_er
|
|
825
|
+
return [true, has_cdata, has_patched] if has_er
|
|
815
826
|
end
|
|
816
|
-
[has_er, has_cdata]
|
|
827
|
+
[has_er, has_cdata, has_patched]
|
|
817
828
|
end
|
|
818
829
|
|
|
819
830
|
def has_declaration?(native_doc, _wrapper)
|
data/lib/moxml/adapter/rexml.rb
CHANGED
|
@@ -25,12 +25,12 @@ module Moxml
|
|
|
25
25
|
def parse(xml, options = {}, _context = nil)
|
|
26
26
|
xml = "" if xml.nil?
|
|
27
27
|
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
# (& < > " ') XML entity references in one
|
|
144
163
|
# single pass. The resulting characters are data; no further
|
data/lib/moxml/version.rb
CHANGED
|
@@ -110,4 +110,34 @@ RSpec.describe Moxml::Adapter::Ox do
|
|
|
110
110
|
end
|
|
111
111
|
end
|
|
112
112
|
end
|
|
113
|
+
|
|
114
|
+
# moxml#301: text enumeration hands out CustomizedOx::Text
|
|
115
|
+
# (String children are patched so they can carry a parent); dup or
|
|
116
|
+
# append of an enumerated wrapper plants the subclass instance in
|
|
117
|
+
# the raw tree, and ::Ox.dump rejects classes it does not know.
|
|
118
|
+
# The custom-serialize gate must trip on it (has_patched_text).
|
|
119
|
+
describe "duped and appended text nodes serialize" do
|
|
120
|
+
let(:ctx) { Moxml.new(:ox) }
|
|
121
|
+
|
|
122
|
+
it "round-trips a duped text appended to a parsed tree" do
|
|
123
|
+
doc = ctx.parse(%(<r><a>t</a></r>))
|
|
124
|
+
doc.root.add_child(doc.root.children[0].children[0].dup)
|
|
125
|
+
expect(doc.to_xml).to eq(%(<r><a>t</a>t</r>))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "round-trips an enumerated text moved into a built document" do
|
|
129
|
+
src = ctx.parse(%(<r>x</r>))
|
|
130
|
+
built = ctx.create_document
|
|
131
|
+
root = built.create_element("r")
|
|
132
|
+
built.root = root
|
|
133
|
+
root.add_child(src.root.children[0].dup)
|
|
134
|
+
expect(built.to_xml).to eq("<r>x</r>")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "round-trips a cloned text appended deep" do
|
|
138
|
+
doc = ctx.parse(%(<r><a>keep</a></r>))
|
|
139
|
+
doc.root.children[0].add_child(doc.root.children[0].children[0].clone)
|
|
140
|
+
expect(doc.to_xml).to eq(%(<r><a>keepkeep</a></r>))
|
|
141
|
+
end
|
|
142
|
+
end
|
|
113
143
|
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
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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.97
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
Moxml is a unified XML manipulation library that provides a common API
|
|
@@ -461,6 +461,7 @@ files:
|
|
|
461
461
|
- spec/moxml/c14n/adapter_delegation_spec.rb
|
|
462
462
|
- spec/moxml/c14n/api_spec.rb
|
|
463
463
|
- spec/moxml/c14n/c14n_spec.rb
|
|
464
|
+
- spec/moxml/c14n/character_encoding_spec.rb
|
|
464
465
|
- spec/moxml/c14n/comments_pis_spec.rb
|
|
465
466
|
- spec/moxml/c14n/inclusive10_spec.rb
|
|
466
467
|
- spec/moxml/c14n/namespace_edge_cases_spec.rb
|