moxml 0.1.25 → 0.2.0

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.
Files changed (181) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +9 -0
  3. data/.github/workflows/windows.yml +62 -0
  4. data/Gemfile +3 -1
  5. data/README.adoc +133 -6
  6. data/docs/signature/algorithms.md +108 -0
  7. data/docs/signature/architecture.md +148 -0
  8. data/docs/signature/c14n.md +110 -0
  9. data/docs/signature/examples.md +109 -0
  10. data/docs/signature/flows.md +132 -0
  11. data/docs/signature/quick-reference.md +115 -0
  12. data/docs/signature/security.md +143 -0
  13. data/examples/signature/auto_key_extraction.rb +24 -0
  14. data/examples/signature/enveloped_rsa.rb +45 -0
  15. data/examples/signature/hmac.rb +33 -0
  16. data/lib/compat/opal/moxml_boot.rb +5 -0
  17. data/lib/moxml/adapter/base.rb +53 -49
  18. data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
  19. data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
  20. data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
  21. data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
  22. data/lib/moxml/adapter/customized_leptris.rb +16 -0
  23. data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
  24. data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
  25. data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
  26. data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
  27. data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
  28. data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
  29. data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
  30. data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
  31. data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
  32. data/lib/moxml/adapter/customized_oga.rb +2 -0
  33. data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
  34. data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
  35. data/lib/moxml/adapter/headed_ox.rb +9 -128
  36. data/lib/moxml/adapter/leptris.rb +965 -0
  37. data/lib/moxml/adapter/libxml.rb +131 -99
  38. data/lib/moxml/adapter/nokogiri.rb +29 -8
  39. data/lib/moxml/adapter/oga.rb +91 -54
  40. data/lib/moxml/adapter/ox.rb +93 -137
  41. data/lib/moxml/adapter/rexml.rb +61 -50
  42. data/lib/moxml/adapter.rb +2 -1
  43. data/lib/moxml/attribute.rb +8 -4
  44. data/lib/moxml/attribute_resolver.rb +189 -0
  45. data/lib/moxml/c14n/attribute_handler.rb +71 -0
  46. data/lib/moxml/c14n/character_encoder.rb +29 -0
  47. data/lib/moxml/c14n/data_model.rb +145 -0
  48. data/lib/moxml/c14n/exclusive.rb +188 -0
  49. data/lib/moxml/c14n/inclusive_10.rb +42 -0
  50. data/lib/moxml/c14n/inclusive_11.rb +21 -0
  51. data/lib/moxml/c14n/namespace_context.rb +68 -0
  52. data/lib/moxml/c14n/namespace_handler.rb +71 -0
  53. data/lib/moxml/c14n/node.rb +42 -0
  54. data/lib/moxml/c14n/nodes/attribute_node.rb +50 -0
  55. data/lib/moxml/c14n/nodes/comment_node.rb +29 -0
  56. data/lib/moxml/c14n/nodes/element_node.rb +62 -0
  57. data/lib/moxml/c14n/nodes/namespace_node.rb +41 -0
  58. data/lib/moxml/c14n/nodes/processing_instruction_node.rb +30 -0
  59. data/lib/moxml/c14n/nodes/root_node.rb +22 -0
  60. data/lib/moxml/c14n/nodes/text_node.rb +29 -0
  61. data/lib/moxml/c14n/nodes.rb +17 -0
  62. data/lib/moxml/c14n/processor.rb +118 -0
  63. data/lib/moxml/c14n/writer.rb +121 -0
  64. data/lib/moxml/c14n/xml_base_handler.rb +140 -0
  65. data/lib/moxml/c14n.rb +96 -0
  66. data/lib/moxml/config.rb +29 -3
  67. data/lib/moxml/context.rb +12 -0
  68. data/lib/moxml/element.rb +29 -12
  69. data/lib/moxml/entity/reference.rb +28 -0
  70. data/lib/moxml/entity.rb +125 -0
  71. data/lib/moxml/node.rb +90 -1
  72. data/lib/moxml/sax/namespace_splitter.rb +5 -2
  73. data/lib/moxml/signature/algorithms/base64_transform.rb +40 -0
  74. data/lib/moxml/signature/algorithms/canonicalization_base.rb +87 -0
  75. data/lib/moxml/signature/algorithms/digest_base.rb +51 -0
  76. data/lib/moxml/signature/algorithms/dsa_sha.rb +107 -0
  77. data/lib/moxml/signature/algorithms/ecdsa_sha.rb +125 -0
  78. data/lib/moxml/signature/algorithms/enveloped_signature_transform.rb +79 -0
  79. data/lib/moxml/signature/algorithms/exc_c14n_10.rb +23 -0
  80. data/lib/moxml/signature/algorithms/hmac_sha.rb +117 -0
  81. data/lib/moxml/signature/algorithms/inclusive_c14n_10.rb +21 -0
  82. data/lib/moxml/signature/algorithms/inclusive_c14n_11.rb +23 -0
  83. data/lib/moxml/signature/algorithms/rsa_pkcs1_sha.rb +56 -0
  84. data/lib/moxml/signature/algorithms/sha1.rb +13 -0
  85. data/lib/moxml/signature/algorithms/sha224.rb +13 -0
  86. data/lib/moxml/signature/algorithms/sha256.rb +13 -0
  87. data/lib/moxml/signature/algorithms/sha384.rb +13 -0
  88. data/lib/moxml/signature/algorithms/sha512.rb +13 -0
  89. data/lib/moxml/signature/algorithms/signature_method_base.rb +58 -0
  90. data/lib/moxml/signature/algorithms/transform_base.rb +40 -0
  91. data/lib/moxml/signature/algorithms.rb +113 -0
  92. data/lib/moxml/signature/errors.rb +99 -0
  93. data/lib/moxml/signature/key_extractor.rb +172 -0
  94. data/lib/moxml/signature/model/algorithm_method.rb +18 -0
  95. data/lib/moxml/signature/model/digest_method.rb +15 -0
  96. data/lib/moxml/signature/model/key/dsa_key_value.rb +28 -0
  97. data/lib/moxml/signature/model/key/ec_key_value.rb +22 -0
  98. data/lib/moxml/signature/model/key/rsa_key_value.rb +19 -0
  99. data/lib/moxml/signature/model/key/x509_data.rb +32 -0
  100. data/lib/moxml/signature/model/key/x509_digest.rb +19 -0
  101. data/lib/moxml/signature/model/key/x509_issuer_serial.rb +19 -0
  102. data/lib/moxml/signature/model/key_info.rb +21 -0
  103. data/lib/moxml/signature/model/key_value.rb +20 -0
  104. data/lib/moxml/signature/model/object_element.rb +18 -0
  105. data/lib/moxml/signature/model/reference.rb +22 -0
  106. data/lib/moxml/signature/model/signature.rb +21 -0
  107. data/lib/moxml/signature/model/signature_value.rb +16 -0
  108. data/lib/moxml/signature/model/signed_info.rb +20 -0
  109. data/lib/moxml/signature/model/transform.rb +16 -0
  110. data/lib/moxml/signature/model/transforms.rb +36 -0
  111. data/lib/moxml/signature/model.rb +35 -0
  112. data/lib/moxml/signature/parser.rb +270 -0
  113. data/lib/moxml/signature/reference_resolver.rb +79 -0
  114. data/lib/moxml/signature/reference_result.rb +22 -0
  115. data/lib/moxml/signature/serializer.rb +163 -0
  116. data/lib/moxml/signature/signer.rb +73 -0
  117. data/lib/moxml/signature/single_verification_result.rb +31 -0
  118. data/lib/moxml/signature/transform_pipeline.rb +106 -0
  119. data/lib/moxml/signature/verification_result.rb +27 -0
  120. data/lib/moxml/signature/verifier.rb +129 -0
  121. data/lib/moxml/signature.rb +94 -0
  122. data/lib/moxml/version.rb +1 -1
  123. data/lib/moxml/xml_emitter.rb +71 -0
  124. data/lib/moxml/xpath/compiler.rb +60 -7
  125. data/lib/moxml/xpath/parser.rb +22 -15
  126. data/lib/moxml.rb +5 -0
  127. data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
  128. data/reference-docs/w3c-xmldsig-core.md +400 -0
  129. data/spec/examples/readme_examples_spec.rb +0 -4
  130. data/spec/examples/xpath_examples_spec.rb +0 -11
  131. data/spec/fixtures/xmldsig/keys/rsa_private.pem +28 -0
  132. data/spec/fixtures/xmldsig/keys/rsa_public.pem +9 -0
  133. data/spec/fixtures/xmldsig/keys/rsa_ref.pem +15 -0
  134. data/spec/fixtures/xmldsig/keys/rsa_ref.pub +6 -0
  135. data/spec/fixtures/xmldsig/sign2-doc.xml +6 -0
  136. data/spec/fixtures/xmldsig/sign2-result.xml +25 -0
  137. data/spec/fixtures/xmldsig/sign3-result.xml +39 -0
  138. data/spec/integration/all_adapters_spec.rb +17 -0
  139. data/spec/integration/sax_parity_spec.rb +58 -0
  140. data/spec/integration/shared_examples/edge_cases.rb +0 -10
  141. data/spec/integration/shared_examples/integration_workflows.rb +0 -10
  142. data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
  143. data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
  144. data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +0 -8
  145. data/spec/moxml/adapter/leptris_spec.rb +75 -0
  146. data/spec/moxml/adapter/ox_spec.rb +20 -6
  147. data/spec/moxml/adapter/platform_spec.rb +15 -2
  148. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
  149. data/spec/moxml/attribute_resolver_spec.rb +108 -0
  150. data/spec/moxml/c14n/api_spec.rb +117 -0
  151. data/spec/moxml/c14n/c14n_spec.rb +88 -0
  152. data/spec/moxml/c14n/comments_pis_spec.rb +65 -0
  153. data/spec/moxml/c14n/inclusive10_spec.rb +60 -0
  154. data/spec/moxml/c14n/namespace_edge_cases_spec.rb +88 -0
  155. data/spec/moxml/c14n/xml_attributes_spec.rb +54 -0
  156. data/spec/moxml/doctype_spec.rb +1 -1
  157. data/spec/moxml/entity_spec.rb +76 -0
  158. data/spec/moxml/node_spec.rb +104 -0
  159. data/spec/moxml/sax_entity_parity_spec.rb +358 -0
  160. data/spec/moxml/signature/algorithms/base64_transform_spec.rb +26 -0
  161. data/spec/moxml/signature/algorithms/digest_base_spec.rb +72 -0
  162. data/spec/moxml/signature/algorithms/dsa_sha_spec.rb +46 -0
  163. data/spec/moxml/signature/algorithms/ecdsa_sha_spec.rb +72 -0
  164. data/spec/moxml/signature/algorithms/enveloped_signature_transform_spec.rb +40 -0
  165. data/spec/moxml/signature/algorithms/hmac_sha_spec.rb +71 -0
  166. data/spec/moxml/signature/algorithms/rsa_pkcs1_sha_spec.rb +55 -0
  167. data/spec/moxml/signature/algorithms_spec.rb +76 -0
  168. data/spec/moxml/signature/cross_verify_spec.rb +64 -0
  169. data/spec/moxml/signature/edge_cases_spec.rb +284 -0
  170. data/spec/moxml/signature/fixtures_spec.rb +80 -0
  171. data/spec/moxml/signature/key_extractor_spec.rb +96 -0
  172. data/spec/moxml/signature/model_spec.rb +54 -0
  173. data/spec/moxml/signature/round_trip_spec.rb +113 -0
  174. data/spec/moxml/xml_emitter_spec.rb +64 -0
  175. data/spec/moxml/xpath/axes_spec.rb +72 -0
  176. data/spec/moxml/xpath/parser_spec.rb +6 -0
  177. data/spec/moxml/xpath_capabilities_spec.rb +5 -3
  178. data/spec/performance/benchmark_spec.rb +13 -6
  179. data/spec/performance/thread_safety_spec.rb +0 -4
  180. data/spec/spec_helper.rb +5 -1
  181. metadata +130 -2
