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
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module C14n
|
|
5
|
+
# C14N data-model node types. All nodes inherit from {Moxml::C14n::Node}.
|
|
6
|
+
module Nodes
|
|
7
|
+
autoload :AttributeNode, "moxml/c14n/nodes/attribute_node"
|
|
8
|
+
autoload :CommentNode, "moxml/c14n/nodes/comment_node"
|
|
9
|
+
autoload :ElementNode, "moxml/c14n/nodes/element_node"
|
|
10
|
+
autoload :NamespaceNode, "moxml/c14n/nodes/namespace_node"
|
|
11
|
+
autoload :ProcessingInstructionNode,
|
|
12
|
+
"moxml/c14n/nodes/processing_instruction_node"
|
|
13
|
+
autoload :RootNode, "moxml/c14n/nodes/root_node"
|
|
14
|
+
autoload :TextNode, "moxml/c14n/nodes/text_node"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module C14n
|
|
5
|
+
# C14N 1.0/1.1 processor. Walks the data model and emits canonical
|
|
6
|
+
# octets. Handles node-set subsets (omitted ancestors), xml:base
|
|
7
|
+
# fixup, and xml:* inheritable attribute resolution.
|
|
8
|
+
#
|
|
9
|
+
# Ported from canon (lutaml/canon).
|
|
10
|
+
class Processor
|
|
11
|
+
attr_reader :with_comments
|
|
12
|
+
|
|
13
|
+
def initialize(with_comments: false)
|
|
14
|
+
@with_comments = with_comments
|
|
15
|
+
@encoder = CharacterEncoder.new
|
|
16
|
+
@namespace_handler = NamespaceHandler.new(@encoder)
|
|
17
|
+
@attribute_handler = AttributeHandler.new(@encoder)
|
|
18
|
+
@xml_base_handler = XmlBaseHandler.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def process(root_node)
|
|
22
|
+
output = (+"")
|
|
23
|
+
process_node(root_node, output)
|
|
24
|
+
output
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def process_node(node, output, parent_element = nil, omitted_ancestors = [])
|
|
30
|
+
case node.node_type
|
|
31
|
+
when :root
|
|
32
|
+
node.children.each { |child| process_node(child, output) }
|
|
33
|
+
when :element
|
|
34
|
+
process_element_node(node, output, parent_element, omitted_ancestors)
|
|
35
|
+
when :text
|
|
36
|
+
process_text_node(node, output)
|
|
37
|
+
when :comment
|
|
38
|
+
process_comment_node(node, output, parent_element)
|
|
39
|
+
when :processing_instruction
|
|
40
|
+
process_pi_node(node, output, parent_element)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def process_element_node(node, output, parent_element, omitted_ancestors)
|
|
45
|
+
if node.in_node_set?
|
|
46
|
+
render_element(node, output, parent_element, omitted_ancestors)
|
|
47
|
+
else
|
|
48
|
+
# Element not in node-set, but its children may be. Pass the
|
|
49
|
+
# element as an omitted ancestor for inheritable-attr fixup.
|
|
50
|
+
new_omitted = omitted_ancestors + [node]
|
|
51
|
+
node.children.each do |child|
|
|
52
|
+
process_node(child, output, parent_element, new_omitted)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def render_element(node, output, parent_element, omitted_ancestors)
|
|
58
|
+
output << "<" << node.qname
|
|
59
|
+
|
|
60
|
+
@namespace_handler.process_namespaces(node, output, parent_element)
|
|
61
|
+
process_element_attributes(node, output, omitted_ancestors)
|
|
62
|
+
|
|
63
|
+
output << ">"
|
|
64
|
+
|
|
65
|
+
node.children.each { |child| process_node(child, output, node, []) }
|
|
66
|
+
|
|
67
|
+
output << "</" << node.qname << ">"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def process_element_attributes(node, output, omitted_ancestors)
|
|
71
|
+
@attribute_handler.process_attributes(node, output, omitted_ancestors)
|
|
72
|
+
|
|
73
|
+
return unless omitted_ancestors.any?
|
|
74
|
+
|
|
75
|
+
fixed_base = @xml_base_handler.fixup_xml_base(node, omitted_ancestors)
|
|
76
|
+
return unless fixed_base && !fixed_base.empty?
|
|
77
|
+
|
|
78
|
+
has_base = node.attribute_nodes.any?(&:xml_base?)
|
|
79
|
+
return if has_base
|
|
80
|
+
|
|
81
|
+
output << ' xml:base="'
|
|
82
|
+
output << @encoder.encode_attribute(fixed_base)
|
|
83
|
+
output << '"'
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def process_text_node(node, output)
|
|
87
|
+
return unless node.in_node_set?
|
|
88
|
+
|
|
89
|
+
output << @encoder.encode_text(node.value)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def process_comment_node(node, output, parent_element)
|
|
93
|
+
return unless with_comments
|
|
94
|
+
return unless node.in_node_set?
|
|
95
|
+
|
|
96
|
+
# Comment outside the document element gets a line break before/after
|
|
97
|
+
# to keep canonical output readable.
|
|
98
|
+
if parent_element.nil? && output.length.positive?
|
|
99
|
+
output << "\n"
|
|
100
|
+
end
|
|
101
|
+
output << "<!--" << node.value << "-->"
|
|
102
|
+
output << "\n" if parent_element.nil?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def process_pi_node(node, output, parent_element)
|
|
106
|
+
return unless node.in_node_set?
|
|
107
|
+
|
|
108
|
+
if parent_element.nil? && output.length.positive?
|
|
109
|
+
output << "\n"
|
|
110
|
+
end
|
|
111
|
+
output << "<?" << node.target
|
|
112
|
+
output << " " << node.data unless node.data.to_s.empty?
|
|
113
|
+
output << "?>"
|
|
114
|
+
output << "\n" if parent_element.nil?
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module C14n
|
|
5
|
+
# Builds canonical octet output incrementally.
|
|
6
|
+
# Output is always UTF-8 with no BOM.
|
|
7
|
+
class Writer
|
|
8
|
+
attr_reader :output
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@output = (+"")
|
|
12
|
+
# Tracks namespaces already rendered in the output ancestor chain,
|
|
13
|
+
# so descendants don't re-render the same declaration.
|
|
14
|
+
# Key: element (by object identity), value: { prefix => uri }.
|
|
15
|
+
@rendered_stack = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def <<(str)
|
|
19
|
+
@output << str
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def raw(str)
|
|
24
|
+
@output << str.to_s
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Push a fresh "rendered" frame for the given element. Called by the
|
|
29
|
+
# canonicalizer before rendering the element's namespaces.
|
|
30
|
+
def open_rendered_frame(element)
|
|
31
|
+
parent_rendered = @rendered_stack.last || {}
|
|
32
|
+
# Inherit parent's rendered namespaces as the starting set.
|
|
33
|
+
@rendered_stack.push(parent_rendered.dup)
|
|
34
|
+
element
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def close_rendered_frame
|
|
38
|
+
@rendered_stack.pop
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def rendered_namespaces_for(_element)
|
|
43
|
+
@rendered_stack.last || {}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def mark_rendered(_element, prefix, uri)
|
|
47
|
+
(@rendered_stack.last || {})[prefix] = uri
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def open_tag(prefix, local)
|
|
52
|
+
@output << "<"
|
|
53
|
+
@output << "#{prefix}:" unless prefix.nil? || prefix.empty?
|
|
54
|
+
@output << local
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def close_tag_open
|
|
59
|
+
@output << ">"
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def close_tag_self_close
|
|
64
|
+
@output << "></"
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def close_tag(prefix, local)
|
|
69
|
+
@output << "</"
|
|
70
|
+
@output << "#{prefix}:" unless prefix.nil? || prefix.empty?
|
|
71
|
+
@output << local
|
|
72
|
+
@output << ">"
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def attribute(expanded_name, value)
|
|
77
|
+
@output << " "
|
|
78
|
+
@output << expanded_name
|
|
79
|
+
@output << "=\""
|
|
80
|
+
@output << C14n.escape_attribute(value)
|
|
81
|
+
@output << "\""
|
|
82
|
+
self
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def namespace(prefix, uri)
|
|
86
|
+
@output << " "
|
|
87
|
+
if prefix.nil? || prefix.empty?
|
|
88
|
+
@output << "xmlns"
|
|
89
|
+
else
|
|
90
|
+
@output << "xmlns:"
|
|
91
|
+
@output << prefix
|
|
92
|
+
end
|
|
93
|
+
@output << "=\""
|
|
94
|
+
@output << C14n.escape_attribute(uri)
|
|
95
|
+
@output << "\""
|
|
96
|
+
self
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def text(content)
|
|
100
|
+
@output << C14n.escape_text(content)
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def comment(content)
|
|
105
|
+
@output << "<!--"
|
|
106
|
+
@output << C14n.escape_text(content)
|
|
107
|
+
@output << "-->"
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def processing_instruction(target, content)
|
|
112
|
+
@output << "<?"
|
|
113
|
+
@output << target
|
|
114
|
+
@output << " "
|
|
115
|
+
@output << content
|
|
116
|
+
@output << "?>"
|
|
117
|
+
self
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Moxml
|
|
6
|
+
module C14n
|
|
7
|
+
# xml:base fixup handler for document subsets (C14N 1.1 §2.4).
|
|
8
|
+
# Implements RFC 3986 URI joining with C14N 1.1 modifications.
|
|
9
|
+
class XmlBaseHandler
|
|
10
|
+
# Returns the fixed-up xml:base value to emit on the element, or
|
|
11
|
+
# nil if no fixup is needed.
|
|
12
|
+
def fixup_xml_base(element, omitted_ancestors)
|
|
13
|
+
return nil if omitted_ancestors.empty?
|
|
14
|
+
|
|
15
|
+
base_values = collect_base_values(element, omitted_ancestors)
|
|
16
|
+
return nil if base_values.empty?
|
|
17
|
+
|
|
18
|
+
join_base_values(base_values)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def collect_base_values(element, omitted_ancestors)
|
|
24
|
+
values = []
|
|
25
|
+
omitted_ancestors.each do |ancestor|
|
|
26
|
+
base_attr = ancestor.attribute_nodes.find(&:xml_base?)
|
|
27
|
+
values << base_attr.value if base_attr
|
|
28
|
+
end
|
|
29
|
+
element_base = element.attribute_nodes.find(&:xml_base?)
|
|
30
|
+
values << element_base.value if element_base
|
|
31
|
+
values
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def join_base_values(values)
|
|
35
|
+
result = values.first
|
|
36
|
+
values[1..].each { |ref| result = join_uri_references(result, ref) }
|
|
37
|
+
result
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Join two URI references per RFC 3986 §5.2.1–5.2.4 with the
|
|
41
|
+
# C14N 1.1 modification (drop fragment).
|
|
42
|
+
def join_uri_references(base, ref)
|
|
43
|
+
ref_parts = parse_uri(ref)
|
|
44
|
+
return remove_dot_segments(ref_parts[:path] || "") if ref_parts[:scheme]
|
|
45
|
+
|
|
46
|
+
base_parts = parse_uri(base)
|
|
47
|
+
result_parts = {}
|
|
48
|
+
|
|
49
|
+
if ref_parts[:authority]
|
|
50
|
+
result_parts[:authority] = ref_parts[:authority]
|
|
51
|
+
result_parts[:path] = remove_dot_segments(ref_parts[:path] || "")
|
|
52
|
+
result_parts[:query] = ref_parts[:query]
|
|
53
|
+
else
|
|
54
|
+
if ref_parts[:path].nil? || ref_parts[:path].empty?
|
|
55
|
+
result_parts[:path] = base_parts[:path]
|
|
56
|
+
result_parts[:query] = ref_parts[:query] || base_parts[:query]
|
|
57
|
+
elsif ref_parts[:path].start_with?("/")
|
|
58
|
+
result_parts[:path] = remove_dot_segments(ref_parts[:path])
|
|
59
|
+
else
|
|
60
|
+
result_parts[:path] = remove_dot_segments(
|
|
61
|
+
merge_paths(base_parts[:path], ref_parts[:path]),
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
result_parts[:query] = ref_parts[:query]
|
|
65
|
+
result_parts[:authority] = base_parts[:authority]
|
|
66
|
+
end
|
|
67
|
+
result_parts[:scheme] = base_parts[:scheme]
|
|
68
|
+
reconstruct_uri(result_parts)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def parse_uri(uri_str)
|
|
72
|
+
parts = {}
|
|
73
|
+
if uri_str.to_s =~ %r{\A(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?\z}
|
|
74
|
+
parts[:scheme] = Regexp.last_match(2)
|
|
75
|
+
parts[:authority] = Regexp.last_match(4)
|
|
76
|
+
parts[:path] = Regexp.last_match(5)
|
|
77
|
+
parts[:query] = Regexp.last_match(7)
|
|
78
|
+
end
|
|
79
|
+
parts
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def merge_paths(base_path, ref_path)
|
|
83
|
+
if base_path&.include?("/")
|
|
84
|
+
base_path.sub(%r{/[^/]*\z}, "/#{ref_path}")
|
|
85
|
+
else
|
|
86
|
+
ref_path
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# RFC 3986 §5.2.4 dot-segment removal with C14N 1.1 modifications.
|
|
91
|
+
TERMINAL_DOT_SEGMENTS = %w[. ..].freeze.freeze
|
|
92
|
+
private_constant :TERMINAL_DOT_SEGMENTS
|
|
93
|
+
|
|
94
|
+
def remove_dot_segments(path)
|
|
95
|
+
input = path.to_s.dup
|
|
96
|
+
input = input.sub(%r{/\.\.\z}, "/../")
|
|
97
|
+
output = +""
|
|
98
|
+
|
|
99
|
+
until input.empty?
|
|
100
|
+
if input.start_with?("../")
|
|
101
|
+
input = input[3..]
|
|
102
|
+
elsif input.start_with?("./")
|
|
103
|
+
input = input[2..]
|
|
104
|
+
elsif input.start_with?("/./")
|
|
105
|
+
input = "/#{input[3..]}"
|
|
106
|
+
elsif input == "/."
|
|
107
|
+
input = "/"
|
|
108
|
+
elsif input.start_with?("/../")
|
|
109
|
+
input = "/#{input[4..]}"
|
|
110
|
+
output = output.sub(%r{/[^/]*\z}, "")
|
|
111
|
+
elsif input == "/.."
|
|
112
|
+
input = "/"
|
|
113
|
+
output = output.sub(%r{/[^/]*\z}, "")
|
|
114
|
+
elsif TERMINAL_DOT_SEGMENTS.include?(input)
|
|
115
|
+
input = ""
|
|
116
|
+
else
|
|
117
|
+
seg_match = input.start_with?("/") ? input.match(%r{\A(/[^/]*)}) : input.match(%r{\A([^/]*)})
|
|
118
|
+
seg = seg_match[1]
|
|
119
|
+
input = input[seg.length..]
|
|
120
|
+
output << seg
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
output.squeeze("/").then do |out|
|
|
125
|
+
out << "/" if out.end_with?("/..")
|
|
126
|
+
out
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def reconstruct_uri(parts)
|
|
131
|
+
result = +""
|
|
132
|
+
result << "#{parts[:scheme]}:" if parts[:scheme]
|
|
133
|
+
result << "//#{parts[:authority]}" if parts[:authority]
|
|
134
|
+
result << parts[:path].to_s if parts[:path]
|
|
135
|
+
result << "?#{parts[:query]}" if parts[:query]
|
|
136
|
+
result
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
data/lib/moxml/c14n.rb
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
# Canonicalization engine. Core moxml feature — sibling to
|
|
5
|
+
# Moxml::XPath, Moxml::Builder, Moxml::SAX.
|
|
6
|
+
#
|
|
7
|
+
# Inclusive C14N (1.0 and 1.1) is ported from canon (lutaml/canon):
|
|
8
|
+
# mature, tested, full-featured (xml:base fixup, inheritable xml:*
|
|
9
|
+
# attribute resolution, node-set subset canonicalization).
|
|
10
|
+
#
|
|
11
|
+
# Exclusive C14N (1.0) is a moxml-native implementation; canon does
|
|
12
|
+
# not implement exclusive. It is required by XML signature to keep
|
|
13
|
+
# signatures valid when subdocuments are moved between contexts.
|
|
14
|
+
module C14n
|
|
15
|
+
autoload :Node, "moxml/c14n/node"
|
|
16
|
+
autoload :Nodes, "moxml/c14n/nodes"
|
|
17
|
+
autoload :DataModel, "moxml/c14n/data_model"
|
|
18
|
+
autoload :Processor, "moxml/c14n/processor"
|
|
19
|
+
autoload :CharacterEncoder, "moxml/c14n/character_encoder"
|
|
20
|
+
autoload :NamespaceHandler, "moxml/c14n/namespace_handler"
|
|
21
|
+
autoload :AttributeHandler, "moxml/c14n/attribute_handler"
|
|
22
|
+
autoload :XmlBaseHandler, "moxml/c14n/xml_base_handler"
|
|
23
|
+
|
|
24
|
+
# Engine classes used directly by signature algorithms.
|
|
25
|
+
autoload :Writer, "moxml/c14n/writer"
|
|
26
|
+
autoload :NamespaceContext, "moxml/c14n/namespace_context"
|
|
27
|
+
autoload :Exclusive, "moxml/c14n/exclusive"
|
|
28
|
+
autoload :Inclusive10, "moxml/c14n/inclusive_10"
|
|
29
|
+
autoload :Inclusive11, "moxml/c14n/inclusive_11"
|
|
30
|
+
|
|
31
|
+
XMLNS_URI = "http://www.w3.org/2000/xmlns/"
|
|
32
|
+
XML_URI = "http://www.w3.org/XML/1998/namespace"
|
|
33
|
+
|
|
34
|
+
# Canonicalize using the named algorithm.
|
|
35
|
+
#
|
|
36
|
+
# algorithm is one of:
|
|
37
|
+
# :inclusive10 Canonical XML 1.0 (default; W3C REC-xml-c14n-20010315)
|
|
38
|
+
# :inclusive11 Canonical XML 1.1 (W3C REC-xml-c14n11-20080502)
|
|
39
|
+
# :exclusive10 Exclusive C14N 1.0 (W3C REC-xml-exc-c14n-20020718)
|
|
40
|
+
def self.canonicalize(node_or_xml, with_comments: false,
|
|
41
|
+
algorithm: :inclusive10, inclusive_namespaces: [])
|
|
42
|
+
engine_for(algorithm).canonicalize(
|
|
43
|
+
node_or_xml,
|
|
44
|
+
with_comments: with_comments,
|
|
45
|
+
inclusive_namespaces: inclusive_namespaces,
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.canonicalize_inclusive10(node_or_xml, with_comments: false)
|
|
50
|
+
Inclusive10.new.canonicalize(node_or_xml, with_comments: with_comments)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.canonicalize_inclusive11(node_or_xml, with_comments: false)
|
|
54
|
+
Inclusive11.new.canonicalize(node_or_xml, with_comments: with_comments)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.canonicalize_exclusive(node_or_xml, with_comments: false,
|
|
58
|
+
inclusive_namespaces: [])
|
|
59
|
+
Exclusive.new.canonicalize(
|
|
60
|
+
node_or_xml,
|
|
61
|
+
with_comments: with_comments,
|
|
62
|
+
inclusive_namespaces: inclusive_namespaces,
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Compare two XML inputs by their canonical forms.
|
|
67
|
+
def self.equivalent?(left, right, with_comments: false,
|
|
68
|
+
algorithm: :inclusive10, inclusive_namespaces: [])
|
|
69
|
+
canonicalize(left, with_comments: with_comments, algorithm: algorithm,
|
|
70
|
+
inclusive_namespaces: inclusive_namespaces) ==
|
|
71
|
+
canonicalize(right, with_comments: with_comments, algorithm: algorithm,
|
|
72
|
+
inclusive_namespaces: inclusive_namespaces)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.escape_text(text)
|
|
76
|
+
CharacterEncoder.new.encode_text(text)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.escape_attribute(value)
|
|
80
|
+
CharacterEncoder.new.encode_attribute(value)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.engine_for(algorithm)
|
|
84
|
+
case algorithm
|
|
85
|
+
when :inclusive10 then Inclusive10.new
|
|
86
|
+
when :inclusive11 then Inclusive11.new
|
|
87
|
+
when :exclusive10 then Exclusive.new
|
|
88
|
+
else
|
|
89
|
+
raise ArgumentError,
|
|
90
|
+
"unknown C14N algorithm #{algorithm.inspect}; expected one of " \
|
|
91
|
+
":inclusive10, :inclusive11, :exclusive10"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
private_class_method :engine_for
|
|
95
|
+
end
|
|
96
|
+
end
|
data/lib/moxml/config.rb
CHANGED
|
@@ -5,8 +5,15 @@ module Moxml
|
|
|
5
5
|
LINE_ENDING_LF = "\n"
|
|
6
6
|
LINE_ENDING_CRLF = "\r\n"
|
|
7
7
|
VALID_LINE_ENDINGS = [LINE_ENDING_LF, LINE_ENDING_CRLF].freeze
|
|
8
|
-
VALID_ADAPTERS = %i[nokogiri oga rexml ox headed_ox libxml].freeze
|
|
9
|
-
|
|
8
|
+
VALID_ADAPTERS = %i[nokogiri oga rexml ox headed_ox libxml leptris].freeze
|
|
9
|
+
# Preferred CRuby default (issue #96): fastest on every measured
|
|
10
|
+
# operation and the cleanest deployment story. Only takes effect
|
|
11
|
+
# when the installed leptris supports programmatic document
|
|
12
|
+
# construction (Leptris::XML::Document.create); otherwise moxml
|
|
13
|
+
# falls back to FALLBACK_ADAPTER so released-gem environments keep
|
|
14
|
+
# working. Oga remains the Opal default (pure Ruby).
|
|
15
|
+
PREFERRED_ADAPTER = :leptris
|
|
16
|
+
FALLBACK_ADAPTER = :nokogiri
|
|
10
17
|
OPAL_DEFAULT_ADAPTER = :oga
|
|
11
18
|
|
|
12
19
|
# Entity loading modes:
|
|
@@ -30,7 +37,26 @@ module Moxml
|
|
|
30
37
|
def runtime_default_adapter
|
|
31
38
|
return OPAL_DEFAULT_ADAPTER if RUBY_ENGINE == "opal"
|
|
32
39
|
|
|
33
|
-
|
|
40
|
+
return PREFERRED_ADAPTER if leptris_preferred_available?
|
|
41
|
+
|
|
42
|
+
detect_loaded_adapter || FALLBACK_ADAPTER
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# True when the leptris gem is installed AND new enough for the
|
|
46
|
+
# adapter (it needs programmatic document construction, which
|
|
47
|
+
# released 1.1.x lacks). Memoized: the require probe runs once.
|
|
48
|
+
def leptris_preferred_available?
|
|
49
|
+
return @leptris_preferred_available if defined?(@leptris_preferred_available)
|
|
50
|
+
|
|
51
|
+
@leptris_preferred_available = begin
|
|
52
|
+
require "leptris"
|
|
53
|
+
# leptris-ruby 1.9.0 regression (leptris-ruby#53): the XML
|
|
54
|
+
# autoload manifest is shadowed; load it explicitly.
|
|
55
|
+
require "leptris/xml"
|
|
56
|
+
::Leptris::XML::Document.respond_to?(:create)
|
|
57
|
+
rescue LoadError, NameError
|
|
58
|
+
false
|
|
59
|
+
end
|
|
34
60
|
end
|
|
35
61
|
|
|
36
62
|
def detect_loaded_adapter
|
data/lib/moxml/context.rb
CHANGED
|
@@ -6,6 +6,18 @@ module Moxml
|
|
|
6
6
|
|
|
7
7
|
def initialize(adapter = nil)
|
|
8
8
|
@config = Config.new(adapter)
|
|
9
|
+
# Bumped on any namespace-scope mutation; Element scope caches
|
|
10
|
+
# compare against it so every wrapper sees changes without
|
|
11
|
+
# needing wrapper identity.
|
|
12
|
+
@namespace_scope_generation = 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def namespace_scope_generation
|
|
16
|
+
@namespace_scope_generation
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def bump_namespace_scope_generation
|
|
20
|
+
@namespace_scope_generation += 1
|
|
9
21
|
end
|
|
10
22
|
|
|
11
23
|
def entity_registry
|
data/lib/moxml/element.rb
CHANGED
|
@@ -37,19 +37,23 @@ module Moxml
|
|
|
37
37
|
ns&.uri
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# Write path shares the resolver's expanded-name semantics (see
|
|
41
|
+
# AttributeResolver.assign): bare names target no-namespace
|
|
42
|
+
# attributes only; prefixed names replace by namespace URI and
|
|
43
|
+
# require a declared prefix. Cache coherence is the resolver's.
|
|
40
44
|
def []=(name, value)
|
|
41
|
-
|
|
42
|
-
@attributes = nil
|
|
45
|
+
Moxml::AttributeResolver.assign(self, name, normalize_xml_value(value))
|
|
43
46
|
end
|
|
44
47
|
|
|
48
|
+
# Unified XML-correct name resolution (see AttributeResolver):
|
|
49
|
+
# bare names match only no-namespace attributes; prefixed names
|
|
50
|
+
# resolve through in-scope declarations to expanded names.
|
|
45
51
|
def [](name)
|
|
46
|
-
|
|
47
|
-
val ? adapter.restore_entities(val) : val
|
|
52
|
+
Moxml::AttributeResolver.resolve(self, name)&.value
|
|
48
53
|
end
|
|
49
54
|
|
|
50
55
|
def attribute(name)
|
|
51
|
-
|
|
52
|
-
native_attr && Attribute.new(native_attr, context)
|
|
56
|
+
Moxml::AttributeResolver.resolve(self, name)
|
|
53
57
|
end
|
|
54
58
|
|
|
55
59
|
# Returns attribute value by name (used by XPath engine)
|
|
@@ -66,15 +70,14 @@ module Moxml
|
|
|
66
70
|
end
|
|
67
71
|
|
|
68
72
|
def remove_attribute(name)
|
|
69
|
-
|
|
70
|
-
@attributes = nil
|
|
73
|
+
Moxml::AttributeResolver.remove(self, name)
|
|
71
74
|
self
|
|
72
75
|
end
|
|
73
76
|
|
|
74
77
|
def add_namespace(prefix, uri)
|
|
75
78
|
adapter.create_namespace(@native, prefix, uri,
|
|
76
79
|
namespace_validation_mode: context.config.namespace_validation_mode)
|
|
77
|
-
|
|
80
|
+
invalidate_namespace_cache!
|
|
78
81
|
self
|
|
79
82
|
rescue ValidationError => e
|
|
80
83
|
# Re-raise as NamespaceError, provide attributes for error context
|
|
@@ -106,7 +109,7 @@ module Moxml
|
|
|
106
109
|
else
|
|
107
110
|
adapter.set_namespace(@native, ns_or_hash&.native)
|
|
108
111
|
end
|
|
109
|
-
|
|
112
|
+
invalidate_namespace_cache!
|
|
110
113
|
end
|
|
111
114
|
|
|
112
115
|
def namespaces
|
|
@@ -119,9 +122,14 @@ module Moxml
|
|
|
119
122
|
# Returns all namespaces in scope for this element,
|
|
120
123
|
# including those inherited from ancestor elements.
|
|
121
124
|
def in_scope_namespaces
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
generation = context.namespace_scope_generation
|
|
126
|
+
if @in_scope_namespaces.nil? || @in_scope_generation != generation
|
|
127
|
+
@in_scope_namespaces = adapter.in_scope_namespaces(@native).map do |ns|
|
|
128
|
+
Namespace.new(ns, context)
|
|
129
|
+
end
|
|
130
|
+
@in_scope_generation = generation
|
|
124
131
|
end
|
|
132
|
+
@in_scope_namespaces
|
|
125
133
|
end
|
|
126
134
|
|
|
127
135
|
# Returns the namespace URI of this element (alias for namespace_uri)
|
|
@@ -209,5 +217,14 @@ module Moxml
|
|
|
209
217
|
def invalidate_attribute_cache!
|
|
210
218
|
@attributes = nil
|
|
211
219
|
end
|
|
220
|
+
|
|
221
|
+
# Clear the namespace caches and bump the context's scope
|
|
222
|
+
# generation so every wrapper — including ones not reachable from
|
|
223
|
+
# any children cache — recomputes on next read.
|
|
224
|
+
def invalidate_namespace_cache!
|
|
225
|
+
@namespaces = nil
|
|
226
|
+
@in_scope_namespaces = nil
|
|
227
|
+
context.bump_namespace_scope_generation
|
|
228
|
+
end
|
|
212
229
|
end
|
|
213
230
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Entity
|
|
5
|
+
# A named entity reference (&name;) as a value object, for
|
|
6
|
+
# adapters whose native trees cannot represent entity references
|
|
7
|
+
# (ox stores them in the tree; rexml and leptris carry them
|
|
8
|
+
# alongside). Adapter-specific namespaces alias this class so
|
|
9
|
+
# node_type dispatch keeps working.
|
|
10
|
+
class Reference
|
|
11
|
+
attr_reader :name
|
|
12
|
+
attr_accessor :parent
|
|
13
|
+
|
|
14
|
+
def initialize(name)
|
|
15
|
+
@name = name
|
|
16
|
+
@parent = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_xml
|
|
20
|
+
"&#{name};"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def ==(other)
|
|
24
|
+
other.is_a?(Reference) && name == other.name
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|