moxml 0.5.92 → 0.5.94
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 +15 -2
- data/lib/moxml/adapter/libxml.rb +4 -1
- data/lib/moxml/c14n/data_model.rb +155 -35
- data/lib/moxml/c14n.rb +17 -0
- data/lib/moxml/version.rb +1 -1
- data/spec/moxml/c14n/subset_spec.rb +128 -0
- data/spec/moxml/c14n/w3c_examples_spec.rb +104 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e54a93041f955c95affa2211704839bfaf47dafc0908cd73b58012946ffe99f1
|
|
4
|
+
data.tar.gz: 4d28d6376f61ae7441e13075ac5e9433bd18b6c393cf95afe363a639a4b8d251
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e13a34f69a9e91b18d7fc9cef1e30c40ced24c8c73b322459ecba15af568439b002db8eec51097af2188571d026bfae7861646cc33077127c1fbd570b179863
|
|
7
|
+
data.tar.gz: a873c43e7ee3ccffc1997bf103263b7848d8ed17c61e25322a8d550bdf62a46d2f1e9e4a93f6e2832484a6df9d93350fd53d833a0b48ce04e1247583fc447d50
|
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? ==
|
|
@@ -1638,7 +1638,13 @@ module Moxml
|
|
|
1638
1638
|
end
|
|
1639
1639
|
|
|
1640
1640
|
def attribute_element(attr)
|
|
1641
|
-
|
|
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
|
|
1642
1648
|
end
|
|
1643
1649
|
|
|
1644
1650
|
def attribute_name(attr)
|
|
@@ -1936,7 +1942,14 @@ module Moxml
|
|
|
1936
1942
|
end
|
|
1937
1943
|
|
|
1938
1944
|
def processing_instruction_content(node)
|
|
1939
|
-
|
|
1945
|
+
if NATIVE_READ_LAYER && node.is_a?(NN)
|
|
1946
|
+
content = NN_CONTENT.bind_call(node)
|
|
1947
|
+
# NativeNode#content answers nil for PI-kind nodes too
|
|
1948
|
+
# (leptris-ruby#344 covers comments); bridge for the data.
|
|
1949
|
+
return to_binding(node).content if content.nil?
|
|
1950
|
+
|
|
1951
|
+
return content
|
|
1952
|
+
end
|
|
1940
1953
|
|
|
1941
1954
|
node.content
|
|
1942
1955
|
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)
|
|
@@ -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.
|
|
@@ -41,6 +113,7 @@ module Moxml
|
|
|
41
113
|
# adapters mint fresh wrappers per children call, so wrapper
|
|
42
114
|
# identity cannot locate it.
|
|
43
115
|
seen_root = false
|
|
116
|
+
idx = 0
|
|
44
117
|
document.children.each do |child|
|
|
45
118
|
if child.is_a?(::Moxml::Element)
|
|
46
119
|
next if seen_root
|
|
@@ -48,40 +121,48 @@ module Moxml
|
|
|
48
121
|
seen_root = true
|
|
49
122
|
end
|
|
50
123
|
|
|
51
|
-
built = build_node(child)
|
|
52
|
-
|
|
124
|
+
built = build_node(child, [idx])
|
|
125
|
+
next unless built
|
|
126
|
+
|
|
127
|
+
root.add_child(built)
|
|
128
|
+
idx += 1
|
|
53
129
|
end
|
|
54
|
-
root.add_child(build_element_node(document.root)) unless seen_root
|
|
130
|
+
root.add_child(build_element_node(document.root, [idx])) unless seen_root
|
|
55
131
|
end
|
|
56
132
|
|
|
57
133
|
root
|
|
58
134
|
end
|
|
59
135
|
|
|
60
|
-
def self.build_node(moxml_node)
|
|
136
|
+
def self.build_node(moxml_node, path)
|
|
61
137
|
case moxml_node
|
|
62
|
-
when ::Moxml::Element then build_element_node(moxml_node)
|
|
138
|
+
when ::Moxml::Element then build_element_node(moxml_node, path)
|
|
63
139
|
# CDATA sections are character data to canonicalization: their
|
|
64
140
|
# content is emitted as escaped text (spec §3.1 "text nodes").
|
|
65
|
-
when ::Moxml::Text, ::Moxml::Cdata then build_text_node(moxml_node)
|
|
66
|
-
when ::Moxml::Comment then build_comment_node(moxml_node)
|
|
67
|
-
when ::Moxml::ProcessingInstruction then build_pi_node(moxml_node)
|
|
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)
|
|
68
144
|
end
|
|
69
145
|
end
|
|
70
146
|
|
|
71
|
-
def self.build_element_node(moxml_element)
|
|
147
|
+
def self.build_element_node(moxml_element, path)
|
|
72
148
|
ns = moxml_element.namespace
|
|
73
149
|
element = Nodes::ElementNode.new(
|
|
74
150
|
name: moxml_element.name,
|
|
75
151
|
namespace_uri: ns&.uri,
|
|
76
152
|
prefix: ns&.prefix,
|
|
77
153
|
)
|
|
154
|
+
source_map[path] = element
|
|
78
155
|
|
|
79
156
|
build_namespace_nodes(moxml_element, element)
|
|
80
|
-
build_attribute_nodes(moxml_element, element)
|
|
157
|
+
build_attribute_nodes(moxml_element, element, path)
|
|
81
158
|
|
|
159
|
+
idx = 0
|
|
82
160
|
moxml_element.children.each do |child|
|
|
83
|
-
built = build_node(child)
|
|
84
|
-
|
|
161
|
+
built = build_node(child, path + [idx])
|
|
162
|
+
next unless built
|
|
163
|
+
|
|
164
|
+
element.add_child(built)
|
|
165
|
+
idx += 1
|
|
85
166
|
end
|
|
86
167
|
|
|
87
168
|
element
|
|
@@ -101,8 +182,8 @@ module Moxml
|
|
|
101
182
|
)
|
|
102
183
|
end
|
|
103
184
|
|
|
104
|
-
def self.build_attribute_nodes(moxml_element, element)
|
|
105
|
-
moxml_element.attributes.
|
|
185
|
+
def self.build_attribute_nodes(moxml_element, element, path)
|
|
186
|
+
moxml_element.attributes.each_with_index do |attr, idx|
|
|
106
187
|
ns = attr.namespace
|
|
107
188
|
# The xml prefix is reserved: any attribute bound to the XML
|
|
108
189
|
# namespace URI renders (and sorts) as prefix "xml" regardless
|
|
@@ -113,30 +194,36 @@ module Moxml
|
|
|
113
194
|
prefix = "xml"
|
|
114
195
|
uri = XML_URI
|
|
115
196
|
end
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
prefix: prefix,
|
|
122
|
-
),
|
|
197
|
+
node = Nodes::AttributeNode.new(
|
|
198
|
+
name: attr.name,
|
|
199
|
+
value: attr.value,
|
|
200
|
+
namespace_uri: uri,
|
|
201
|
+
prefix: prefix,
|
|
123
202
|
)
|
|
203
|
+
source_map[path + [:attribute, idx]] = node
|
|
204
|
+
element.add_attribute(node)
|
|
124
205
|
end
|
|
125
206
|
end
|
|
126
207
|
|
|
127
|
-
def self.build_text_node(moxml_text)
|
|
128
|
-
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
|
|
129
212
|
end
|
|
130
213
|
|
|
131
|
-
def self.build_comment_node(moxml_comment)
|
|
132
|
-
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
|
|
133
218
|
end
|
|
134
219
|
|
|
135
|
-
def self.build_pi_node(moxml_pi)
|
|
136
|
-
Nodes::ProcessingInstructionNode.new(
|
|
220
|
+
def self.build_pi_node(moxml_pi, path)
|
|
221
|
+
node = Nodes::ProcessingInstructionNode.new(
|
|
137
222
|
target: moxml_pi.target || moxml_pi.name,
|
|
138
223
|
data: moxml_pi.content || "",
|
|
139
224
|
)
|
|
225
|
+
source_map[path] = node
|
|
226
|
+
node
|
|
140
227
|
end
|
|
141
228
|
|
|
142
229
|
def self.mark_all(node, value)
|
|
@@ -144,21 +231,54 @@ module Moxml
|
|
|
144
231
|
node.children.each { |child| mark_all(child, value) }
|
|
145
232
|
end
|
|
146
233
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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. Comments and PIs render
|
|
241
|
+
# only when the expression itself selects them (spec §3: the
|
|
242
|
+
# node-set is literal).
|
|
243
|
+
def self.mark_subset_paths(root_node, paths)
|
|
244
|
+
mark_all(root_node, false)
|
|
245
|
+
paths.each do |path|
|
|
246
|
+
node = lookup_path(root_node, path)
|
|
247
|
+
next unless node
|
|
248
|
+
|
|
249
|
+
if node.is_a?(Nodes::ElementNode)
|
|
250
|
+
node.in_node_set = true
|
|
251
|
+
node.namespace_nodes.each { |ns| ns.in_node_set = true }
|
|
252
|
+
node.attribute_nodes.each { |attr| attr.in_node_set = true }
|
|
253
|
+
node.children.each do |child|
|
|
254
|
+
child.in_node_set = true if child.is_a?(Nodes::TextNode)
|
|
255
|
+
end
|
|
256
|
+
else
|
|
257
|
+
node.in_node_set = true
|
|
258
|
+
end
|
|
259
|
+
end
|
|
150
260
|
end
|
|
151
261
|
|
|
152
|
-
def self.
|
|
153
|
-
|
|
154
|
-
|
|
262
|
+
def self.lookup_path(root_node, path)
|
|
263
|
+
cur = root_node
|
|
264
|
+
i = 0
|
|
265
|
+
while i < path.length && path[i].is_a?(Integer)
|
|
266
|
+
cur = cur.children[path[i]]
|
|
267
|
+
return nil unless cur
|
|
268
|
+
|
|
269
|
+
i += 1
|
|
270
|
+
end
|
|
271
|
+
return cur if i >= path.length
|
|
272
|
+
return nil unless path[i] == :attribute
|
|
273
|
+
|
|
274
|
+
cur.attribute_nodes[path[i + 1]]
|
|
155
275
|
end
|
|
156
276
|
|
|
157
277
|
private_class_method :build_from_document, :build_node,
|
|
158
278
|
:build_element_node, :build_namespace_nodes,
|
|
159
279
|
:build_attribute_nodes, :build_text_node,
|
|
160
280
|
:build_comment_node, :build_pi_node,
|
|
161
|
-
:
|
|
281
|
+
:mark_all, :lookup_path
|
|
162
282
|
end
|
|
163
283
|
end
|
|
164
284
|
end
|
data/lib/moxml/c14n.rb
CHANGED
|
@@ -75,6 +75,23 @@ module Moxml
|
|
|
75
75
|
Inclusive10.new.canonicalize(node_or_xml, with_comments: with_comments)
|
|
76
76
|
end
|
|
77
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
|
+
|
|
78
95
|
def self.canonicalize_inclusive11(node_or_xml, with_comments: false)
|
|
79
96
|
Inclusive11.new.canonicalize(node_or_xml, with_comments: with_comments)
|
|
80
97
|
end
|
data/lib/moxml/version.rb
CHANGED
|
@@ -0,0 +1,128 @@
|
|
|
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 renders explicitly selected comments only",
|
|
35
|
+
%(<a><!-- keep --><b><!-- in --><c/></b></a>), "//b | //comment()",
|
|
36
|
+
%(<!-- keep -->\n<b><!-- in --></b>)
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
COMMENTS_EXCLUDED_CASE = [
|
|
40
|
+
"unselected comments stay out",
|
|
41
|
+
%(<a><!-- keep --><b><!-- in --><c/></b></a>), "//b",
|
|
42
|
+
%(<b></b>)
|
|
43
|
+
].freeze
|
|
44
|
+
|
|
45
|
+
COMMENTS_PADDING_LOSS = %i[ox headed_ox].freeze
|
|
46
|
+
# rexml's adapter xpath does not return comment()/text() node results
|
|
47
|
+
# (pre-existing capability gap, same family for both axes).
|
|
48
|
+
COMMENT_SELECTION_GAP = %i[rexml].freeze
|
|
49
|
+
|
|
50
|
+
RSpec.describe "Moxml::C14n subset canonicalization" do
|
|
51
|
+
# Node-set semantics follow the enveloped-signature interop
|
|
52
|
+
# (libxml2/xmlsec): a matched element renders with its namespaces,
|
|
53
|
+
# attributes and non-element children; child ELEMENTS need their
|
|
54
|
+
# own match, so an excluded subtree stays excluded under matched
|
|
55
|
+
# ancestors. Inheritable xml:* attributes of omitted ancestors and
|
|
56
|
+
# apex namespace attraction still render (spec §2.3/§2.4/§3).
|
|
57
|
+
describe "byte-identical subset canonical form across adapters" do
|
|
58
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
59
|
+
context "with the #{adapter_name} adapter" do
|
|
60
|
+
let(:ctx) { Moxml.new(adapter_name) }
|
|
61
|
+
|
|
62
|
+
CORPUS.each do |label, xml, xpath, expected|
|
|
63
|
+
next if label == "text selection" && adapter_name == :rexml
|
|
64
|
+
|
|
65
|
+
# rexml's adapter xpath does not return text nodes for
|
|
66
|
+
# text() node tests (pre-existing capability gap).
|
|
67
|
+
|
|
68
|
+
it "canonicalizes #{label}" do
|
|
69
|
+
doc = ctx.parse(xml)
|
|
70
|
+
expect(Moxml::C14n.canonicalize_subset(doc, xpath)).to eq(expected)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# ox/headed_ox strip comment content padding at the parse
|
|
75
|
+
# layer (pre-existing, same family as their PI content loss);
|
|
76
|
+
# only the rendered-comment case depends on it. rexml's
|
|
77
|
+
# comment() selection gap also only affects this case.
|
|
78
|
+
unless COMMENTS_PADDING_LOSS.include?(adapter_name) ||
|
|
79
|
+
COMMENT_SELECTION_GAP.include?(adapter_name)
|
|
80
|
+
it COMMENTS_CASE[0] do
|
|
81
|
+
doc = ctx.parse(COMMENTS_CASE[1])
|
|
82
|
+
expect(Moxml::C14n.canonicalize_subset(doc, COMMENTS_CASE[2],
|
|
83
|
+
with_comments: true))
|
|
84
|
+
.to eq(COMMENTS_CASE[3])
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it COMMENTS_EXCLUDED_CASE[0] do
|
|
89
|
+
doc = ctx.parse(COMMENTS_EXCLUDED_CASE[1])
|
|
90
|
+
expect(Moxml::C14n.canonicalize_subset(doc, COMMENTS_EXCLUDED_CASE[2],
|
|
91
|
+
with_comments: true))
|
|
92
|
+
.to eq(COMMENTS_EXCLUDED_CASE[3])
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
describe "enveloped-signature oracle" do
|
|
99
|
+
it "equals canonicalizing the DOM with the Signature removed" do
|
|
100
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
101
|
+
subset = Moxml::C14n.canonicalize_subset(
|
|
102
|
+
Moxml.new(adapter_name).parse(ENVELOPED_DOC), ENVELOPED_EXCLUDE
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
doc = Moxml.new(adapter_name).parse(ENVELOPED_DOC)
|
|
106
|
+
doc.root.xpath(SIGNATURE_XPATH).to_a[0].remove
|
|
107
|
+
removed = Moxml::C14n.canonicalize(doc.root)
|
|
108
|
+
|
|
109
|
+
expect(subset).to eq(removed)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
describe "input forms" do
|
|
115
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
116
|
+
|
|
117
|
+
it "accepts an XML string" do
|
|
118
|
+
expect(Moxml::C14n.canonicalize_subset(%(<a><b>t</b></a>), "//b"))
|
|
119
|
+
.to eq("<b>t</b>")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "accepts an element source, paths relative to it" do
|
|
123
|
+
doc = ctx.parse(%(<a><b><c>t</c></b></a>))
|
|
124
|
+
b = doc.root.xpath("//b").to_a[0]
|
|
125
|
+
expect(Moxml::C14n.canonicalize_subset(b, ".//c")).to eq("<c>t</c>")
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/c14n"
|
|
5
|
+
|
|
6
|
+
# Shapes drawn from the W3C Canonical XML 1.0 REC examples (§2–§3):
|
|
7
|
+
# attribute-value whitespace normalization, character-reference CR
|
|
8
|
+
# preservation vs literal-CR folding, CDATA as character data, empty
|
|
9
|
+
# element form, PI/comment placement, and the §3 document-subset
|
|
10
|
+
# whitespace rules. Expected bytes are the forms all conformant
|
|
11
|
+
# adapters answer identically.
|
|
12
|
+
#
|
|
13
|
+
# Documented parse-layer divergences (skipped rows):
|
|
14
|
+
# - ox/headed_ox expand 
 to LF, strip inter-element whitespace
|
|
15
|
+
# and comment padding at parse (out of scope here)
|
|
16
|
+
# - oga keeps the target/content separator space in PI data
|
|
17
|
+
W3C_FULL_FORMS = [
|
|
18
|
+
["attribute-value whitespace collapses to single spaces",
|
|
19
|
+
"<doc>\n <e attr1=\"v1\"\n attr2 = \"v2\"\n > content </e>\n</doc>",
|
|
20
|
+
"<doc>\n <e attr1=\"v1\" attr2=\"v2\"> content </e>\n</doc>"],
|
|
21
|
+
["character-reference CR stays 
; literal CR folds to LF",
|
|
22
|
+
%(<doc>a
b\nc
d</doc>),
|
|
23
|
+
%(<doc>a
b\nc\nd</doc>)],
|
|
24
|
+
["CDATA is character data and escapes",
|
|
25
|
+
%(<doc><![CDATA[a<b]]> & < x</doc>),
|
|
26
|
+
%(<doc>a<b & < x</doc>)],
|
|
27
|
+
["empty elements render as start and end tags",
|
|
28
|
+
%(<doc><e/><f></f></doc>),
|
|
29
|
+
"<doc><e></e><f></f></doc>"],
|
|
30
|
+
["PIs render in place; comments drop without with_comments",
|
|
31
|
+
%(<doc><?pi c?><!-- c1 --><e/><!-- c2 --></doc>),
|
|
32
|
+
%(<doc><?pi c?><e></e></doc>)],
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
W3C_SUBSET_FORM = [
|
|
36
|
+
%(<doc>\n <e1 a="1" b="2"/>\n <e2/>\n <e3/>\n <!-- A small comment -->\n <?pi x?>\n</doc>),
|
|
37
|
+
"//e1 | //e2 | //e3",
|
|
38
|
+
%(<e1 a="1" b="2"></e1><e2></e2><e3></e3>),
|
|
39
|
+
].freeze
|
|
40
|
+
|
|
41
|
+
W3C_SUBSET_WITH_ROOT = [
|
|
42
|
+
%(<doc>\n <e1 a="1" b="2"/>\n <e2/>\n <e3/>\n <!-- A small comment -->\n <?pi x?>\n</doc>),
|
|
43
|
+
"/doc | //e1 | //e2 | //e3",
|
|
44
|
+
%(<doc>\n <e1 a="1" b="2"></e1>\n <e2></e2>\n <e3></e3>\n \n \n</doc>),
|
|
45
|
+
].freeze
|
|
46
|
+
|
|
47
|
+
CR_SHAPE = 1
|
|
48
|
+
PI_SHAPE = 4
|
|
49
|
+
WHITESPACE_SHAPES = %i[ox headed_ox].freeze
|
|
50
|
+
|
|
51
|
+
RSpec.describe "Moxml::C14n W3C REC-xml-c14n examples" do
|
|
52
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
53
|
+
context "with the #{adapter_name} adapter" do
|
|
54
|
+
let(:ctx) { Moxml.new(adapter_name) }
|
|
55
|
+
|
|
56
|
+
W3C_FULL_FORMS.each_with_index do |(label, xml, expected), idx|
|
|
57
|
+
# ox/headed_ox strip inter-element whitespace, expand 
|
|
58
|
+
# and pad comments at parse; oga keeps the PI separator space.
|
|
59
|
+
skip_shapes = { ox: [0, CR_SHAPE, PI_SHAPE],
|
|
60
|
+
headed_ox: [0, CR_SHAPE, PI_SHAPE],
|
|
61
|
+
oga: [PI_SHAPE] }[adapter_name] || []
|
|
62
|
+
next if skip_shapes.include?(idx)
|
|
63
|
+
|
|
64
|
+
it "canonicalizes #{label}" do
|
|
65
|
+
doc = ctx.parse(xml)
|
|
66
|
+
expect(Moxml::C14n.canonicalize_inclusive10(doc)).to eq(expected)
|
|
67
|
+
expect(Moxml::C14n.canonicalize(doc.root)).to eq(expected)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe "W3C §3 document-subset whitespace" do
|
|
74
|
+
Moxml::Adapter::AVAILABLE_ADAPTERS.each do |adapter_name|
|
|
75
|
+
next if WHITESPACE_SHAPES.include?(adapter_name)
|
|
76
|
+
|
|
77
|
+
it "renders selected nodes only, whitespace runs in place (#{adapter_name})" do
|
|
78
|
+
doc = Moxml.new(adapter_name).parse(W3C_SUBSET_FORM[0])
|
|
79
|
+
expect(Moxml::C14n.canonicalize_subset(doc, W3C_SUBSET_FORM[1]))
|
|
80
|
+
.to eq(W3C_SUBSET_FORM[2])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "keeps the root's whitespace runs when the root is selected (#{adapter_name})" do
|
|
84
|
+
doc = Moxml.new(adapter_name).parse(W3C_SUBSET_WITH_ROOT[0])
|
|
85
|
+
expect(Moxml::C14n.canonicalize_subset(doc, W3C_SUBSET_WITH_ROOT[1]))
|
|
86
|
+
.to eq(W3C_SUBSET_WITH_ROOT[2])
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
describe "Ruby reference agrees with the native engines" do
|
|
92
|
+
%i[leptris nokogiri].each do |adapter_name|
|
|
93
|
+
next unless Moxml::Adapter::AVAILABLE_ADAPTERS.include?(adapter_name)
|
|
94
|
+
|
|
95
|
+
it "on every full-document shape (#{adapter_name})" do
|
|
96
|
+
W3C_FULL_FORMS.each do |_label, xml, _expected|
|
|
97
|
+
root = Moxml.new(adapter_name).parse(xml).root
|
|
98
|
+
expect(Moxml::C14n.canonicalize(root))
|
|
99
|
+
.to eq(Moxml::C14n.canonicalize_inclusive10(root))
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
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.94
|
|
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-09-
|
|
11
|
+
date: 2026-09-30 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
|
|
@@ -463,6 +463,8 @@ files:
|
|
|
463
463
|
- spec/moxml/c14n/comments_pis_spec.rb
|
|
464
464
|
- spec/moxml/c14n/inclusive10_spec.rb
|
|
465
465
|
- spec/moxml/c14n/namespace_edge_cases_spec.rb
|
|
466
|
+
- spec/moxml/c14n/subset_spec.rb
|
|
467
|
+
- spec/moxml/c14n/w3c_examples_spec.rb
|
|
466
468
|
- spec/moxml/c14n/xml_attributes_spec.rb
|
|
467
469
|
- spec/moxml/cdata_spec.rb
|
|
468
470
|
- spec/moxml/comment_spec.rb
|