@@ -125,6 +125,186 @@ RSpec.shared_examples "xml adapter" do
125
125
  value = attr.respond_to?(:to_xml) ? attr&.to_xml : attr.to_s
126
126
  expect(value).to match(/< > & ("|") ('|')/)
127
127
  end
128
+
129
+ it "keeps attributes that share a local name across namespaces (issues #94/#95)" do
130
+ collision = described_class.parse(
131
+ '<probe xmlns:xmi="http://www.example.com/xmi" ' \
132
+ 'xmi:type="uml:Parameter" type="EAnone_void"/>',
133
+ ).native
134
+ probe = described_class.root(collision)
135
+
136
+ attrs = described_class.attributes(probe)
137
+ expect(attrs.size).to eq(2)
138
+ expect(attrs.map(&:value).sort).to eq(["EAnone_void", "uml:Parameter"])
139
+
140
+ reversed = described_class.parse(
141
+ '<probe type="EAnone_void" ' \
142
+ 'xmlns:xmi="http://www.example.com/xmi" xmi:type="uml:Parameter"/>',
143
+ ).native
144
+ expect(described_class.attributes(described_class.root(reversed)).size).to eq(2)
145
+
146
+ # Bare name resolves to the namespace-less attribute; the
147
+ # prefixed name to the namespaced one — on every adapter.
148
+ expect(described_class.get_attribute_value(probe, "type"))
149
+ .to eq("EAnone_void")
150
+ expect(described_class.get_attribute_value(probe, "xmi:type"))
151
+ .to eq("uml:Parameter")
152
+ expect(described_class.get_attribute_value(probe, "bogus:missing"))
153
+ .to be_nil
154
+ end
155
+ end
156
+
157
+ describe "mutation return contract" do
158
+ let(:doc) { described_class.parse(xml).native }
159
+ let(:element) { described_class.children(described_class.root(doc)).first }
160
+
161
+ it "returns the native to track from set_attribute_name" do
162
+ native = described_class.attributes(element).first
163
+ tracked = described_class.set_attribute_name(native, "renamed")
164
+ expect(tracked).to be_a(native.class)
165
+ expect(described_class.attribute_name(tracked)).to eq("renamed")
166
+ end
167
+
168
+ it "returns the native to track from set_namespace on an attribute" do
169
+ native = described_class.attributes(element).first
170
+ ns = described_class.namespace_definitions(
171
+ described_class.root(doc),
172
+ ).last
173
+ tracked = described_class.set_namespace(native, ns)
174
+ expect(tracked).to be_a(native.class)
175
+ end
176
+
177
+ it "returns the native to track from set_attribute_value" do
178
+ native = described_class.attributes(element).first
179
+ tracked = described_class.set_attribute_value(native, "updated")
180
+ expect(tracked).to be_a(native.class)
181
+ expect(tracked.value).to eq("updated")
182
+ end
183
+ end
184
+
185
+ describe "tree mutation" do
186
+ let(:doc) { described_class.parse(xml).native }
187
+ let(:root) { described_class.root(doc) }
188
+
189
+ def names_of(node)
190
+ described_class.children(node)
191
+ .map { |child| described_class.node_name(child) }
192
+ end
193
+
194
+ it "inserts previous and next siblings" do
195
+ target = described_class.children(root)[1]
196
+ described_class.add_previous_sibling(
197
+ target, described_class.create_element("before")
198
+ )
199
+ described_class.add_next_sibling(
200
+ target, described_class.create_element("after")
201
+ )
202
+
203
+ expect(names_of(root)).to eq(%w[child before child after special])
204
+ end
205
+
206
+ it "removes nodes from the tree" do
207
+ described_class.remove(described_class.children(root)[1])
208
+ expect(names_of(root)).to eq(%w[child special])
209
+ end
210
+
211
+ it "replaces a node in place" do
212
+ described_class.replace(
213
+ described_class.children(root)[1],
214
+ described_class.create_element("replacement"),
215
+ )
216
+ expect(names_of(root)).to eq(%w[child replacement special])
217
+ end
218
+
219
+ it "replaces all children" do
220
+ described_class.replace_children(
221
+ root, [described_class.create_element("only")]
222
+ )
223
+ expect(names_of(root)).to eq(%w[only])
224
+ end
225
+ end
226
+
227
+ describe "content accessors" do
228
+ let(:doc) { described_class.parse(xml).native }
229
+ let(:root) { described_class.root(doc) }
230
+
231
+ it "reads inner text from direct text children" do
232
+ child = described_class.children(root)[0]
233
+ expect(described_class.inner_text(child)).to eq("Text")
234
+ end
235
+
236
+ it "writes text content" do
237
+ element = described_class.create_element("scratch")
238
+ described_class.set_text_content(element, "written")
239
+ expect(described_class.inner_text(element)).to eq("written")
240
+ end
241
+
242
+ it "reads and writes comment content" do
243
+ comment = described_class.create_comment("note")
244
+ expect(described_class.comment_content(comment)).to eq("note")
245
+ described_class.set_comment_content(comment, "changed")
246
+ expect(described_class.comment_content(comment)).to eq("changed")
247
+ end
248
+
249
+ it "reads and writes CDATA content" do
250
+ cdata = described_class.create_cdata("<raw>")
251
+ expect(described_class.cdata_content(cdata)).to eq("<raw>")
252
+ described_class.set_cdata_content(cdata, "<new>")
253
+ expect(described_class.cdata_content(cdata)).to eq("<new>")
254
+ end
255
+ end
256
+
257
+ describe "auxiliary node creation" do
258
+ it "creates doctypes with external identifiers" do
259
+ doctype = described_class.create_doctype("cfg", "pub", "sys")
260
+ expect(described_class.node_type(doctype)).to eq(:doctype)
261
+ expect(described_class.doctype_name(doctype)).to eq("cfg")
262
+ expect(described_class.doctype_external_id(doctype)).to eq("pub")
263
+ expect(described_class.doctype_system_id(doctype)).to eq("sys")
264
+ end
265
+
266
+ it "creates declarations with version, encoding and standalone" do
267
+ declaration = described_class.create_declaration("1.0", "UTF-8", "yes")
268
+ expect(described_class.declaration_attribute(declaration, "version"))
269
+ .to eq("1.0")
270
+ expect(described_class.declaration_attribute(declaration, "standalone"))
271
+ .to eq("yes")
272
+ end
273
+
274
+ it "creates entity references by name" do
275
+ reference = described_class.create_entity_reference("copy")
276
+ expect(described_class.node_type(reference)).to eq(:entity_reference)
277
+ expect(described_class.entity_reference_name(reference)).to eq("copy")
278
+ end
279
+
280
+ it "duplicates nodes with their names" do
281
+ element = described_class.create_element("original")
282
+ duplicate = described_class.duplicate_node(element)
283
+ expect(described_class.node_name(duplicate)).to eq("original")
284
+ expect(duplicate).not_to equal(element)
285
+ end
286
+ end
287
+
288
+ describe "in-scope namespaces" do
289
+ let(:doc) { described_class.parse(xml).native }
290
+ let(:special) { described_class.children(described_class.root(doc)).last }
291
+
292
+ it "returns the ancestor-declared prefixes" do
293
+ prefixes = described_class.in_scope_namespaces(special)
294
+ .map { |ns| described_class.namespace_prefix(ns) }
295
+ # The default declaration is reported as a nil prefix everywhere
296
+ # except Ox, which spells it "xmlns"
297
+ expect(prefixes).to include("x")
298
+ expect(prefixes.size).to eq(2)
299
+ end
300
+ end
301
+
302
+ describe "line numbers" do
303
+ it "returns a 1-based position or nil" do
304
+ doc = described_class.parse(xml).native
305
+ line = described_class.line_number(described_class.root(doc))
306
+ expect(line.nil? || (line.is_a?(Integer) && line >= 1)).to be(true)
307
+ end
128
308
  end
