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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +9 -0
- data/.github/workflows/windows.yml +62 -0
- data/Gemfile +3 -1
- data/README.adoc +133 -6
- data/docs/signature/algorithms.md +108 -0
- data/docs/signature/architecture.md +148 -0
- data/docs/signature/c14n.md +110 -0
- data/docs/signature/examples.md +109 -0
- data/docs/signature/flows.md +132 -0
- data/docs/signature/quick-reference.md +115 -0
- data/docs/signature/security.md +143 -0
- data/examples/signature/auto_key_extraction.rb +24 -0
- data/examples/signature/enveloped_rsa.rb +45 -0
- data/examples/signature/hmac.rb +33 -0
- data/lib/compat/opal/moxml_boot.rb +5 -0
- data/lib/moxml/adapter/base.rb +53 -49
- data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
- data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
- data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
- data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
- data/lib/moxml/adapter/customized_leptris.rb +16 -0
- data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
- data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
- data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
- data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
- data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
- data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
- data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
- data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
- data/lib/moxml/adapter/customized_oga.rb +2 -0
- data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
- data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
- data/lib/moxml/adapter/headed_ox.rb +9 -128
- data/lib/moxml/adapter/leptris.rb +965 -0
- data/lib/moxml/adapter/libxml.rb +131 -99
- data/lib/moxml/adapter/nokogiri.rb +29 -8
- data/lib/moxml/adapter/oga.rb +91 -54
- data/lib/moxml/adapter/ox.rb +93 -137
- data/lib/moxml/adapter/rexml.rb +61 -50
- data/lib/moxml/adapter.rb +2 -1
- data/lib/moxml/attribute.rb +8 -4
- data/lib/moxml/attribute_resolver.rb +189 -0
- data/lib/moxml/c14n/attribute_handler.rb +71 -0
- data/lib/moxml/c14n/character_encoder.rb +29 -0
- data/lib/moxml/c14n/data_model.rb +145 -0
- data/lib/moxml/c14n/exclusive.rb +188 -0
- data/lib/moxml/c14n/inclusive_10.rb +42 -0
- data/lib/moxml/c14n/inclusive_11.rb +21 -0
- data/lib/moxml/c14n/namespace_context.rb +68 -0
- data/lib/moxml/c14n/namespace_handler.rb +71 -0
- data/lib/moxml/c14n/node.rb +42 -0
- data/lib/moxml/c14n/nodes/attribute_node.rb +50 -0
- data/lib/moxml/c14n/nodes/comment_node.rb +29 -0
- data/lib/moxml/c14n/nodes/element_node.rb +62 -0
- data/lib/moxml/c14n/nodes/namespace_node.rb +41 -0
- data/lib/moxml/c14n/nodes/processing_instruction_node.rb +30 -0
- data/lib/moxml/c14n/nodes/root_node.rb +22 -0
- data/lib/moxml/c14n/nodes/text_node.rb +29 -0
- data/lib/moxml/c14n/nodes.rb +17 -0
- data/lib/moxml/c14n/processor.rb +118 -0
- data/lib/moxml/c14n/writer.rb +121 -0
- data/lib/moxml/c14n/xml_base_handler.rb +140 -0
- data/lib/moxml/c14n.rb +96 -0
- data/lib/moxml/config.rb +29 -3
- data/lib/moxml/context.rb +12 -0
- data/lib/moxml/element.rb +29 -12
- data/lib/moxml/entity/reference.rb +28 -0
- data/lib/moxml/entity.rb +125 -0
- data/lib/moxml/node.rb +90 -1
- data/lib/moxml/sax/namespace_splitter.rb +5 -2
- data/lib/moxml/signature/algorithms/base64_transform.rb +40 -0
- data/lib/moxml/signature/algorithms/canonicalization_base.rb +87 -0
- data/lib/moxml/signature/algorithms/digest_base.rb +51 -0
- data/lib/moxml/signature/algorithms/dsa_sha.rb +107 -0
- data/lib/moxml/signature/algorithms/ecdsa_sha.rb +125 -0
- data/lib/moxml/signature/algorithms/enveloped_signature_transform.rb +79 -0
- data/lib/moxml/signature/algorithms/exc_c14n_10.rb +23 -0
- data/lib/moxml/signature/algorithms/hmac_sha.rb +117 -0
- data/lib/moxml/signature/algorithms/inclusive_c14n_10.rb +21 -0
- data/lib/moxml/signature/algorithms/inclusive_c14n_11.rb +23 -0
- data/lib/moxml/signature/algorithms/rsa_pkcs1_sha.rb +56 -0
- data/lib/moxml/signature/algorithms/sha1.rb +13 -0
- data/lib/moxml/signature/algorithms/sha224.rb +13 -0
- data/lib/moxml/signature/algorithms/sha256.rb +13 -0
- data/lib/moxml/signature/algorithms/sha384.rb +13 -0
- data/lib/moxml/signature/algorithms/sha512.rb +13 -0
- data/lib/moxml/signature/algorithms/signature_method_base.rb +58 -0
- data/lib/moxml/signature/algorithms/transform_base.rb +40 -0
- data/lib/moxml/signature/algorithms.rb +113 -0
- data/lib/moxml/signature/errors.rb +99 -0
- data/lib/moxml/signature/key_extractor.rb +172 -0
- data/lib/moxml/signature/model/algorithm_method.rb +18 -0
- data/lib/moxml/signature/model/digest_method.rb +15 -0
- data/lib/moxml/signature/model/key/dsa_key_value.rb +28 -0
- data/lib/moxml/signature/model/key/ec_key_value.rb +22 -0
- data/lib/moxml/signature/model/key/rsa_key_value.rb +19 -0
- data/lib/moxml/signature/model/key/x509_data.rb +32 -0
- data/lib/moxml/signature/model/key/x509_digest.rb +19 -0
- data/lib/moxml/signature/model/key/x509_issuer_serial.rb +19 -0
- data/lib/moxml/signature/model/key_info.rb +21 -0
- data/lib/moxml/signature/model/key_value.rb +20 -0
- data/lib/moxml/signature/model/object_element.rb +18 -0
- data/lib/moxml/signature/model/reference.rb +22 -0
- data/lib/moxml/signature/model/signature.rb +21 -0
- data/lib/moxml/signature/model/signature_value.rb +16 -0
- data/lib/moxml/signature/model/signed_info.rb +20 -0
- data/lib/moxml/signature/model/transform.rb +16 -0
- data/lib/moxml/signature/model/transforms.rb +36 -0
- data/lib/moxml/signature/model.rb +35 -0
- data/lib/moxml/signature/parser.rb +270 -0
- data/lib/moxml/signature/reference_resolver.rb +79 -0
- data/lib/moxml/signature/reference_result.rb +22 -0
- data/lib/moxml/signature/serializer.rb +163 -0
- data/lib/moxml/signature/signer.rb +73 -0
- data/lib/moxml/signature/single_verification_result.rb +31 -0
- data/lib/moxml/signature/transform_pipeline.rb +106 -0
- data/lib/moxml/signature/verification_result.rb +27 -0
- data/lib/moxml/signature/verifier.rb +129 -0
- data/lib/moxml/signature.rb +94 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_emitter.rb +71 -0
- data/lib/moxml/xpath/compiler.rb +60 -7
- data/lib/moxml/xpath/parser.rb +22 -15
- data/lib/moxml.rb +5 -0
- data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
- data/reference-docs/w3c-xmldsig-core.md +400 -0
- data/spec/examples/readme_examples_spec.rb +0 -4
- data/spec/examples/xpath_examples_spec.rb +0 -11
- data/spec/fixtures/xmldsig/keys/rsa_private.pem +28 -0
- data/spec/fixtures/xmldsig/keys/rsa_public.pem +9 -0
- data/spec/fixtures/xmldsig/keys/rsa_ref.pem +15 -0
- data/spec/fixtures/xmldsig/keys/rsa_ref.pub +6 -0
- data/spec/fixtures/xmldsig/sign2-doc.xml +6 -0
- data/spec/fixtures/xmldsig/sign2-result.xml +25 -0
- data/spec/fixtures/xmldsig/sign3-result.xml +39 -0
- data/spec/integration/all_adapters_spec.rb +17 -0
- data/spec/integration/sax_parity_spec.rb +58 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -10
- data/spec/integration/shared_examples/integration_workflows.rb +0 -10
- data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
- data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
- data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +0 -8
- data/spec/moxml/adapter/leptris_spec.rb +75 -0
- data/spec/moxml/adapter/ox_spec.rb +20 -6
- data/spec/moxml/adapter/platform_spec.rb +15 -2
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
- data/spec/moxml/attribute_resolver_spec.rb +108 -0
- data/spec/moxml/c14n/api_spec.rb +117 -0
- data/spec/moxml/c14n/c14n_spec.rb +88 -0
- data/spec/moxml/c14n/comments_pis_spec.rb +65 -0
- data/spec/moxml/c14n/inclusive10_spec.rb +60 -0
- data/spec/moxml/c14n/namespace_edge_cases_spec.rb +88 -0
- data/spec/moxml/c14n/xml_attributes_spec.rb +54 -0
- data/spec/moxml/doctype_spec.rb +1 -1
- data/spec/moxml/entity_spec.rb +76 -0
- data/spec/moxml/node_spec.rb +104 -0
- data/spec/moxml/sax_entity_parity_spec.rb +358 -0
- data/spec/moxml/signature/algorithms/base64_transform_spec.rb +26 -0
- data/spec/moxml/signature/algorithms/digest_base_spec.rb +72 -0
- data/spec/moxml/signature/algorithms/dsa_sha_spec.rb +46 -0
- data/spec/moxml/signature/algorithms/ecdsa_sha_spec.rb +72 -0
- data/spec/moxml/signature/algorithms/enveloped_signature_transform_spec.rb +40 -0
- data/spec/moxml/signature/algorithms/hmac_sha_spec.rb +71 -0
- data/spec/moxml/signature/algorithms/rsa_pkcs1_sha_spec.rb +55 -0
- data/spec/moxml/signature/algorithms_spec.rb +76 -0
- data/spec/moxml/signature/cross_verify_spec.rb +64 -0
- data/spec/moxml/signature/edge_cases_spec.rb +284 -0
- data/spec/moxml/signature/fixtures_spec.rb +80 -0
- data/spec/moxml/signature/key_extractor_spec.rb +96 -0
- data/spec/moxml/signature/model_spec.rb +54 -0
- data/spec/moxml/signature/round_trip_spec.rb +113 -0
- data/spec/moxml/xml_emitter_spec.rb +64 -0
- data/spec/moxml/xpath/axes_spec.rb +72 -0
- data/spec/moxml/xpath/parser_spec.rb +6 -0
- data/spec/moxml/xpath_capabilities_spec.rb +5 -3
- data/spec/performance/benchmark_spec.rb +13 -6
- data/spec/performance/thread_safety_spec.rb +0 -4
- data/spec/spec_helper.rb +5 -1
- metadata +130 -2
data/lib/moxml/adapter/rexml.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "rexml/document"
|
|
4
4
|
require "rexml/xpath"
|
|
5
|
+
require "rexml/streamlistener"
|
|
5
6
|
require "set" unless RUBY_ENGINE == "opal"
|
|
6
7
|
require "stringio" if RUBY_ENGINE == "opal"
|
|
7
8
|
|
|
@@ -60,18 +61,21 @@ module Moxml
|
|
|
60
61
|
# @param handler [Moxml::SAX::Handler] Moxml SAX handler
|
|
61
62
|
# @return [void]
|
|
62
63
|
def sax_parse(xml, handler)
|
|
63
|
-
require "rexml/parsers/
|
|
64
|
+
require "rexml/parsers/streamparser"
|
|
64
65
|
require "rexml/source"
|
|
65
|
-
require "stringio"
|
|
66
66
|
|
|
67
|
-
bridge =
|
|
67
|
+
bridge = REXMLStreamBridge.new(handler)
|
|
68
68
|
|
|
69
69
|
xml_string = xml.is_a?(IO) || xml.is_a?(StringIO) ? xml.read : xml.to_s
|
|
70
|
-
source = ::REXML::IOSource.new(StringIO.new(xml_string))
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
# StreamParser (rather than SAX2Parser) is used because
|
|
72
|
+
# SAX2Parser normalizes plain "&#NN;" and "&#NN;" attribute
|
|
73
|
+
# literals to the same raw string, making spec-correct decoding
|
|
74
|
+
# impossible after the fact. StreamParser delivers values in
|
|
75
|
+
# the same decoded form as REXML's DOM.
|
|
76
|
+
handler.on_start_document
|
|
77
|
+
::REXML::Parsers::StreamParser.new(xml_string, bridge).parse
|
|
78
|
+
handler.on_end_document
|
|
75
79
|
rescue ::REXML::ParseException => e
|
|
76
80
|
error = Moxml::ParseError.new(e.message, line: e.line)
|
|
77
81
|
handler.on_error(error)
|
|
@@ -117,15 +121,12 @@ module Moxml
|
|
|
117
121
|
def create_native_doctype(name, external_id, system_id)
|
|
118
122
|
return nil unless name
|
|
119
123
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
::REXML::DocType.new(parts.join(" "))
|
|
124
|
+
external = if external_id
|
|
125
|
+
system_id ? %(PUBLIC "#{external_id}" "#{system_id}") : %(PUBLIC "#{external_id}")
|
|
126
|
+
elsif system_id
|
|
127
|
+
%(SYSTEM "#{system_id}")
|
|
128
|
+
end
|
|
129
|
+
external ? ::REXML::DocType.new(name.to_s, external) : ::REXML::DocType.new(name.to_s)
|
|
129
130
|
end
|
|
130
131
|
|
|
131
132
|
def set_root(doc, element)
|
|
@@ -250,9 +251,17 @@ module Moxml
|
|
|
250
251
|
def attributes(element)
|
|
251
252
|
return [] unless element.is_a?(::REXML::Element)
|
|
252
253
|
|
|
253
|
-
#
|
|
254
|
-
|
|
255
|
-
|
|
254
|
+
# Each real Attribute node, not Attributes#values: when two
|
|
255
|
+
# attributes share a local name across namespaces, REXML's
|
|
256
|
+
# values leaks its internal prefix-grouping Hash alongside
|
|
257
|
+
# the Attribute objects (issue #95).
|
|
258
|
+
result = []
|
|
259
|
+
element.attributes.each_attribute do |attr|
|
|
260
|
+
next if attr.prefix.to_s.start_with?("xmlns")
|
|
261
|
+
|
|
262
|
+
result << attr
|
|
263
|
+
end
|
|
264
|
+
result
|
|
256
265
|
end
|
|
257
266
|
|
|
258
267
|
def attribute_element(attribute)
|
|
@@ -271,10 +280,17 @@ module Moxml
|
|
|
271
280
|
element = attribute.element
|
|
272
281
|
element.attributes.delete(old_name)
|
|
273
282
|
element.attributes << attribute
|
|
283
|
+
attribute
|
|
274
284
|
end
|
|
275
285
|
|
|
276
286
|
def set_attribute_value(attribute, value)
|
|
277
287
|
attribute.normalized = value
|
|
288
|
+
attribute
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def remove_attribute_native(attribute)
|
|
292
|
+
attribute.element.attributes.delete(attribute.expanded_name)
|
|
293
|
+
attribute
|
|
278
294
|
end
|
|
279
295
|
|
|
280
296
|
def get_attribute(element, name)
|
|
@@ -468,9 +484,11 @@ module Moxml
|
|
|
468
484
|
element.add_namespace(prefix,
|
|
469
485
|
ns.value)
|
|
470
486
|
end
|
|
487
|
+
# Renames the node in place (works for elements and
|
|
488
|
+
# attributes alike); the caller keeps tracking the same
|
|
489
|
+
# native.
|
|
471
490
|
element.name = "#{prefix}:#{element.name}"
|
|
472
|
-
|
|
473
|
-
::REXML::Attribute.new(prefix, ns.value, owner)
|
|
491
|
+
element
|
|
474
492
|
end
|
|
475
493
|
|
|
476
494
|
def namespace_prefix(node)
|
|
@@ -518,11 +536,17 @@ module Moxml
|
|
|
518
536
|
end
|
|
519
537
|
|
|
520
538
|
def doctype_external_id(native)
|
|
521
|
-
|
|
539
|
+
# Constructed doctypes keep the identifier as a raw
|
|
540
|
+
# external_id string; parsed ones populate public directly
|
|
541
|
+
native.public ||
|
|
542
|
+
native.external_id&.match(/PUBLIC\s+"([^"]*)"/)&.[](1)
|
|
522
543
|
end
|
|
523
544
|
|
|
524
545
|
def doctype_system_id(native)
|
|
525
|
-
native.system
|
|
546
|
+
return native.system if native.system
|
|
547
|
+
|
|
548
|
+
quoted = native.external_id&.scan(/"([^"]*)"/)
|
|
549
|
+
quoted&.last&.first
|
|
526
550
|
end
|
|
527
551
|
|
|
528
552
|
# not used at the moment
|
|
@@ -642,29 +666,33 @@ module Moxml
|
|
|
642
666
|
end
|
|
643
667
|
end
|
|
644
668
|
|
|
645
|
-
# Bridge between REXML
|
|
669
|
+
# Bridge between REXML StreamParser and Moxml SAX
|
|
646
670
|
#
|
|
647
|
-
#
|
|
671
|
+
# StreamParser (rather than SAX2Parser) is used because SAX2Parser
|
|
672
|
+
# normalizes plain "&#NN;" and "&#NN;" attribute literals to
|
|
673
|
+
# the same raw string, which makes spec-correct decoding impossible
|
|
674
|
+
# after the fact. StreamParser delivers attribute values in the
|
|
675
|
+
# same decoded form as REXML's DOM.
|
|
648
676
|
#
|
|
649
677
|
# @private
|
|
650
|
-
class
|
|
678
|
+
class REXMLStreamBridge
|
|
679
|
+
include ::REXML::StreamListener
|
|
651
680
|
include Moxml::SAX::NamespaceSplitter
|
|
652
681
|
|
|
653
682
|
def initialize(handler)
|
|
654
683
|
@handler = handler
|
|
655
684
|
end
|
|
656
685
|
|
|
657
|
-
|
|
658
|
-
def start_element(_uri, _localname, qname, attributes)
|
|
686
|
+
def tag_start(name, attributes)
|
|
659
687
|
attr_hash, ns_hash = split_attributes_and_namespaces(attributes)
|
|
660
|
-
@handler.on_start_element(
|
|
688
|
+
@handler.on_start_element(name, attr_hash, ns_hash)
|
|
661
689
|
end
|
|
662
690
|
|
|
663
|
-
def
|
|
664
|
-
@handler.on_end_element(
|
|
691
|
+
def tag_end(name)
|
|
692
|
+
@handler.on_end_element(name)
|
|
665
693
|
end
|
|
666
694
|
|
|
667
|
-
def
|
|
695
|
+
def text(text)
|
|
668
696
|
@handler.on_characters(text)
|
|
669
697
|
end
|
|
670
698
|
|
|
@@ -676,26 +704,9 @@ module Moxml
|
|
|
676
704
|
@handler.on_comment(text)
|
|
677
705
|
end
|
|
678
706
|
|
|
679
|
-
def
|
|
707
|
+
def instruction(target, data)
|
|
680
708
|
@handler.on_processing_instruction(target, data || "")
|
|
681
709
|
end
|
|
682
|
-
|
|
683
|
-
def start_document
|
|
684
|
-
@handler.on_start_document
|
|
685
|
-
end
|
|
686
|
-
|
|
687
|
-
def end_document
|
|
688
|
-
@handler.on_end_document
|
|
689
|
-
end
|
|
690
|
-
|
|
691
|
-
# REXML calls these but we don't need to handle them
|
|
692
|
-
def xmldecl(version, encoding, standalone)
|
|
693
|
-
# XML declaration - we don't need to do anything
|
|
694
|
-
end
|
|
695
|
-
|
|
696
|
-
def progress(position)
|
|
697
|
-
# Progress callback - we don't need to do anything
|
|
698
|
-
end
|
|
699
710
|
end
|
|
700
711
|
end
|
|
701
712
|
end
|
data/lib/moxml/adapter.rb
CHANGED
|
@@ -7,8 +7,9 @@ module Moxml
|
|
|
7
7
|
autoload :CustomizedOx, "moxml/adapter/customized_ox"
|
|
8
8
|
autoload :CustomizedRexml, "moxml/adapter/customized_rexml"
|
|
9
9
|
autoload :CustomizedLibxml, "moxml/adapter/customized_libxml"
|
|
10
|
+
autoload :CustomizedLeptris, "moxml/adapter/customized_leptris"
|
|
10
11
|
|
|
11
|
-
AVAILABLE_ADAPTERS = %i[nokogiri oga rexml ox headed_ox libxml].freeze
|
|
12
|
+
AVAILABLE_ADAPTERS = %i[nokogiri oga rexml ox headed_ox libxml leptris].freeze
|
|
12
13
|
|
|
13
14
|
# Adapters that work under the Opal (JavaScript) runtime.
|
|
14
15
|
# Oga is pure Ruby and designed with Opal compatibility in mind — it is the
|
data/lib/moxml/attribute.rb
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
module Moxml
|
|
4
4
|
class Attribute < Node
|
|
5
5
|
def name
|
|
6
|
-
@native
|
|
6
|
+
adapter.attribute_name(@native)
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def name=(new_name)
|
|
10
|
-
|
|
10
|
+
# Mutation return contract (Adapter::Base): returns the native
|
|
11
|
+
# to keep tracking — same object for in-place adapters, fresh
|
|
12
|
+
# object for value-object adapters (leptris).
|
|
13
|
+
@native = adapter.set_attribute_name(@native, new_name)
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
# Returns the primary identifier for this attribute (its name)
|
|
@@ -44,7 +47,8 @@ module Moxml
|
|
|
44
47
|
end
|
|
45
48
|
|
|
46
49
|
def namespace=(ns)
|
|
47
|
-
|
|
50
|
+
# See name= for the mutation return contract.
|
|
51
|
+
@native = adapter.set_namespace(@native, ns&.native)
|
|
48
52
|
end
|
|
49
53
|
|
|
50
54
|
def element
|
|
@@ -53,7 +57,7 @@ module Moxml
|
|
|
53
57
|
end
|
|
54
58
|
|
|
55
59
|
def remove
|
|
56
|
-
adapter.
|
|
60
|
+
adapter.remove_attribute_native(@native)
|
|
57
61
|
if @parent_node.is_a?(Moxml::Element)
|
|
58
62
|
@parent_node.invalidate_attribute_cache!
|
|
59
63
|
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
# Canonical, adapter-independent attribute-name resolution.
|
|
5
|
+
#
|
|
6
|
+
# XML Namespaces 1.0 semantics, implemented once on the Moxml object
|
|
7
|
+
# model (the unified interface); adapters provide raw access only:
|
|
8
|
+
#
|
|
9
|
+
# - An attribute's expanded name is (namespace URI or none) + local
|
|
10
|
+
# name. Unprefixed attributes are never in a namespace — the
|
|
11
|
+
# default namespace declaration does not apply to attributes
|
|
12
|
+
# (Namespaces 1.0 §5.2).
|
|
13
|
+
# - "prefix:local" resolves the prefix against the element's
|
|
14
|
+
# in-scope declarations and matches by expanded name: an attribute
|
|
15
|
+
# written as q:type matches a lookup as p:type whenever both
|
|
16
|
+
# prefixes are bound to the same URI.
|
|
17
|
+
# - "xml" is prebound to the XML namespace URI; it never needs a
|
|
18
|
+
# declaration (Namespaces 1.0 §4).
|
|
19
|
+
# - "xmlns"/"xmlns:*" are declarations, not attributes: never match.
|
|
20
|
+
# - An undeclared prefix names nothing: nil. No silent prefix-string
|
|
21
|
+
# or bare-name fallback.
|
|
22
|
+
#
|
|
23
|
+
# When several attributes match (only possible in namespace-ill-formed
|
|
24
|
+
# documents), the first in document order wins, matching every
|
|
25
|
+
# adapter's attributes() ordering contract.
|
|
26
|
+
module AttributeResolver
|
|
27
|
+
XML_NAMESPACE_URI = "http://www.w3.org/XML/1998/namespace"
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
# @return [Moxml::Attribute, nil] the matching attribute wrapper
|
|
32
|
+
def resolve(element, name)
|
|
33
|
+
name = name.to_s
|
|
34
|
+
if name.include?(":")
|
|
35
|
+
prefix, local = name.split(":", 2)
|
|
36
|
+
return nil if prefix == "xmlns"
|
|
37
|
+
|
|
38
|
+
uri = prefix_uri(element, prefix)
|
|
39
|
+
return nil if uri.nil?
|
|
40
|
+
|
|
41
|
+
element.attributes.find do |attr|
|
|
42
|
+
local_name(attr.name) == local && attribute_uri(element, attr) == uri
|
|
43
|
+
end
|
|
44
|
+
else
|
|
45
|
+
element.attributes.find do |attr|
|
|
46
|
+
local_name(attr.name) == name && attribute_uri(element, attr).nil?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# URI a prefix is bound to for lookups on this element, or nil when
|
|
52
|
+
# the prefix is undeclared.
|
|
53
|
+
def prefix_uri(element, prefix)
|
|
54
|
+
return XML_NAMESPACE_URI if prefix == "xml"
|
|
55
|
+
|
|
56
|
+
element.in_scope_namespaces
|
|
57
|
+
.find { |candidate| candidate.prefix == prefix }&.uri
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Assign a value to the attribute named by XML-correct resolution,
|
|
61
|
+
# mirroring #resolve:
|
|
62
|
+
#
|
|
63
|
+
# - "xmlns"/"xmlns:*" are namespace declarations, not attributes:
|
|
64
|
+
# delegated verbatim to the adapter (declaration creation is
|
|
65
|
+
# adapter territory).
|
|
66
|
+
# - A bare name replaces the no-namespace attribute only — a
|
|
67
|
+
# namespaced attribute sharing the local name is left alone.
|
|
68
|
+
# - A prefixed name replaces by expanded name: assigning p:x
|
|
69
|
+
# overwrites an attribute spelled q:x when both prefixes are
|
|
70
|
+
# bound to the same URI.
|
|
71
|
+
#
|
|
72
|
+
# When nothing matches, the attribute is created with the given
|
|
73
|
+
# spelling. A prefix not yet in scope is stored raw — builders
|
|
74
|
+
# legitimately write detached children before attaching them under
|
|
75
|
+
# the declaring ancestor, and reads resolve by expanded name once
|
|
76
|
+
# the prefix is in scope (matching nokogiri/libxml creation
|
|
77
|
+
# behavior; namespace validity is a property of the serialized
|
|
78
|
+
# document).
|
|
79
|
+
#
|
|
80
|
+
# @return [String] the assigned value
|
|
81
|
+
def assign(element, name, value)
|
|
82
|
+
name = name.to_s
|
|
83
|
+
adapter = element.context.config.adapter
|
|
84
|
+
if name == "xmlns" || name.start_with?("xmlns:")
|
|
85
|
+
adapter.set_attribute(element.native, name, value)
|
|
86
|
+
element.invalidate_attribute_cache!
|
|
87
|
+
return value
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
existing = resolve(element, name)
|
|
91
|
+
if existing
|
|
92
|
+
existing.value = value
|
|
93
|
+
return value
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
adapter.set_attribute(element.native, name, value)
|
|
97
|
+
element.invalidate_attribute_cache!
|
|
98
|
+
value
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Remove the attribute named by XML-correct resolution (see
|
|
102
|
+
# #assign for the name semantics).
|
|
103
|
+
#
|
|
104
|
+
# @return [Moxml::Attribute, nil] the removed attribute
|
|
105
|
+
def remove(element, name)
|
|
106
|
+
name = name.to_s
|
|
107
|
+
adapter = element.context.config.adapter
|
|
108
|
+
if name == "xmlns" || name.start_with?("xmlns:")
|
|
109
|
+
adapter.remove_attribute(element.native, name)
|
|
110
|
+
element.invalidate_attribute_cache!
|
|
111
|
+
return nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
attr = resolve(element, name)
|
|
115
|
+
return nil unless attr
|
|
116
|
+
|
|
117
|
+
adapter.remove_attribute_native(attr.native)
|
|
118
|
+
element.invalidate_attribute_cache!
|
|
119
|
+
attr
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# XPath 1.0 attribute node test, sharing the resolver's
|
|
123
|
+
# expanded-name semantics:
|
|
124
|
+
#
|
|
125
|
+
# @param prefix [String, Symbol, nil]
|
|
126
|
+
# - String: match by namespace URI resolved against the
|
|
127
|
+
# element's in-scope declarations; the prefix spelling is
|
|
128
|
+
# irrelevant (q:x matches a p:x test when URIs agree)
|
|
129
|
+
# - :any: any namespace, including none (*:local)
|
|
130
|
+
# - nil: bare name — no-namespace attributes only (Namespaces
|
|
131
|
+
# 1.0 §5.2)
|
|
132
|
+
# @param local [String, nil] local name, or nil for any (p:*)
|
|
133
|
+
# @return [Boolean]
|
|
134
|
+
def attribute_test?(element, attr, prefix, local)
|
|
135
|
+
return false if local && local_name(attr.name) != local
|
|
136
|
+
|
|
137
|
+
case prefix
|
|
138
|
+
when :any then true
|
|
139
|
+
when nil then attribute_uri(element, attr).nil?
|
|
140
|
+
else
|
|
141
|
+
uri = prefix_uri(element, prefix)
|
|
142
|
+
!uri.nil? && uri == attribute_uri(element, attr)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Attribute node test with a URI resolved at XPath compile time
|
|
147
|
+
# (the static context's prefix mapping wins over the document's
|
|
148
|
+
# own declarations). See #attribute_test? for local semantics.
|
|
149
|
+
#
|
|
150
|
+
# @return [Boolean]
|
|
151
|
+
def attribute_test_uri?(element, attr, uri, local)
|
|
152
|
+
return false if local && local_name(attr.name) != local
|
|
153
|
+
|
|
154
|
+
attribute_uri(element, attr) == uri
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Local part of a wrapper attribute name. Local names cannot
|
|
158
|
+
# contain ':' in namespace-well-formed documents; splitting is for
|
|
159
|
+
# adapters whose natives carry the qualified name (leptris).
|
|
160
|
+
def local_name(name)
|
|
161
|
+
name.include?(":") ? name.split(":", 2)[1] : name
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# URI of an attribute's namespace, nil for the no-namespace case.
|
|
165
|
+
#
|
|
166
|
+
# Uses the attribute's own namespace when the native binding
|
|
167
|
+
# exposes one with a URI (nokogiri/oga/libxml, and rexml/ox for
|
|
168
|
+
# declared prefixes). Otherwise reconstructs it from the attribute
|
|
169
|
+
# prefix via the element's in-scope declarations — covering
|
|
170
|
+
# adapters that report a prefix without resolving it (rexml/ox for
|
|
171
|
+
# the prebound xml prefix) and adapters that expose neither
|
|
172
|
+
# namespace nor separate prefix (leptris qualified names).
|
|
173
|
+
def attribute_uri(element, attr)
|
|
174
|
+
ns = attr.namespace
|
|
175
|
+
return ns.uri unless ns.nil? || ns.uri.to_s.empty?
|
|
176
|
+
|
|
177
|
+
prefix = ns&.prefix || prefix_part(attr.name)
|
|
178
|
+
return nil if prefix.nil? || prefix.empty?
|
|
179
|
+
return XML_NAMESPACE_URI if prefix == "xml"
|
|
180
|
+
|
|
181
|
+
element.in_scope_namespaces
|
|
182
|
+
.find { |candidate| candidate.prefix == prefix }&.uri
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def prefix_part(name)
|
|
186
|
+
name.include?(":") ? name.split(":", 2)[0] : nil
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module C14n
|
|
5
|
+
# Attribute axis handler for inclusive C14N.
|
|
6
|
+
# Implements W3C C14N 1.0 §2.4 / 1.1 §2.4 attribute rendering,
|
|
7
|
+
# including resolution of simple inheritable attributes (xml:lang,
|
|
8
|
+
# xml:space) for document subsets.
|
|
9
|
+
class AttributeHandler
|
|
10
|
+
attr_reader :encoder
|
|
11
|
+
|
|
12
|
+
def initialize(encoder)
|
|
13
|
+
@encoder = encoder
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def process_attributes(element, output, omitted_ancestors = [])
|
|
17
|
+
return unless element.in_node_set?
|
|
18
|
+
|
|
19
|
+
attributes = collect_attributes(element, omitted_ancestors)
|
|
20
|
+
attributes.each do |attr|
|
|
21
|
+
output << " "
|
|
22
|
+
output << attr.qname
|
|
23
|
+
output << '="'
|
|
24
|
+
output << encoder.encode_attribute(attr.value)
|
|
25
|
+
output << '"'
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def collect_attributes(element, omitted_ancestors)
|
|
32
|
+
attributes = element.sorted_attribute_nodes.select(&:in_node_set?)
|
|
33
|
+
|
|
34
|
+
return attributes if omitted_ancestors.empty?
|
|
35
|
+
|
|
36
|
+
inherited = collect_inherited_attributes(element, omitted_ancestors)
|
|
37
|
+
merge_attributes(attributes, inherited)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Walk omitted ancestors to collect simple inheritable attributes
|
|
41
|
+
# not already declared on the element. Per C14N 1.1 §2.4, these
|
|
42
|
+
# are inherited from the nearest ancestor in which they are declared.
|
|
43
|
+
def collect_inherited_attributes(element, omitted_ancestors)
|
|
44
|
+
inherited = []
|
|
45
|
+
seen = Set.new
|
|
46
|
+
|
|
47
|
+
element.attribute_nodes.each do |attr|
|
|
48
|
+
seen.add(attr.name) if attr.simple_inheritable?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
omitted_ancestors.reverse_each do |ancestor|
|
|
52
|
+
ancestor.attribute_nodes.each do |attr|
|
|
53
|
+
next unless attr.simple_inheritable?
|
|
54
|
+
next if seen.include?(attr.name)
|
|
55
|
+
|
|
56
|
+
inherited << attr
|
|
57
|
+
seen.add(attr.name)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
inherited
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def merge_attributes(element_attrs, inherited_attrs)
|
|
65
|
+
(element_attrs + inherited_attrs).sort_by do |attr|
|
|
66
|
+
[attr.namespace_uri.to_s, attr.local_name]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module C14n
|
|
5
|
+
# Character encoder for C14N output. Handles the entity escaping
|
|
6
|
+
# mandated by W3C C14N 1.0 §2.4 / 1.1 §2.4 for text and attribute values.
|
|
7
|
+
class CharacterEncoder
|
|
8
|
+
TEXT_ESCAPES = {
|
|
9
|
+
"&" => "&",
|
|
10
|
+
"<" => "<",
|
|
11
|
+
">" => ">",
|
|
12
|
+
"\r" => "
",
|
|
13
|
+
}.freeze
|
|
14
|
+
ATTRIBUTE_ESCAPES = TEXT_ESCAPES.merge(
|
|
15
|
+
'"' => """,
|
|
16
|
+
"\t" => "	",
|
|
17
|
+
"\n" => "
",
|
|
18
|
+
).freeze
|
|
19
|
+
|
|
20
|
+
def encode_text(text)
|
|
21
|
+
text.to_s.gsub(/[&<>\r]/, TEXT_ESCAPES)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def encode_attribute(value)
|
|
25
|
+
value.to_s.gsub(/[&<"\t\n\r]/, ATTRIBUTE_ESCAPES)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module C14n
|
|
5
|
+
# Builds a C14N data model from a Moxml::Node tree (or XML string).
|
|
6
|
+
#
|
|
7
|
+
# The data model exists because canonicalization needs:
|
|
8
|
+
# - sorted namespace and attribute axes (spec §2.3, §2.4)
|
|
9
|
+
# - in_node_set flags for subset canonicalization (spec §3)
|
|
10
|
+
# - xml:* inheritable attribute resolution
|
|
11
|
+
#
|
|
12
|
+
# Ported from canon (lutaml/canon) — adapted to build directly from
|
|
13
|
+
# Moxml::Node rather than via a separate Nokogiri pass. Matches
|
|
14
|
+
# canon's document-level node iteration (PIs and comments outside
|
|
15
|
+
# the document root element).
|
|
16
|
+
class DataModel
|
|
17
|
+
def self.from_xml(xml_string)
|
|
18
|
+
from_node(::Moxml.parse(xml_string))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.from_node(moxml_node)
|
|
22
|
+
return build_from_document(moxml_node) if moxml_node.is_a?(::Moxml::Document)
|
|
23
|
+
|
|
24
|
+
root = Nodes::RootNode.new
|
|
25
|
+
root.add_child(build_node(moxml_node))
|
|
26
|
+
root
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Build from a Moxml::Document. Matches canon's Nokogiri path:
|
|
30
|
+
# the root element is added first, then all other document-level
|
|
31
|
+
# children (PIs, comments) in document order.
|
|
32
|
+
def self.build_from_document(document)
|
|
33
|
+
root = Nodes::RootNode.new
|
|
34
|
+
|
|
35
|
+
if document.root
|
|
36
|
+
root.add_child(build_element_node(document.root))
|
|
37
|
+
# Iterate ALL document children — not just the root element.
|
|
38
|
+
# This captures PIs and comments that appear outside the
|
|
39
|
+
# document element, which are part of the canonical form.
|
|
40
|
+
document.children.each do |child|
|
|
41
|
+
next if child.equal?(document.root)
|
|
42
|
+
next if child.is_a?(::Moxml::Element)
|
|
43
|
+
|
|
44
|
+
built = build_node(child)
|
|
45
|
+
root.add_child(built) if built
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
root
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.build_node(moxml_node)
|
|
53
|
+
case moxml_node
|
|
54
|
+
when ::Moxml::Element then build_element_node(moxml_node)
|
|
55
|
+
when ::Moxml::Text then build_text_node(moxml_node)
|
|
56
|
+
when ::Moxml::Comment then build_comment_node(moxml_node)
|
|
57
|
+
when ::Moxml::ProcessingInstruction then build_pi_node(moxml_node)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.build_element_node(moxml_element)
|
|
62
|
+
ns = moxml_element.namespace
|
|
63
|
+
element = Nodes::ElementNode.new(
|
|
64
|
+
name: moxml_element.name,
|
|
65
|
+
namespace_uri: ns&.uri,
|
|
66
|
+
prefix: ns&.prefix,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
build_namespace_nodes(moxml_element, element)
|
|
70
|
+
build_attribute_nodes(moxml_element, element)
|
|
71
|
+
|
|
72
|
+
moxml_element.children.each do |child|
|
|
73
|
+
built = build_node(child)
|
|
74
|
+
element.add_child(built) if built
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
element
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.build_namespace_nodes(moxml_element, element)
|
|
81
|
+
moxml_element.in_scope_namespaces.each do |ns|
|
|
82
|
+
element.add_namespace(
|
|
83
|
+
Nodes::NamespaceNode.new(prefix: ns.prefix || "", uri: ns.uri),
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
return if element.namespace_nodes.any? { |n| n.prefix == "xml" }
|
|
88
|
+
|
|
89
|
+
element.add_namespace(
|
|
90
|
+
Nodes::NamespaceNode.new(prefix: "xml", uri: XML_URI),
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.build_attribute_nodes(moxml_element, element)
|
|
95
|
+
moxml_element.attributes.each do |attr|
|
|
96
|
+
ns = attr.namespace
|
|
97
|
+
element.add_attribute(
|
|
98
|
+
Nodes::AttributeNode.new(
|
|
99
|
+
name: attr.name,
|
|
100
|
+
value: attr.value,
|
|
101
|
+
namespace_uri: ns&.uri,
|
|
102
|
+
prefix: ns&.prefix,
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def self.build_text_node(moxml_text)
|
|
109
|
+
Nodes::TextNode.new(value: moxml_text.content)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.build_comment_node(moxml_comment)
|
|
113
|
+
Nodes::CommentNode.new(value: moxml_comment.content)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.build_pi_node(moxml_pi)
|
|
117
|
+
Nodes::ProcessingInstructionNode.new(
|
|
118
|
+
target: moxml_pi.target || moxml_pi.name,
|
|
119
|
+
data: moxml_pi.content || "",
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.mark_all(node, value)
|
|
124
|
+
node.in_node_set = value
|
|
125
|
+
node.children.each { |child| mark_all(child, value) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.mark_subset(root_node, matched)
|
|
129
|
+
matched.each { |node| mark_node_and_descendants(node) }
|
|
130
|
+
root_node.in_node_set = true
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.mark_node_and_descendants(node)
|
|
134
|
+
node.in_node_set = true
|
|
135
|
+
node.children.each { |child| mark_node_and_descendants(child) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private_class_method :build_from_document, :build_node,
|
|
139
|
+
:build_element_node, :build_namespace_nodes,
|
|
140
|
+
:build_attribute_nodes, :build_text_node,
|
|
141
|
+
:build_comment_node, :build_pi_node,
|
|
142
|
+
:mark_node_and_descendants
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|