moxml 0.5.91 → 0.5.93
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 +6 -0
- data/lib/moxml/adapter/customized_libxml/node.rb +8 -3
- data/lib/moxml/adapter/leptris.rb +44 -2
- data/lib/moxml/adapter/libxml.rb +4 -1
- data/lib/moxml/adapter/nokogiri.rb +29 -0
- data/lib/moxml/adapter/rexml.rb +3 -0
- data/lib/moxml/c14n/data_model.rb +177 -40
- data/lib/moxml/c14n.rb +24 -5
- data/lib/moxml/version.rb +1 -1
- data/spec/moxml/c14n/adapter_delegation_spec.rb +120 -0
- data/spec/moxml/c14n/subset_spec.rb +109 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 92d24efac79874e02cbfa26e20fc8135e2a714020a727896cdf2a460fa7d0508
|
|
4
|
+
data.tar.gz: 91f18637388f6983c2c75c606e95e93f15919b3099fbdc0c8e59ebf9aca93d5e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91cd7d93def6717a7eedf675e871ff7d05d3dfb5378f8ddbe8f839daa68d0d1dbe0e1bdf6bdb939704bb03f673950c9faa858265321e2a209fbcb2042224a3cd
|
|
7
|
+
data.tar.gz: c973d0559eb5fcc42f45dfb824758cdbec8758ec0dc378a4ce4034d918b3b50fcdada1120cd9f18b47c39a42055d4cd810225588dbcf598346c920a2a510e0c7
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -184,6 +184,12 @@ module Moxml
|
|
|
184
184
|
one == other
|
|
185
185
|
end
|
|
186
186
|
|
|
187
|
+
# Owner element native for an attribute native, where the
|
|
188
|
+
# engine exposes it (nil elsewhere).
|
|
189
|
+
def attribute_element(_attribute)
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
|
|
187
193
|
# Backends without a native subtree digest answer nil —
|
|
188
194
|
# the wrapper contract Node#digest gates on (issue #173).
|
|
189
195
|
def digest(*)
|
|
@@ -26,12 +26,17 @@ module Moxml
|
|
|
26
26
|
@native = fresh
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
# Compare wrappers based on their native nodes
|
|
29
|
+
# Compare wrappers based on their native nodes. Either side
|
|
30
|
+
# may arrive double-wrapped (a Customized node stored as
|
|
31
|
+
# another wrapper's native); LibXML's eql? raises on the
|
|
32
|
+
# class mismatch, so unwrap both before comparing.
|
|
30
33
|
def ==(other)
|
|
31
34
|
return false unless other
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
mine = @native.is_a?(CustomizedLibxml::Node) ? @native.native : @native
|
|
37
|
+
theirs = other.is_a?(self.class) ? other.native : other
|
|
38
|
+
theirs = theirs.native while theirs.is_a?(CustomizedLibxml::Node)
|
|
39
|
+
mine == theirs
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
alias eql? ==
|
|
@@ -1140,9 +1140,14 @@ module Moxml
|
|
|
1140
1140
|
(prefix = node.prefix || prefix_part(node.name))
|
|
1141
1141
|
resolved = resolve_prefix_ns(node.element, prefix)
|
|
1142
1142
|
return resolved if resolved
|
|
1143
|
+
# The xml prefix is reserved (Namespaces in XML 1.0 §2.10):
|
|
1144
|
+
# bound to its URI on every element, never in
|
|
1145
|
+
# namespace_definitions, so the scope walk always misses.
|
|
1146
|
+
return implicit_xml_namespace(node) if prefix == "xml"
|
|
1143
1147
|
end
|
|
1144
1148
|
|
|
1145
1149
|
ns = node.namespace
|
|
1150
|
+
return implicit_xml_namespace(node) if reserved_xml_uri?(ns)
|
|
1146
1151
|
return ns unless ns.nil?
|
|
1147
1152
|
|
|
1148
1153
|
# Set-side namespaces (created attributes, renamed elements)
|
|
@@ -1174,6 +1179,30 @@ module Moxml
|
|
|
1174
1179
|
nil
|
|
1175
1180
|
end
|
|
1176
1181
|
|
|
1182
|
+
# The binding reports an attribute's namespace as a bare URI
|
|
1183
|
+
# String; when no prefix can be recovered from the qualified
|
|
1184
|
+
# name, the reserved xml binding must still answer for xml:*
|
|
1185
|
+
# attributes.
|
|
1186
|
+
RESERVED_XML_URI = "http://www.w3.org/XML/1998/namespace"
|
|
1187
|
+
|
|
1188
|
+
def reserved_xml_uri?(namespace)
|
|
1189
|
+
case namespace
|
|
1190
|
+
when ::Leptris::XML::Namespace
|
|
1191
|
+
namespace.prefix.to_s.empty? && namespace.href == RESERVED_XML_URI
|
|
1192
|
+
when String
|
|
1193
|
+
namespace == RESERVED_XML_URI
|
|
1194
|
+
else
|
|
1195
|
+
false
|
|
1196
|
+
end
|
|
1197
|
+
end
|
|
1198
|
+
|
|
1199
|
+
def implicit_xml_namespace(node)
|
|
1200
|
+
owner = node.is_a?(::Leptris::XML::Attr) ? node.element : node
|
|
1201
|
+
return nil unless owner.is_a?(::Leptris::XML::Element)
|
|
1202
|
+
|
|
1203
|
+
::Leptris::XML::Namespace.new(owner, RESERVED_XML_URI, prefix: "xml")
|
|
1204
|
+
end
|
|
1205
|
+
|
|
1177
1206
|
def prefix_part(name)
|
|
1178
1207
|
name.include?(":") ? name.split(":", 2)[0] : nil
|
|
1179
1208
|
end
|
|
@@ -1609,7 +1638,13 @@ module Moxml
|
|
|
1609
1638
|
end
|
|
1610
1639
|
|
|
1611
1640
|
def attribute_element(attr)
|
|
1612
|
-
|
|
1641
|
+
# XPath results mint ResultAttr (parent-backed); document
|
|
1642
|
+
# attributes are binding Attr (element-backed).
|
|
1643
|
+
if attr.is_a?(::Leptris::XML::ResultAttr)
|
|
1644
|
+
attr.parent
|
|
1645
|
+
else
|
|
1646
|
+
attr.element
|
|
1647
|
+
end
|
|
1613
1648
|
end
|
|
1614
1649
|
|
|
1615
1650
|
def attribute_name(attr)
|
|
@@ -1889,7 +1924,14 @@ module Moxml
|
|
|
1889
1924
|
end
|
|
1890
1925
|
|
|
1891
1926
|
def comment_content(node)
|
|
1892
|
-
|
|
1927
|
+
if NATIVE_READ_LAYER && node.is_a?(NN)
|
|
1928
|
+
content = NN_CONTENT.bind_call(node)
|
|
1929
|
+
# NativeNode#content answers nil for comment-kind nodes
|
|
1930
|
+
# (leptris-ruby#313); the shared-pointer bridge has it.
|
|
1931
|
+
return to_binding(node).content if content.nil?
|
|
1932
|
+
|
|
1933
|
+
return content
|
|
1934
|
+
end
|
|
1893
1935
|
|
|
1894
1936
|
node.content
|
|
1895
1937
|
end
|
data/lib/moxml/adapter/libxml.rb
CHANGED
|
@@ -421,7 +421,10 @@ module Moxml
|
|
|
421
421
|
end
|
|
422
422
|
|
|
423
423
|
def attribute_element(attr)
|
|
424
|
-
|
|
424
|
+
# Customized nodes wrap raw LibXML nodes one level deep;
|
|
425
|
+
# callers of this face expect the raw native.
|
|
426
|
+
parent = attr&.parent
|
|
427
|
+
parent.is_a?(::Moxml::Adapter::CustomizedLibxml::Node) ? parent.native : parent
|
|
425
428
|
end
|
|
426
429
|
|
|
427
430
|
def attribute_namespace(attr)
|
|
@@ -50,6 +50,35 @@ module Moxml
|
|
|
50
50
|
true
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# libxml2-native inclusive C14N: delegated for the default
|
|
54
|
+
# shape (Inclusive 1.0, no comments, no InclusiveNamespaces
|
|
55
|
+
# prefix list) when the probe below proves libxml2's output
|
|
56
|
+
# byte-identical to the Ruby reference (ported from canon).
|
|
57
|
+
# Every other combination keeps the Ruby engine.
|
|
58
|
+
# Lazy, not load-time: the probe exercises the wrapper layer
|
|
59
|
+
# (Document/C14n), which is circular while THIS adapter file
|
|
60
|
+
# is still loading.
|
|
61
|
+
def native_inclusive10(native)
|
|
62
|
+
return nil unless native_c14n_byte_safe?
|
|
63
|
+
|
|
64
|
+
native.canonicalize(::Nokogiri::XML::XML_C14N_1_0)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def native_c14n_byte_safe?
|
|
68
|
+
return @native_c14n_byte_safe unless @native_c14n_byte_safe.nil?
|
|
69
|
+
|
|
70
|
+
@native_c14n_byte_safe = begin
|
|
71
|
+
probe_xml = %(<?xml version="1.0"?><doc xmlns:p="urn:p" xmlns="urn:d" b="2" a="1"><e p:x="v" z="w">t & u</e><!-- c --></doc>)
|
|
72
|
+
native_doc = ::Nokogiri::XML(probe_xml) { |c| c.strict.nonet }
|
|
73
|
+
native = native_doc.root.canonicalize(::Nokogiri::XML::XML_C14N_1_0)
|
|
74
|
+
wrapper = Wrappers::Document.new(native_doc, Context.new(:nokogiri))
|
|
75
|
+
reference = Moxml::C14n::Inclusive10.new.canonicalize(wrapper.root)
|
|
76
|
+
native == reference
|
|
77
|
+
rescue StandardError
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
53
82
|
def set_root(doc, element)
|
|
54
83
|
doc.root = element
|
|
55
84
|
end
|
data/lib/moxml/adapter/rexml.rb
CHANGED
|
@@ -278,6 +278,9 @@ module Moxml
|
|
|
278
278
|
result = []
|
|
279
279
|
element.attributes.each_attribute do |attr|
|
|
280
280
|
next if attr.prefix.to_s.start_with?("xmlns")
|
|
281
|
+
# REXML yields the default declaration with an empty prefix
|
|
282
|
+
# and name "xmlns"; treat it like any other declaration.
|
|
283
|
+
next if attr.name == "xmlns" && attr.prefix.to_s.empty?
|
|
281
284
|
|
|
282
285
|
result << attr
|
|
283
286
|
end
|
|
@@ -19,13 +19,85 @@ module Moxml
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def self.from_node(moxml_node)
|
|
22
|
+
@source_map = {}
|
|
22
23
|
return build_from_document(moxml_node) if moxml_node.is_a?(::Moxml::Document)
|
|
23
24
|
|
|
24
25
|
root = Nodes::RootNode.new
|
|
25
|
-
|
|
26
|
+
built = build_node(moxml_node, [0])
|
|
27
|
+
root.add_child(built) if built
|
|
26
28
|
root
|
|
27
29
|
end
|
|
28
30
|
|
|
31
|
+
# Position keys for subset selection: adapters mint fresh
|
|
32
|
+
# wrappers per traversal, so XPath-matched wrappers cannot be
|
|
33
|
+
# keyed by object identity. Both sides instead compute the same
|
|
34
|
+
# child-index path — the build walks dm children in order, the
|
|
35
|
+
# match side climbs parents counting buildable siblings (the
|
|
36
|
+
# same types build_node answers non-nil for).
|
|
37
|
+
class << self
|
|
38
|
+
attr_reader :source_map
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
BUILDABLE_TYPES = [::Moxml::Element, ::Moxml::Text, ::Moxml::Cdata,
|
|
42
|
+
::Moxml::Comment,
|
|
43
|
+
::Moxml::ProcessingInstruction].freeze
|
|
44
|
+
|
|
45
|
+
def self.buildable?(node)
|
|
46
|
+
BUILDABLE_TYPES.any? { |type| node.is_a?(type) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Path key of a matched wrapper relative to the source the data
|
|
50
|
+
# model was built from; nil when the wrapper is outside it.
|
|
51
|
+
# Attributes extend their owner's path with [:attribute, index].
|
|
52
|
+
# The adapter comes from the caller: the Attribute contract
|
|
53
|
+
# keeps its own adapter hop protected.
|
|
54
|
+
def self.path_key_for(wrapper, source, adapter)
|
|
55
|
+
case wrapper
|
|
56
|
+
when ::Moxml::Attribute
|
|
57
|
+
owner = wrapper.parent
|
|
58
|
+
unless owner.is_a?(::Moxml::Element)
|
|
59
|
+
# XPath results are re-wrapped from natives: attribute
|
|
60
|
+
# wrappers carry no minted parent. The adapter answers
|
|
61
|
+
# the owner element instead.
|
|
62
|
+
owner_native = adapter.attribute_element(wrapper.native)
|
|
63
|
+
owner = owner_native && ::Moxml::Node.wrap(owner_native, wrapper.context)
|
|
64
|
+
end
|
|
65
|
+
return nil unless owner.is_a?(::Moxml::Element)
|
|
66
|
+
|
|
67
|
+
owner_path = node_path(owner, source)
|
|
68
|
+
return nil unless owner_path
|
|
69
|
+
|
|
70
|
+
idx = owner.attributes.index { |a| a == wrapper }
|
|
71
|
+
return nil unless idx
|
|
72
|
+
|
|
73
|
+
owner_path + [:attribute, idx]
|
|
74
|
+
when *BUILDABLE_TYPES
|
|
75
|
+
node_path(wrapper, source)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.node_path(node, source)
|
|
80
|
+
parts = []
|
|
81
|
+
cur = node
|
|
82
|
+
loop do
|
|
83
|
+
if !source.is_a?(::Moxml::Document) && cur.equal?(source)
|
|
84
|
+
parts.unshift(0)
|
|
85
|
+
return parts
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
parent = cur.parent
|
|
89
|
+
return nil unless parent
|
|
90
|
+
|
|
91
|
+
idx = parent.children.select { |c| buildable?(c) }.index { |c| c == cur }
|
|
92
|
+
return nil unless idx
|
|
93
|
+
|
|
94
|
+
parts.unshift(idx)
|
|
95
|
+
return parts if source.is_a?(::Moxml::Document) && parent.is_a?(::Moxml::Document)
|
|
96
|
+
|
|
97
|
+
cur = parent
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
29
101
|
# Build from a Moxml::Document. Matches canon's Nokogiri path:
|
|
30
102
|
# the root element is added first, then all other document-level
|
|
31
103
|
# children (PIs, comments) in document order.
|
|
@@ -33,45 +105,64 @@ module Moxml
|
|
|
33
105
|
root = Nodes::RootNode.new
|
|
34
106
|
|
|
35
107
|
if document.root
|
|
36
|
-
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
108
|
+
# Document-level children (PIs and comments outside the
|
|
109
|
+
# document element) are part of the canonical form and must
|
|
110
|
+
# keep document order — a leading PI canonicalizes before the
|
|
111
|
+
# root element (spec §2.1; the native engines emit it there
|
|
112
|
+
# too). The document element is the first element child: some
|
|
113
|
+
# adapters mint fresh wrappers per children call, so wrapper
|
|
114
|
+
# identity cannot locate it.
|
|
115
|
+
seen_root = false
|
|
116
|
+
idx = 0
|
|
40
117
|
document.children.each do |child|
|
|
41
|
-
|
|
42
|
-
|
|
118
|
+
if child.is_a?(::Moxml::Element)
|
|
119
|
+
next if seen_root
|
|
120
|
+
|
|
121
|
+
seen_root = true
|
|
122
|
+
end
|
|
43
123
|
|
|
44
|
-
built = build_node(child)
|
|
45
|
-
|
|
124
|
+
built = build_node(child, [idx])
|
|
125
|
+
next unless built
|
|
126
|
+
|
|
127
|
+
root.add_child(built)
|
|
128
|
+
idx += 1
|
|
46
129
|
end
|
|
130
|
+
root.add_child(build_element_node(document.root, [idx])) unless seen_root
|
|
47
131
|
end
|
|
48
132
|
|
|
49
133
|
root
|
|
50
134
|
end
|
|
51
135
|
|
|
52
|
-
def self.build_node(moxml_node)
|
|
136
|
+
def self.build_node(moxml_node, path)
|
|
53
137
|
case moxml_node
|
|
54
|
-
when ::Moxml::Element then build_element_node(moxml_node)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
when ::Moxml::
|
|
138
|
+
when ::Moxml::Element then build_element_node(moxml_node, path)
|
|
139
|
+
# CDATA sections are character data to canonicalization: their
|
|
140
|
+
# content is emitted as escaped text (spec §3.1 "text nodes").
|
|
141
|
+
when ::Moxml::Text, ::Moxml::Cdata then build_text_node(moxml_node, path)
|
|
142
|
+
when ::Moxml::Comment then build_comment_node(moxml_node, path)
|
|
143
|
+
when ::Moxml::ProcessingInstruction then build_pi_node(moxml_node, path)
|
|
58
144
|
end
|
|
59
145
|
end
|
|
60
146
|
|
|
61
|
-
def self.build_element_node(moxml_element)
|
|
147
|
+
def self.build_element_node(moxml_element, path)
|
|
62
148
|
ns = moxml_element.namespace
|
|
63
149
|
element = Nodes::ElementNode.new(
|
|
64
150
|
name: moxml_element.name,
|
|
65
151
|
namespace_uri: ns&.uri,
|
|
66
152
|
prefix: ns&.prefix,
|
|
67
153
|
)
|
|
154
|
+
source_map[path] = element
|
|
68
155
|
|
|
69
156
|
build_namespace_nodes(moxml_element, element)
|
|
70
|
-
build_attribute_nodes(moxml_element, element)
|
|
157
|
+
build_attribute_nodes(moxml_element, element, path)
|
|
71
158
|
|
|
159
|
+
idx = 0
|
|
72
160
|
moxml_element.children.each do |child|
|
|
73
|
-
built = build_node(child)
|
|
74
|
-
|
|
161
|
+
built = build_node(child, path + [idx])
|
|
162
|
+
next unless built
|
|
163
|
+
|
|
164
|
+
element.add_child(built)
|
|
165
|
+
idx += 1
|
|
75
166
|
end
|
|
76
167
|
|
|
77
168
|
element
|
|
@@ -91,33 +182,48 @@ module Moxml
|
|
|
91
182
|
)
|
|
92
183
|
end
|
|
93
184
|
|
|
94
|
-
def self.build_attribute_nodes(moxml_element, element)
|
|
95
|
-
moxml_element.attributes.
|
|
185
|
+
def self.build_attribute_nodes(moxml_element, element, path)
|
|
186
|
+
moxml_element.attributes.each_with_index do |attr, idx|
|
|
96
187
|
ns = attr.namespace
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
188
|
+
# The xml prefix is reserved: any attribute bound to the XML
|
|
189
|
+
# namespace URI renders (and sorts) as prefix "xml" regardless
|
|
190
|
+
# of how the adapter reports it (spec §2.3 attribute-axis key).
|
|
191
|
+
prefix = ns&.prefix
|
|
192
|
+
uri = ns&.uri
|
|
193
|
+
if prefix == "xml" || uri == XML_URI
|
|
194
|
+
prefix = "xml"
|
|
195
|
+
uri = XML_URI
|
|
196
|
+
end
|
|
197
|
+
node = Nodes::AttributeNode.new(
|
|
198
|
+
name: attr.name,
|
|
199
|
+
value: attr.value,
|
|
200
|
+
namespace_uri: uri,
|
|
201
|
+
prefix: prefix,
|
|
104
202
|
)
|
|
203
|
+
source_map[path + [:attribute, idx]] = node
|
|
204
|
+
element.add_attribute(node)
|
|
105
205
|
end
|
|
106
206
|
end
|
|
107
207
|
|
|
108
|
-
def self.build_text_node(moxml_text)
|
|
109
|
-
Nodes::TextNode.new(value: moxml_text.content)
|
|
208
|
+
def self.build_text_node(moxml_text, path)
|
|
209
|
+
node = Nodes::TextNode.new(value: moxml_text.content)
|
|
210
|
+
source_map[path] = node
|
|
211
|
+
node
|
|
110
212
|
end
|
|
111
213
|
|
|
112
|
-
def self.build_comment_node(moxml_comment)
|
|
113
|
-
Nodes::CommentNode.new(value: moxml_comment.content)
|
|
214
|
+
def self.build_comment_node(moxml_comment, path)
|
|
215
|
+
node = Nodes::CommentNode.new(value: moxml_comment.content)
|
|
216
|
+
source_map[path] = node
|
|
217
|
+
node
|
|
114
218
|
end
|
|
115
219
|
|
|
116
|
-
def self.build_pi_node(moxml_pi)
|
|
117
|
-
Nodes::ProcessingInstructionNode.new(
|
|
220
|
+
def self.build_pi_node(moxml_pi, path)
|
|
221
|
+
node = Nodes::ProcessingInstructionNode.new(
|
|
118
222
|
target: moxml_pi.target || moxml_pi.name,
|
|
119
223
|
data: moxml_pi.content || "",
|
|
120
224
|
)
|
|
225
|
+
source_map[path] = node
|
|
226
|
+
node
|
|
121
227
|
end
|
|
122
228
|
|
|
123
229
|
def self.mark_all(node, value)
|
|
@@ -125,21 +231,52 @@ module Moxml
|
|
|
125
231
|
node.children.each { |child| mark_all(child, value) }
|
|
126
232
|
end
|
|
127
233
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
234
|
+
# Mark an XPath-selected subset: reset the whole model, then
|
|
235
|
+
# flag each matched node by position path. Node-set semantics
|
|
236
|
+
# follow the enveloped-signature interop (libxml2/xmlsec): an
|
|
237
|
+
# included element renders with its namespaces, attributes and
|
|
238
|
+
# DIRECT character data, but child ELEMENTS render only if
|
|
239
|
+
# matched too — an unmatched Signature subtree stays out even
|
|
240
|
+
# though its ancestors are matched.
|
|
241
|
+
def self.mark_subset_paths(root_node, paths)
|
|
242
|
+
mark_all(root_node, false)
|
|
243
|
+
paths.each do |path|
|
|
244
|
+
node = lookup_path(root_node, path)
|
|
245
|
+
next unless node
|
|
246
|
+
|
|
247
|
+
if node.is_a?(Nodes::ElementNode)
|
|
248
|
+
node.in_node_set = true
|
|
249
|
+
node.namespace_nodes.each { |ns| ns.in_node_set = true }
|
|
250
|
+
node.attribute_nodes.each { |attr| attr.in_node_set = true }
|
|
251
|
+
node.children.each do |child|
|
|
252
|
+
child.in_node_set = true unless child.is_a?(Nodes::ElementNode)
|
|
253
|
+
end
|
|
254
|
+
else
|
|
255
|
+
node.in_node_set = true
|
|
256
|
+
end
|
|
257
|
+
end
|
|
131
258
|
end
|
|
132
259
|
|
|
133
|
-
def self.
|
|
134
|
-
|
|
135
|
-
|
|
260
|
+
def self.lookup_path(root_node, path)
|
|
261
|
+
cur = root_node
|
|
262
|
+
i = 0
|
|
263
|
+
while i < path.length && path[i].is_a?(Integer)
|
|
264
|
+
cur = cur.children[path[i]]
|
|
265
|
+
return nil unless cur
|
|
266
|
+
|
|
267
|
+
i += 1
|
|
268
|
+
end
|
|
269
|
+
return cur if i >= path.length
|
|
270
|
+
return nil unless path[i] == :attribute
|
|
271
|
+
|
|
272
|
+
cur.attribute_nodes[path[i + 1]]
|
|
136
273
|
end
|
|
137
274
|
|
|
138
275
|
private_class_method :build_from_document, :build_node,
|
|
139
276
|
:build_element_node, :build_namespace_nodes,
|
|
140
277
|
:build_attribute_nodes, :build_text_node,
|
|
141
278
|
:build_comment_node, :build_pi_node,
|
|
142
|
-
:
|
|
279
|
+
:mark_all, :lookup_path
|
|
143
280
|
end
|
|
144
281
|
end
|
|
145
282
|
end
|
data/lib/moxml/c14n.rb
CHANGED
|
@@ -39,7 +39,7 @@ module Moxml
|
|
|
39
39
|
# :exclusive10 Exclusive C14N 1.0 (W3C REC-xml-exc-c14n-20020718)
|
|
40
40
|
def self.canonicalize(node_or_xml, with_comments: false,
|
|
41
41
|
algorithm: :inclusive10, inclusive_namespaces: [])
|
|
42
|
-
if (native = native_inclusive10(node_or_xml, with_comments,
|
|
42
|
+
if (native = native_inclusive10(node_or_xml, algorithm, with_comments,
|
|
43
43
|
inclusive_namespaces))
|
|
44
44
|
return native
|
|
45
45
|
end
|
|
@@ -52,13 +52,15 @@ module Moxml
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# The leptris engine canonicalizes in C — 23x the Ruby reference.
|
|
55
|
-
# Delegated only for the exact default shape (Inclusive 1.0
|
|
56
|
-
# comments, no InclusiveNamespace prefix list) on
|
|
57
|
-
# adapter passed the
|
|
55
|
+
# Delegated only for the exact default shape (Inclusive 1.0
|
|
56
|
+
# algorithm, no comments, no InclusiveNamespace prefix list) on
|
|
57
|
+
# wrappers whose adapter passed the byte-safety probe; every
|
|
58
58
|
# other combination keeps the Ruby engine (node-set subsets,
|
|
59
59
|
# xml:base fixup, exclusive, 1.1 stay on the ported
|
|
60
60
|
# implementation regardless).
|
|
61
|
-
def self.native_inclusive10(node_or_xml, with_comments,
|
|
61
|
+
def self.native_inclusive10(node_or_xml, algorithm, with_comments,
|
|
62
|
+
inclusive_namespaces)
|
|
63
|
+
return nil unless algorithm.nil? || algorithm == :inclusive10
|
|
62
64
|
return nil if with_comments || !inclusive_namespaces.empty?
|
|
63
65
|
return nil unless algorithm_shape_inclusive10?(node_or_xml)
|
|
64
66
|
|
|
@@ -73,6 +75,23 @@ module Moxml
|
|
|
73
75
|
Inclusive10.new.canonicalize(node_or_xml, with_comments: with_comments)
|
|
74
76
|
end
|
|
75
77
|
|
|
78
|
+
# Canonicalize an XPath-selected subset of the document (spec §3).
|
|
79
|
+
# The node-set is whatever the expression matches (elements,
|
|
80
|
+
# attributes, text): matched elements render with their
|
|
81
|
+
# namespaces, attributes and descendants; matched attributes
|
|
82
|
+
# render on their owner element alone. Enveloped-signature flows
|
|
83
|
+
# use this to digest the document minus the Signature element
|
|
84
|
+
# without deep-copying.
|
|
85
|
+
def self.canonicalize_subset(node_or_xml, xpath, with_comments: false)
|
|
86
|
+
source = node_or_xml.is_a?(String) ? ::Moxml.parse(node_or_xml) : node_or_xml
|
|
87
|
+
root = DataModel.from_node(source)
|
|
88
|
+
adapter = source.context.config.adapter
|
|
89
|
+
matched = source.xpath(xpath).to_a
|
|
90
|
+
paths = matched.filter_map { |wrapper| DataModel.path_key_for(wrapper, source, adapter) }
|
|
91
|
+
DataModel.mark_subset_paths(root, paths)
|
|
92
|
+
Processor.new(with_comments: with_comments).process(root)
|
|
93
|
+
end
|
|
94
|
+
|
|
76
95
|
def self.canonicalize_inclusive11(node_or_xml, with_comments: false)
|
|
77
96
|
Inclusive11.new.canonicalize(node_or_xml, with_comments: with_comments)
|
|
78
97
|
end
|
data/lib/moxml/version.rb
CHANGED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/c14n"
|
|
5
|
+
|
|
6
|
+
CORPUS = {
|
|
7
|
+
"cdata content becomes escaped text" => [
|
|
8
|
+
"<doc><![CDATA[a < b]]>tail</doc>",
|
|
9
|
+
"<doc>a < btail</doc>",
|
|
10
|
+
],
|
|
11
|
+
"prefixed and unprefixed attributes" => [
|
|
12
|
+
%(<doc xmlns:p="urn:p"><e p:x="1" y="2"/></doc>),
|
|
13
|
+
%(<doc xmlns:p="urn:p"><e y="2" p:x="1"></e></doc>),
|
|
14
|
+
],
|
|
15
|
+
"reserved xml:* attributes keep the xml prefix" => [
|
|
16
|
+
%(<doc xml:base="http://a/b/" xml:lang="en" xml:space="preserve"><e xml:id="i1"/></doc>),
|
|
17
|
+
%(<doc xml:base="http://a/b/" xml:lang="en" xml:space="preserve"><e xml:id="i1"></e></doc>),
|
|
18
|
+
],
|
|
19
|
+
"default namespace transitions" => [
|
|
20
|
+
%(<doc xmlns="urn:a"><e xmlns=""/><e xmlns="urn:a"/></doc>),
|
|
21
|
+
%(<doc xmlns="urn:a"><e xmlns=""></e><e></e></doc>),
|
|
22
|
+
],
|
|
23
|
+
"namespace redeclaration" => [
|
|
24
|
+
%(<doc xmlns:m="urn:m"><m:e xmlns:m="urn:m2"/></doc>),
|
|
25
|
+
%(<doc xmlns:m="urn:m"><m:e xmlns:m="urn:m2"></m:e></doc>),
|
|
26
|
+
],
|
|
27
|
+
"prefixed namespace ordering" => [
|
|
28
|
+
%(<r xmlns:z="urn:z" xmlns:a="urn:a" b="2" a="1"/>),
|
|
29
|
+
%(<r xmlns:a="urn:a" xmlns:z="urn:z" a="1" b="2"></r>),
|
|
30
|
+
],
|
|
31
|
+
"entities stay escaped" => [
|
|
32
|
+
"<doc>a & b < c</doc>",
|
|
33
|
+
"<doc>a & b < c</doc>",
|
|
34
|
+
],
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
RSpec.describe "Moxml::C14n cross-adapter canonicalization" do
|
|
38
|
+
# Every installed adapter must answer identical canonical bytes: the
|
|
39
|
+
# Ruby reference path (Inclusive10) is shared, but attribute/namespace
|
|
40
|
+
# reporting differs per adapter, and two adapters additionally
|
|
41
|
+
# delegate the default shape to a native engine (leptris in C,
|
|
42
|
+
# nokogiri via libxml2).
|
|
43
|
+
describe "byte-identical canonical form across adapters" do
|
|
44
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
45
|
+
context "with the #{adapter_name} adapter" do
|
|
46
|
+
let(:ctx) { Moxml.new(adapter_name) }
|
|
47
|
+
|
|
48
|
+
CORPUS.each do |label, (input, expected)|
|
|
49
|
+
it "canonicalizes #{label}" do
|
|
50
|
+
root = ctx.parse(input).root
|
|
51
|
+
expect(Moxml::C14n.canonicalize_inclusive10(root)).to eq(expected)
|
|
52
|
+
expect(Moxml::C14n.canonicalize(root)).to eq(expected)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe "document-level nodes keep document order" do
|
|
60
|
+
# oga surfaces the XML declaration as a <?xml?> PI and keeps the
|
|
61
|
+
# separator space in PI content; ox/headed_ox drop PI content on
|
|
62
|
+
# parse. Both are pre-existing parse-layer divergences, out of
|
|
63
|
+
# scope here.
|
|
64
|
+
let(:adapters) { Moxml::Adapter::AVAILABLE_ADAPTERS - %i[oga ox headed_ox] }
|
|
65
|
+
|
|
66
|
+
it "canonicalizes a leading PI before the root element" do
|
|
67
|
+
adapters.each do |adapter_name|
|
|
68
|
+
doc = Moxml.new(adapter_name)
|
|
69
|
+
.parse(%(<?xml version="1.0"?><?render mode="x"?><doc><e/></doc>))
|
|
70
|
+
expect(Moxml::C14n.canonicalize(doc))
|
|
71
|
+
.to eq(%(<?render mode="x"?>\n<doc><e></e></doc>))
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe "native delegation" do
|
|
77
|
+
adapters_with_native = %i[leptris nokogiri].select do |name|
|
|
78
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.include?(name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
adapters_with_native.each do |adapter_name|
|
|
82
|
+
context "with the #{adapter_name} adapter" do
|
|
83
|
+
let(:adapter_class) do
|
|
84
|
+
Moxml::Adapter.const_get(adapter_name.to_s.split("_").map(&:capitalize).join)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "passes the byte-safety probe" do
|
|
88
|
+
expect(adapter_class.native_c14n_byte_safe?).to be(true)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "engages the native path for the default shape" do
|
|
92
|
+
doc = Moxml.new(adapter_name)
|
|
93
|
+
.parse(%(<doc xmlns="urn:d" a="1"><e>t</e></doc>))
|
|
94
|
+
native = Moxml::C14n.native_inclusive10(doc.root, :inclusive10, false, [])
|
|
95
|
+
expect(native).to be_a(String)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "keeps exclusive on the Ruby engine" do
|
|
99
|
+
doc = Moxml.new(adapter_name)
|
|
100
|
+
.parse(%(<root xmlns:foo='urn:foo'><foo:a/></root>))
|
|
101
|
+
expect(Moxml::C14n.canonicalize(doc.root, algorithm: :exclusive10))
|
|
102
|
+
.to eq(Moxml::C14n.canonicalize_exclusive(doc.root))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "keeps inclusive 1.1 on the Ruby engine" do
|
|
106
|
+
doc = Moxml.new(adapter_name).parse(%(<root a="1"/>))
|
|
107
|
+
expect(Moxml::C14n.canonicalize(doc.root, algorithm: :inclusive11))
|
|
108
|
+
.to eq(Moxml::C14n.canonicalize_inclusive11(doc.root))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "keeps with_comments on the Ruby engine" do
|
|
112
|
+
doc = Moxml.new(adapter_name).parse(%(<doc><!-- c --><e/></doc>))
|
|
113
|
+
expect(Moxml::C14n.canonicalize(doc.root, with_comments: true))
|
|
114
|
+
.to eq(Moxml::C14n.canonicalize_inclusive10(doc.root,
|
|
115
|
+
with_comments: true))
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/c14n"
|
|
5
|
+
|
|
6
|
+
SIG = "<Signature xmlns=\"http://www.w3.org/2000/09/xmldsig#\"><SignedInfo>" \
|
|
7
|
+
"<m xmlns=\"urn:m\">x</m></SignedInfo><Object>o</Object></Signature>"
|
|
8
|
+
ENVELOPED_DOC = %(<env><payload id="p1">data & more</payload>#{SIG}<tail/></env>).freeze
|
|
9
|
+
ENVELOPED_EXCLUDE = '//*[not(ancestor-or-self::*[local-name()="Signature"])]'
|
|
10
|
+
SIGNATURE_XPATH = '//*[local-name()="Signature"]'
|
|
11
|
+
|
|
12
|
+
CORPUS = [
|
|
13
|
+
["apex attraction + text",
|
|
14
|
+
%(<a xml:lang="en"><b id="1"><c>t</c></b><d/></a>), "//b",
|
|
15
|
+
%(<b id="1" xml:lang="en"></b>)],
|
|
16
|
+
["explicit descendant inclusion",
|
|
17
|
+
%(<a xml:lang="en"><b id="1"><c>t</c></b><d/></a>), "//b | //c",
|
|
18
|
+
%(<b id="1" xml:lang="en"><c>t</c></b>)],
|
|
19
|
+
["direct character data of a matched element",
|
|
20
|
+
%(<a><b>t</b><d/></a>), "//b", "<b>t</b>"],
|
|
21
|
+
["unmatched child element excluded",
|
|
22
|
+
%(<a><b><c>t</c></b></a>), "//b", "<b></b>"],
|
|
23
|
+
["excluded sibling subtree",
|
|
24
|
+
%(<a><b><c/></b><d/></a>), "//*[not(self::d)]",
|
|
25
|
+
%(<a><b><c></c></b></a>)],
|
|
26
|
+
["attribute-only selection renders nothing",
|
|
27
|
+
%(<a><b id="1"/></a>), "//@id", ""],
|
|
28
|
+
# C14N renders a text-only node-set without its omitted parents.
|
|
29
|
+
["text selection", %(<a><b>t1</b><b>t2</b></a>), "//b/text()", "t1t2"],
|
|
30
|
+
["empty result", %(<a><b/></a>), "//missing", ""],
|
|
31
|
+
].freeze
|
|
32
|
+
|
|
33
|
+
COMMENTS_CASE = [
|
|
34
|
+
"with_comments keeps matched comments only",
|
|
35
|
+
%(<a><!-- keep --><b><!-- in --><c/></b></a>), "//b",
|
|
36
|
+
%(<b><!-- in --></b>)
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
COMMENTS_PADDING_LOSS = %i[ox headed_ox].freeze
|
|
40
|
+
|
|
41
|
+
RSpec.describe "Moxml::C14n subset canonicalization" do
|
|
42
|
+
# Node-set semantics follow the enveloped-signature interop
|
|
43
|
+
# (libxml2/xmlsec): a matched element renders with its namespaces,
|
|
44
|
+
# attributes and non-element children; child ELEMENTS need their
|
|
45
|
+
# own match, so an excluded subtree stays excluded under matched
|
|
46
|
+
# ancestors. Inheritable xml:* attributes of omitted ancestors and
|
|
47
|
+
# apex namespace attraction still render (spec §2.3/§2.4/§3).
|
|
48
|
+
describe "byte-identical subset canonical form across adapters" do
|
|
49
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
50
|
+
context "with the #{adapter_name} adapter" do
|
|
51
|
+
let(:ctx) { Moxml.new(adapter_name) }
|
|
52
|
+
|
|
53
|
+
CORPUS.each do |label, xml, xpath, expected|
|
|
54
|
+
next if label == "text selection" && adapter_name == :rexml
|
|
55
|
+
|
|
56
|
+
# rexml's adapter xpath does not return text nodes for
|
|
57
|
+
# text() node tests (pre-existing capability gap).
|
|
58
|
+
|
|
59
|
+
it "canonicalizes #{label}" do
|
|
60
|
+
doc = ctx.parse(xml)
|
|
61
|
+
expect(Moxml::C14n.canonicalize_subset(doc, xpath)).to eq(expected)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# ox/headed_ox strip comment content padding at the parse
|
|
66
|
+
# layer (pre-existing, same family as their PI content loss).
|
|
67
|
+
unless COMMENTS_PADDING_LOSS.include?(adapter_name)
|
|
68
|
+
it COMMENTS_CASE[0] do
|
|
69
|
+
doc = ctx.parse(COMMENTS_CASE[1])
|
|
70
|
+
expect(Moxml::C14n.canonicalize_subset(doc, COMMENTS_CASE[2],
|
|
71
|
+
with_comments: true))
|
|
72
|
+
.to eq(COMMENTS_CASE[3])
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe "enveloped-signature oracle" do
|
|
80
|
+
it "equals canonicalizing the DOM with the Signature removed" do
|
|
81
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
82
|
+
subset = Moxml::C14n.canonicalize_subset(
|
|
83
|
+
Moxml.new(adapter_name).parse(ENVELOPED_DOC), ENVELOPED_EXCLUDE
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
doc = Moxml.new(adapter_name).parse(ENVELOPED_DOC)
|
|
87
|
+
doc.root.xpath(SIGNATURE_XPATH).to_a[0].remove
|
|
88
|
+
removed = Moxml::C14n.canonicalize(doc.root)
|
|
89
|
+
|
|
90
|
+
expect(subset).to eq(removed)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe "input forms" do
|
|
96
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
97
|
+
|
|
98
|
+
it "accepts an XML string" do
|
|
99
|
+
expect(Moxml::C14n.canonicalize_subset(%(<a><b>t</b></a>), "//b"))
|
|
100
|
+
.to eq("<b>t</b>")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "accepts an element source, paths relative to it" do
|
|
104
|
+
doc = ctx.parse(%(<a><b><c>t</c></b></a>))
|
|
105
|
+
b = doc.root.xpath("//b").to_a[0]
|
|
106
|
+
expect(Moxml::C14n.canonicalize_subset(b, ".//c")).to eq("<c>t</c>")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
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.93
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -457,11 +457,13 @@ files:
|
|
|
457
457
|
- spec/moxml/attribute_resolver_spec.rb
|
|
458
458
|
- spec/moxml/attribute_spec.rb
|
|
459
459
|
- spec/moxml/builder_spec.rb
|
|
460
|
+
- spec/moxml/c14n/adapter_delegation_spec.rb
|
|
460
461
|
- spec/moxml/c14n/api_spec.rb
|
|
461
462
|
- spec/moxml/c14n/c14n_spec.rb
|
|
462
463
|
- spec/moxml/c14n/comments_pis_spec.rb
|
|
463
464
|
- spec/moxml/c14n/inclusive10_spec.rb
|
|
464
465
|
- spec/moxml/c14n/namespace_edge_cases_spec.rb
|
|
466
|
+
- spec/moxml/c14n/subset_spec.rb
|
|
465
467
|
- spec/moxml/c14n/xml_attributes_spec.rb
|
|
466
468
|
- spec/moxml/cdata_spec.rb
|
|
467
469
|
- spec/moxml/comment_spec.rb
|