129
309
 
130
310
  describe "namespaces" do
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::AttributeResolver do
6
+ let(:context) { Moxml.new }
7
+ let(:doc) do
8
+ context.parse(<<~XML)
9
+ <root xmlns:p="http://x.org" xmlns:q="http://x.org">
10
+ <e q:type="namespaced" type="bare"/>
11
+ </root>
12
+ XML
13
+ end
14
+ let(:element) { doc.root.children.find(&:element?) }
15
+
16
+ describe ".resolve" do
17
+ it "matches bare names against no-namespace attributes only" do
18
+ expect(described_class.resolve(element, "type")&.value).to eq("bare")
19
+ end
20
+
21
+ it "matches prefixed names by expanded name" do
22
+ expect(described_class.resolve(element, "p:type")&.value)
23
+ .to eq("namespaced")
24
+ expect(described_class.resolve(element, "q:type")&.value)
25
+ .to eq("namespaced")
26
+ end
27
+
28
+ it "returns nil for undeclared prefixes and declarations" do
29
+ expect(described_class.resolve(element, "z:type")).to be_nil
30
+ expect(described_class.resolve(element, "xmlns:q")).to be_nil
31
+ end
32
+ end
33
+
34
+ describe ".assign" do
35
+ it "delegates xmlns declarations to the adapter" do
36
+ described_class.assign(element, "xmlns:r", "http://r.org")
37
+ expect(doc.to_xml).to include('xmlns:r="http://r.org"')
38
+ end
39
+
40
+ it "replaces the matching attribute in place" do
41
+ described_class.assign(element, "p:type", "rewritten")
42
+ expect(element["q:type"]).to eq("rewritten")
43
+ expect(element.attributes.size).to eq(2)
44
+ end
45
+
46
+ it "stores undeclared prefixes raw until they come into scope" do
47
+ described_class.assign(element, "z:type", "value")
48
+ expect(element["z:type"]).to be_nil
49
+
50
+ element.add_namespace("z", "http://z.org")
51
+ expect(element["z:type"]).to eq("value")
52
+ end
53
+ end
54
+
55
+ describe ".remove" do
56
+ it "removes by expanded name and returns the attribute" do
57
+ removed = described_class.remove(element, "p:type")
58
+ expect(removed.value).to eq("namespaced")
59
+ expect(element["q:type"]).to be_nil
60
+ expect(element["type"]).to eq("bare")
61
+ end
62
+
63
+ it "returns nil when nothing matches" do
64
+ expect(described_class.remove(element, "z:type")).to be_nil
65
+ end
66
+ end
67
+
68
+ describe ".attribute_test?" do
69
+ let(:attributes) { element.attributes }
70
+
71
+ it "applies Namespaces 1.0 §5.2 to bare names" do
72
+ bare = attributes.find { |a| a.value == "bare" }
73
+ namespaced = attributes.find { |a| a.value == "namespaced" }
74
+ expect(described_class.attribute_test?(element, bare, nil, "type"))
75
+ .to be(true)
76
+ expect(described_class.attribute_test?(element, namespaced, nil, "type"))
77
+ .to be(false)
78
+ end
79
+
80
+ it "matches prefixed tests by URI and accepts any local for nil" do
81
+ namespaced = attributes.find { |a| a.value == "namespaced" }
82
+ expect(described_class.attribute_test?(element, namespaced, "p", "type"))
83
+ .to be(true)
84
+ expect(described_class.attribute_test?(element, namespaced, "p", nil))
85
+ .to be(true)
86
+ expect(described_class.attribute_test?(element, namespaced, "z", "type"))
87
+ .to be(false)
88
+ end
89
+
90
+ it "treats :any as any namespace including none" do
91
+ bare = attributes.find { |a| a.value == "bare" }
92
+ expect(described_class.attribute_test?(element, bare, :any, "type"))
93
+ .to be(true)
94
+ end
95
+ end
96
+
97
+ describe ".attribute_test_uri?" do
98
+ it "matches the compile-time URI regardless of document prefixes" do
99
+ namespaced = element.attributes.find { |a| a.value == "namespaced" }
100
+ expect(described_class.attribute_test_uri?(
101
+ element, namespaced, "http://x.org", "type"
102
+ )).to be(true)
103
+ expect(described_class.attribute_test_uri?(
104
+ element, namespaced, "http://other.org", "type"
105
+ )).to be(false)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "moxml/c14n"
5
+
6
+ RSpec.describe "Moxml::C14n API" do
7
+ let(:ctx) { Moxml.new(:nokogiri) }
8
+
9
+ describe ".canonicalize (algorithm selector)" do
10
+ it "defaults to inclusive C14N 1.0" do
11
+ doc = ctx.parse("<root xmlns:foo='urn:foo'><foo:a/></root>")
12
+ default_result = Moxml::C14n.canonicalize(doc.root)
13
+ incl10_result = Moxml::C14n.canonicalize(doc.root, algorithm: :inclusive10)
14
+ expect(default_result).to eq(incl10_result)
15
+ end
16
+
17
+ it "raises ArgumentError for unknown algorithm" do
18
+ expect do
19
+ Moxml::C14n.canonicalize("<x/>", algorithm: :bogus)
20
+ end.to raise_error(ArgumentError, /unknown C14N algorithm/)
21
+ end
22
+
23
+ it "accepts inclusive_11 algorithm" do
24
+ doc = ctx.parse("<root/>")
25
+ result = Moxml::C14n.canonicalize(doc.root, algorithm: :inclusive11)
26
+ expect(result).to eq("<root></root>")
27
+ end
28
+
29
+ it "accepts exclusive_10 algorithm" do
30
+ doc = ctx.parse("<root xmlns:foo='urn:foo'><foo:a/></root>")
31
+ result = Moxml::C14n.canonicalize(doc.root, algorithm: :exclusive10)
32
+ # Exclusive renders ns where visibly used (on foo:a), not on root
33
+ expect(result).to include('<foo:a xmlns:foo="urn:foo">')
34
+ end
35
+ end
36
+
37
+ describe ".equivalent?" do
38
+ it "returns true for byte-identical inputs" do
39
+ expect(Moxml::C14n).to be_equivalent("<root>x</root>", "<root>x</root>")
40
+ end
41
+
42
+ it "returns true when only attribute whitespace differs" do
43
+ # Whitespace inside the tag is not significant — both parse identically.
44
+ a = %(<root attr="value"/>)
45
+ b = %(<root attr="value" />)
46
+ expect(Moxml::C14n).to be_equivalent(a, b)
47
+ end
48
+
49
+ it "returns false when text whitespace differs (text IS significant)" do
50
+ a = "<root><child>a</child> </root>"
51
+ b = "<root> <child>a</child></root>"
52
+ expect(Moxml::C14n).not_to be_equivalent(a, b)
53
+ end
54
+
55
+ it "returns true when only attribute order differs" do
56
+ a = %(<root a="1" b="2"/>)
57
+ b = %(<root b="2" a="1"/>)
58
+ expect(Moxml::C14n).to be_equivalent(a, b)
59
+ end
60
+
61
+ it "returns true when only namespace prefix differs" do
62
+ a = "<root xmlns:foo='urn:foo'><foo:a/></root>"
63
+ b = "<root xmlns:bar='urn:foo'><bar:a/></root>"
64
+ # Inclusive canonical form: prefix is part of qname → not equivalent
65
+ expect(Moxml::C14n).not_to be_equivalent(a, b)
66
+ end
67
+
68
+ it "returns false for different content" do
69
+ expect(Moxml::C14n).not_to be_equivalent("<root>a</root>", "<root>b</root>")
70
+ end
71
+
72
+ it "respects algorithm argument" do
73
+ a = "<root xmlns:foo='urn:foo'><foo:a/></root>"
74
+ b = "<root><a xmlns:foo='urn:foo'/></root>"
75
+ # Inclusive: ns on root vs on child → different
76
+ expect(Moxml::C14n).not_to be_equivalent(a, b, algorithm: :inclusive10)
77
+ end
78
+
79
+ it "accepts Moxml::Node inputs" do
80
+ doc1 = ctx.parse("<root/>")
81
+ doc2 = ctx.parse("<root></root>")
82
+ expect(Moxml::C14n).to be_equivalent(doc1.root, doc2.root)
83
+ end
84
+ end
85
+
86
+ describe "convenience methods" do
87
+ it ".canonicalize_inclusive10 matches default" do
88
+ doc = ctx.parse("<root/>")
89
+ expect(Moxml::C14n.canonicalize_inclusive10(doc.root))
90
+ .to eq(Moxml::C14n.canonicalize(doc.root))
91
+ end
92
+
93
+ it ".canonicalize_inclusive11 produces output" do
94
+ doc = ctx.parse("<root/>")
95
+ expect(Moxml::C14n.canonicalize_inclusive11(doc.root)).to eq("<root></root>")
96
+ end
97
+
98
+ it ".canonicalize_exclusive accepts inclusive_namespaces" do
99
+ doc = ctx.parse("<root xmlns:foo='urn:foo'/>")
100
+ result = Moxml::C14n.canonicalize_exclusive(doc.root, inclusive_namespaces: ["foo"])
101
+ # Even though foo is not visibly used, inclusive list forces render
102
+ expect(result).to include('xmlns:foo="urn:foo"')
103
+ end
104
+ end
105
+
106
+ describe "escape helpers (backward compat)" do
107
+ it ".escape_text escapes & < >" do
108
+ expect(Moxml::C14n.escape_text("a & b < c > d"))
109
+ .to eq("a &amp; b &lt; c &gt; d")
110
+ end
111
+
112
+ it ".escape_attribute escapes quotes and whitespace" do
113
+ expect(Moxml::C14n.escape_attribute(%(a"b\tc\nd)))
114
+ .to eq(%(a&quot;b&#x9;c&#xA;d))
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "moxml/signature"
5
+
6
+ RSpec.describe "Moxml C14N engine" do
7
+ describe Moxml::C14n::Exclusive do
8
+ let(:ctx) { Moxml.new(:nokogiri) }
9
+ let(:canon) { described_class.new }
10
+
11
+ it "renders a simple element without namespaces" do
12
+ doc = ctx.parse("<root>hello</root>")
13
+ expect(canon.canonicalize(doc.root)).to eq("<root>hello</root>")
14
+ end
15
+
16
+ it "preserves nested element structure" do
17
+ doc = ctx.parse("<root><child>text</child></root>")
18
+ expect(canon.canonicalize(doc.root)).to eq("<root><child>text</child></root>")
19
+ end
20
+
21
+ it "renders declared namespaces" do
22
+ doc = ctx.parse(<<~XML.strip)
23
+ <root xmlns:foo="http://example.com/foo"><foo:child>x</foo:child></root>
24
+ XML
25
+ result = canon.canonicalize(doc.root)
26
+ # Exclusive C14N renders the namespace on the element where it's visibly
27
+ # used (foo:child), not on ancestors that don't use it.
28
+ expect(result).to include('<foo:child xmlns:foo="http://example.com/foo">')
29
+ expect(result).to include("</foo:child>")
30
+ end
31
+
32
+ it "excludes ancestor-inherited namespaces when not visibly used (exclusive)" do
33
+ doc = ctx.parse(<<~XML.strip)
34
+ <outer xmlns:unused="http://example.com/unused"><inner>text</inner></outer>
35
+ XML
36
+ inner = doc.at_xpath("//inner")
37
+ result = canon.canonicalize(inner)
38
+ expect(result).to eq("<inner>text</inner>")
39
+ end
40
+
41
+ it "escapes special characters in text content" do
42
+ doc = ctx.parse("<root>a&lt;b&gt;c&amp;d</root>")
43
+ expect(canon.canonicalize(doc.root)).to eq("<root>a&lt;b&gt;c&amp;d</root>")
44
+ end
45
+
46
+ it "escapes special characters in attribute values" do
47
+ doc = ctx.parse(%(<root attr="a&amp;b&quot;c"/>))
48
+ expect(canon.canonicalize(doc.root))
49
+ .to include(%(attr="a&amp;b&quot;c"))
50
+ end
51
+
52
+ it "sorts attributes by namespace URI then local name" do
53
+ doc = ctx.parse(%(<root b="2" a="1" c="3"/>))
54
+ result = canon.canonicalize(doc.root)
55
+ expect(result).to include('a="1" b="2" c="3"')
56
+ end
57
+
58
+ it "omits comments by default" do
59
+ doc = ctx.parse("<root>a<!-- comment -->b</root>")
60
+ expect(canon.canonicalize(doc.root)).to eq("<root>ab</root>")
61
+ end
62
+
63
+ it "includes comments when with_comments is true" do
64
+ doc = ctx.parse("<root>a<!-- comment -->b</root>")
65
+ expect(canon.canonicalize(doc.root, with_comments: true))
66
+ .to eq("<root>a<!-- comment -->b</root>")
67
+ end
68
+ end
69
+
70
+ describe ".escape_text" do
71
+ it "escapes &, <, >" do
72
+ expect(Moxml::C14n.escape_text("a & b < c > d"))
73
+ .to eq("a &amp; b &lt; c &gt; d")
74
+ end
75
+
76
+ it "escapes bare CR as &#xD;" do
77
+ expect(Moxml::C14n.escape_text("a\rb"))
78
+ .to eq("a&#xD;b")
79
+ end
80
+ end
81
+
82
+ describe ".escape_attribute" do
83
+ it "escapes quotes and tabs/newlines" do
84
+ expect(Moxml::C14n.escape_attribute(%(a"b\tc\nd)))
85
+ .to eq(%(a&quot;b&#x9;c&#xA;d))
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "moxml/c14n"
5
+
6
+ # Edge cases for comment and processing-instruction handling per
7
+ # W3C C14N 1.0 §2.6, §2.7.
8
+ RSpec.describe "C14N comments and PIs" do
9
+ let(:ctx) { Moxml.new(:nokogiri) }
10
+
11
+ describe "comments inside elements" do
12
+ it "omits comments by default" do
13
+ doc = ctx.parse("<root>a<!-- c -->b</root>")
14
+ expect(Moxml::C14n.canonicalize(doc.root)).to eq("<root>ab</root>")
15
+ end
16
+
17
+ it "includes comments when with_comments: true" do
18
+ doc = ctx.parse("<root>a<!-- c -->b</root>")
19
+ expect(Moxml::C14n.canonicalize(doc.root, with_comments: true))
20
+ .to eq("<root>a<!-- c -->b</root>")
21
+ end
22
+
23
+ it "preserves comment content verbatim" do
24
+ doc = ctx.parse("<root><!-- &amp; <stuff> --></root>")
25
+ expect(Moxml::C14n.canonicalize(doc.root, with_comments: true))
26
+ .to eq("<root><!-- &amp; <stuff> --></root>")
27
+ end
28
+ end
29
+
30
+ describe "processing instructions" do
31
+ it "always renders PIs (not affected by with_comments)" do
32
+ doc = ctx.parse("<root><?pi data?>x</root>")
33
+ expect(Moxml::C14n.canonicalize(doc.root))
34
+ .to eq("<root><?pi data?>x</root>")
35
+ end
36
+
37
+ it "renders PI without data when data is empty" do
38
+ doc = ctx.parse("<root><?pi?>x</root>")
39
+ expect(Moxml::C14n.canonicalize(doc.root))
40
+ .to eq("<root><?pi?>x</root>")
41
+ end
42
+
43
+ it "preserves PI target and data verbatim" do
44
+ doc = ctx.parse(%(<root><?xml-stylesheet href="a.css" type="text/css"?>x</root>))
45
+ expect(Moxml::C14n.canonicalize(doc.root))
46
+ .to include(%(<?xml-stylesheet href="a.css" type="text/css"?>))
47
+ end
48
+ end
49
+
50
+ describe "PIs at document level" do
51
+ # TODO.c14n/09: Document-level PIs (outside the document element) are
52
+ # not yet rendered correctly. The moxml PI parser mis-parses document-level
53
+ # PIs, and the DataModel.from_document path drops them. Fixing requires
54
+ # adapter-level work and is tracked separately.
55
+ it "renders PIs outside the document element (KNOWN LIMITATION)" do
56
+ skip "document-level PI rendering not yet implemented"
57
+ end
58
+ end
59
+
60
+ describe "comments at document level" do
61
+ it "renders document-level comments when with_comments: true (KNOWN LIMITATION)" do
62
+ skip "document-level comment rendering not yet implemented"
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "moxml/signature"
5
+
6
+ RSpec.describe Moxml::C14n::Inclusive10 do
7
+ let(:ctx) { Moxml.new(:nokogiri) }
8
+ let(:canon) { described_class.new }
9
+
10
+ it "renders a simple element" do
11
+ doc = ctx.parse("<root>hello</root>")
12
+ expect(canon.canonicalize(doc.root)).to eq("<root>hello</root>")
13
+ end
14
+
15
+ it "attracts ancestor namespaces at the apex (the key inclusive behavior)" do
16
+ xml = <<~XML.strip
17
+ <outer xmlns:foo="http://example.com/foo" xmlns:bar="http://example.com/bar">
18
+ <inner x="1"><foo:child>hello</foo:child></inner>
19
+ </outer>
20
+ XML
21
+ doc = ctx.parse(xml)
22
+ inner = doc.at_xpath("//*[local-name()='inner']")
23
+ result = canon.canonicalize(inner)
24
+ # Both inherited namespaces are rendered on <inner>, even though
25
+ # <inner> itself does not visibly use them. This is the inclusive
26
+ # "ancestor attraction" behavior.
27
+ expect(result).to include('xmlns:bar="http://example.com/bar"')
28
+ expect(result).to include('xmlns:foo="http://example.com/foo"')
29
+ end
30
+
31
+ it "renders each namespace declaration only once per chain" do
32
+ xml = <<~XML.strip
33
+ <root xmlns:a="http://example.com/a">
34
+ <child xmlns:a="http://example.com/a"><grand/></child>
35
+ </root>
36
+ XML
37
+ doc = ctx.parse(xml)
38
+ result = canon.canonicalize(doc.root)
39
+ expect(result.scan('xmlns:a="http://example.com/a"').count).to eq(1)
40
+ end
41
+
42
+ it "re-renders a namespace when re-declared with a different URI" do
43
+ xml = <<~XML.strip
44
+ <root xmlns:a="http://example.com/a">
45
+ <child xmlns:a="http://example.com/other"><grand/></child>
46
+ </root>
47
+ XML
48
+ doc = ctx.parse(xml)
49
+ result = canon.canonicalize(doc.root)
50
+ expect(result.scan("xmlns:a=").count).to eq(2)
51
+ end
52
+
53
+ it "renders the default namespace at apex when inherited" do
54
+ xml = '<outer xmlns="http://example.com"><inner/></outer>'
55
+ doc = ctx.parse(xml)
56
+ inner = doc.at_xpath("//*[local-name()='inner']")
57
+ result = canon.canonicalize(inner)
58
+ expect(result).to include('xmlns="http://example.com"')
59
+ end
60
+ end