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/entity.rb
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
# Entity-reference round-trip pipeline.
|
|
5
|
+
#
|
|
6
|
+
# XML libraries split into two camps: those that preserve named
|
|
7
|
+
# entity references natively (nokogiri) and those that expand or
|
|
8
|
+
# reject them at parse (ox, rexml, oga, libxml, leptris). Moxml
|
|
9
|
+
# bridges the camps with one pipeline, owned here and delegated to
|
|
10
|
+
# by Adapter::Base:
|
|
11
|
+
#
|
|
12
|
+
# 1. Parse time — .preprocess_entities rewrites non-standard
|
|
13
|
+
# entity references to a two-character marker (see MARKER) so
|
|
14
|
+
# the reference survives native parsing verbatim. The five
|
|
15
|
+
# standard entities (& < > " ') are NOT
|
|
16
|
+
# converted.
|
|
17
|
+
# 2. Read time — marker-bearing text is split into text and
|
|
18
|
+
# Entity::Reference nodes (adapters whose natives cannot hold
|
|
19
|
+
# references), or decoded in place (.decode_entities covers the
|
|
20
|
+
# five standard entities plus numeric character references).
|
|
21
|
+
# 3. Serialize time — .restore_entities maps markers back to named
|
|
22
|
+
# references, including markers a native serializer already
|
|
23
|
+
# rendered as character references.
|
|
24
|
+
#
|
|
25
|
+
# How references are STORED between parse and serialize is
|
|
26
|
+
# adapter-shaped (in-tree value objects for ox, attachment
|
|
27
|
+
# sequences for rexml/libxml, text markers for oga/leptris); the
|
|
28
|
+
# marker lifecycle and the Entity::Reference value type are the
|
|
29
|
+
# shared, single-sourced parts.
|
|
30
|
+
module Entity
|
|
31
|
+
autoload :Reference, "moxml/entity/reference"
|
|
32
|
+
|
|
33
|
+
# Marker for adapters that resolve entities during parsing.
|
|
34
|
+
# U+FFFC (Object Replacement Character) + U+FEFF (BOM) is a
|
|
35
|
+
# two-character sentinel chosen because this exact sequence
|
|
36
|
+
# followed by a valid entity name pattern is vanishingly unlikely
|
|
37
|
+
# in real XML content.
|
|
38
|
+
MARKER = "\u{FFFC}\u{FEFF}"
|
|
39
|
+
NAME_PATTERN = "[a-zA-Z_][\\w.:-]*"
|
|
40
|
+
NAME_RE = /&(#{NAME_PATTERN});/
|
|
41
|
+
MARKER_RE = /\u{FFFC}\u{FEFF}(#{NAME_PATTERN});/
|
|
42
|
+
SERIALIZED_MARKER_RE = /(#{NAME_PATTERN});/
|
|
43
|
+
STANDARD_ENTITIES = %w[amp lt gt quot apos].freeze
|
|
44
|
+
|
|
45
|
+
NAMED_DECODE_MAP = {
|
|
46
|
+
"amp" => "&", "lt" => "<", "gt" => ">",
|
|
47
|
+
"quot" => '"', "apos" => "'"
|
|
48
|
+
}.freeze
|
|
49
|
+
private_constant :NAMED_DECODE_MAP
|
|
50
|
+
|
|
51
|
+
DECODE_RE = /&(?:(amp|lt|gt|quot|apos)|#(?:(\d+)|[xX]([0-9a-fA-F]+)));/
|
|
52
|
+
private_constant :DECODE_RE
|
|
53
|
+
|
|
54
|
+
module_function
|
|
55
|
+
|
|
56
|
+
# Replace non-standard entity references with markers before
|
|
57
|
+
# parsing. Always returns a UTF-8 encoded string.
|
|
58
|
+
def preprocess_entities(xml)
|
|
59
|
+
return "" if xml.nil?
|
|
60
|
+
|
|
61
|
+
str = if xml.encoding == Encoding::BINARY
|
|
62
|
+
# Binary strings are assumed to be UTF-8. If the bytes are
|
|
63
|
+
# not valid UTF-8, fall back to encoding as UTF-8 with
|
|
64
|
+
# replacement to avoid raising on gsub.
|
|
65
|
+
dup = xml.dup.force_encoding("UTF-8")
|
|
66
|
+
if dup.valid_encoding?
|
|
67
|
+
dup
|
|
68
|
+
else
|
|
69
|
+
xml.dup.encode("UTF-8",
|
|
70
|
+
"ASCII-8BIT", invalid: :replace, undef: :replace)
|
|
71
|
+
end
|
|
72
|
+
elsif xml.encoding == Encoding::UTF_8
|
|
73
|
+
xml
|
|
74
|
+
else
|
|
75
|
+
xml.encode("UTF-8")
|
|
76
|
+
end
|
|
77
|
+
# Fast path: no `&` means no entity references to mark — skip
|
|
78
|
+
# the regex scan and string allocation entirely. The vast
|
|
79
|
+
# majority of XML payloads contain no entity references.
|
|
80
|
+
return str unless str.include?("&")
|
|
81
|
+
|
|
82
|
+
str.gsub(NAME_RE) do |match|
|
|
83
|
+
STANDARD_ENTITIES.include?(::Regexp.last_match(1)) ? match : "#{MARKER}#{::Regexp.last_match(1)};"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Resolve numeric (&#NN; / &#xNN;) and the five standard named
|
|
88
|
+
# (& < > " ') XML entity references in one
|
|
89
|
+
# single pass. The resulting characters are data; no further
|
|
90
|
+
# decoding is applied. Numeric refs producing invalid UTF-8
|
|
91
|
+
# (NUL, surrogate halves, > U+10FFFF) are preserved verbatim
|
|
92
|
+
# to avoid silently emitting malformed bytes.
|
|
93
|
+
def decode_entities(text)
|
|
94
|
+
return text unless text.is_a?(String) && text.include?("&")
|
|
95
|
+
|
|
96
|
+
text.gsub(DECODE_RE) do
|
|
97
|
+
if (named = ::Regexp.last_match(1))
|
|
98
|
+
NAMED_DECODE_MAP[named]
|
|
99
|
+
else
|
|
100
|
+
code = ::Regexp.last_match(2) ? ::Regexp.last_match(2).to_i : ::Regexp.last_match(3).to_i(16)
|
|
101
|
+
if code.zero? || code.between?(0xD800, 0xDFFF) || code > 0x10FFFF
|
|
102
|
+
::Regexp.last_match(0)
|
|
103
|
+
else
|
|
104
|
+
[code].pack("U")
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Restore entity markers back to named entity references.
|
|
111
|
+
def restore_entities(text)
|
|
112
|
+
return text unless text.is_a?(String)
|
|
113
|
+
|
|
114
|
+
# Force UTF-8 encoding since markers are UTF-8 characters
|
|
115
|
+
str = text.encoding == Encoding::UTF_8 ? text : text.dup.force_encoding("UTF-8")
|
|
116
|
+
# Fast path: the vast majority of documents carry no entity
|
|
117
|
+
# markers — two C-level scans beat two regex passes.
|
|
118
|
+
return str unless str.include?(MARKER) ||
|
|
119
|
+
str.include?("")
|
|
120
|
+
|
|
121
|
+
result = str.gsub(MARKER_RE, '&\1;')
|
|
122
|
+
result.gsub(SERIALIZED_MARKER_RE, '&\1;')
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
data/lib/moxml/node.rb
CHANGED
|
@@ -55,6 +55,8 @@ module Moxml
|
|
|
55
55
|
refreshed = adapter.actual_native(node.native, @native)
|
|
56
56
|
node.refresh_native!(refreshed) if refreshed && refreshed != node.native
|
|
57
57
|
node.parent_node = self
|
|
58
|
+
# The adopted subtree's in-scope namespaces changed
|
|
59
|
+
node.invalidate_namespace_cache!
|
|
58
60
|
invalidate_children_cache!
|
|
59
61
|
self
|
|
60
62
|
end
|
|
@@ -77,9 +79,15 @@ module Moxml
|
|
|
77
79
|
invalidate_parent_children_cache!
|
|
78
80
|
adapter.remove(@native)
|
|
79
81
|
invalidate_children_cache!
|
|
82
|
+
# The detached subtree left its declaring ancestors behind
|
|
83
|
+
invalidate_namespace_cache!
|
|
80
84
|
self
|
|
81
85
|
end
|
|
82
86
|
|
|
87
|
+
# Namespace-scope caches live on Element; the base no-op lets tree
|
|
88
|
+
# mutations invalidate uniformly without type checks.
|
|
89
|
+
def invalidate_namespace_cache!; end
|
|
90
|
+
|
|
83
91
|
def replace(node)
|
|
84
92
|
node = prepare_node(node)
|
|
85
93
|
invalidate_parent_children_cache!
|
|
@@ -103,7 +111,10 @@ module Moxml
|
|
|
103
111
|
end
|
|
104
112
|
|
|
105
113
|
def xpath(expression, namespaces = {})
|
|
106
|
-
|
|
114
|
+
result = adapter.xpath(@native, expression, namespaces)
|
|
115
|
+
# Adapter contract: Array<native> | scalar. Scalars (count(),
|
|
116
|
+
# string-length(), booleans) pass through unwrapped.
|
|
117
|
+
result.is_a?(Array) ? NodeSet.new(result, context) : result
|
|
107
118
|
end
|
|
108
119
|
|
|
109
120
|
def at_xpath(expression, namespaces = {})
|
|
@@ -186,6 +197,69 @@ module Moxml
|
|
|
186
197
|
children.each(&block)
|
|
187
198
|
end
|
|
188
199
|
|
|
200
|
+
# Returns all ancestor nodes from the parent up to and including
|
|
201
|
+
# the document node.
|
|
202
|
+
#
|
|
203
|
+
# @return [NodeSet] ancestors ordered nearest-first
|
|
204
|
+
def ancestors
|
|
205
|
+
return NodeSet.new([], context) if document?
|
|
206
|
+
|
|
207
|
+
natives = []
|
|
208
|
+
current = parent
|
|
209
|
+
while current
|
|
210
|
+
natives << current.native
|
|
211
|
+
break if current.document?
|
|
212
|
+
|
|
213
|
+
current = current.parent
|
|
214
|
+
end
|
|
215
|
+
NodeSet.new(natives, context)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Returns all descendant nodes (children, grandchildren, and so on),
|
|
219
|
+
# excluding the node itself.
|
|
220
|
+
#
|
|
221
|
+
# @return [NodeSet] descendants in document order
|
|
222
|
+
def descendants
|
|
223
|
+
natives = []
|
|
224
|
+
each_node { |node| natives << node.native }
|
|
225
|
+
NodeSet.new(natives, context)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Returns an XPath expression that uniquely locates this node within
|
|
229
|
+
# its document. Positional predicates are emitted only when sibling
|
|
230
|
+
# elements share the same qualified name, keeping paths minimal.
|
|
231
|
+
#
|
|
232
|
+
# @return [String] XPath expression
|
|
233
|
+
# @raise [Moxml::NotImplementedError] for node types other than
|
|
234
|
+
# element and document
|
|
235
|
+
def path
|
|
236
|
+
return "/" if document?
|
|
237
|
+
|
|
238
|
+
unless element?
|
|
239
|
+
raise Moxml::NotImplementedError.new(
|
|
240
|
+
"path is only supported for element and document nodes",
|
|
241
|
+
feature: "path",
|
|
242
|
+
)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
segments = []
|
|
246
|
+
current = self
|
|
247
|
+
while current && !current.document?
|
|
248
|
+
segments.unshift(path_segment_for(current))
|
|
249
|
+
current = current.parent
|
|
250
|
+
end
|
|
251
|
+
"/#{segments.join('/')}"
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Returns the 1-based line number where this node appears in the
|
|
255
|
+
# source XML, or nil when the underlying adapter does not track
|
|
256
|
+
# source positions.
|
|
257
|
+
#
|
|
258
|
+
# @return [Integer, nil]
|
|
259
|
+
def line_number
|
|
260
|
+
adapter.line_number(@native)
|
|
261
|
+
end
|
|
262
|
+
|
|
189
263
|
def outer_xml
|
|
190
264
|
to_xml
|
|
191
265
|
end
|
|
@@ -286,6 +360,21 @@ module Moxml
|
|
|
286
360
|
|
|
287
361
|
private
|
|
288
362
|
|
|
363
|
+
# XPath segment for an element: the qualified name, plus a positional
|
|
364
|
+
# predicate only when same-named element siblings make it ambiguous.
|
|
365
|
+
def path_segment_for(element)
|
|
366
|
+
name = element.name
|
|
367
|
+
parent = element.parent
|
|
368
|
+
return name unless parent
|
|
369
|
+
|
|
370
|
+
same_name = parent.children.select do |child|
|
|
371
|
+
child.element? && child.name == name
|
|
372
|
+
end
|
|
373
|
+
return name if same_name.size == 1
|
|
374
|
+
|
|
375
|
+
"#{name}[#{same_name.find_index(element) + 1}]"
|
|
376
|
+
end
|
|
377
|
+
|
|
289
378
|
def prepare_node(node)
|
|
290
379
|
case node
|
|
291
380
|
when String then Text.new(adapter.create_text(node), context)
|
|
@@ -9,6 +9,8 @@ module Moxml
|
|
|
9
9
|
# attribute. This module provides a single implementation.
|
|
10
10
|
module NamespaceSplitter
|
|
11
11
|
# @param attributes [Hash, Array<Array>] attributes as a hash or array of pairs
|
|
12
|
+
# @yieldparam value [Object] raw attribute/namespace value
|
|
13
|
+
# @yieldreturn [Object] transformed value to store
|
|
12
14
|
# @return [Array(Hash, Hash)] [regular_attrs, namespaces]
|
|
13
15
|
def split_attributes_and_namespaces(attributes)
|
|
14
16
|
attrs = {}
|
|
@@ -16,11 +18,12 @@ module Moxml
|
|
|
16
18
|
|
|
17
19
|
each_attribute(attributes) do |name, value|
|
|
18
20
|
name_s = name.to_s
|
|
21
|
+
v = block_given? ? yield(value) : value
|
|
19
22
|
if name_s == "xmlns" || name_s.start_with?("xmlns:")
|
|
20
23
|
prefix = name_s == "xmlns" ? nil : name_s.sub("xmlns:", "")
|
|
21
|
-
ns[prefix] =
|
|
24
|
+
ns[prefix] = v
|
|
22
25
|
else
|
|
23
|
-
attrs[name_s] =
|
|
26
|
+
attrs[name_s] = v
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
29
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module Moxml
|
|
6
|
+
module Signature
|
|
7
|
+
module Algorithms
|
|
8
|
+
# Base64 decode transform (W3C §6.6.2).
|
|
9
|
+
#
|
|
10
|
+
# Input: octets or nodeset. For nodeset, logically applies self::text(),
|
|
11
|
+
# sorts nodes by document order, concatenates string values, then decodes.
|
|
12
|
+
# Output: octets.
|
|
13
|
+
class Base64Transform < TransformBase
|
|
14
|
+
identifier "http://www.w3.org/2000/09/xmldsig#base64"
|
|
15
|
+
|
|
16
|
+
def self.input_type; :octets; end
|
|
17
|
+
def self.output_type; :octets; end
|
|
18
|
+
|
|
19
|
+
def transform(input)
|
|
20
|
+
text = input.is_a?(Array) ? input.map { |n| text_of(n) }.join : input.to_s
|
|
21
|
+
stripped = text.gsub(/\s+/, "")
|
|
22
|
+
Base64.strict_decode64(stripped)
|
|
23
|
+
rescue ArgumentError => e
|
|
24
|
+
raise TransformError.new(
|
|
25
|
+
"base64 decode failed: #{e.message}",
|
|
26
|
+
algorithm: self.class.identifier_uri,
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def text_of(node)
|
|
33
|
+
return node.text if node.is_a?(::Moxml::Node)
|
|
34
|
+
|
|
35
|
+
node.to_s
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module Moxml
|
|
6
|
+
module Signature
|
|
7
|
+
module Algorithms
|
|
8
|
+
# Base class for canonicalization algorithms.
|
|
9
|
+
#
|
|
10
|
+
# Subclasses declare `identifier "http://..."` and implement #engine
|
|
11
|
+
# (returning a Moxml::C14n::* walker). Canonicalizers operate on a
|
|
12
|
+
# Moxml::Node subtree and return UTF-8 octets.
|
|
13
|
+
#
|
|
14
|
+
# Canonicalization algorithms can also be used as transforms per
|
|
15
|
+
# spec §6.6.1. The base class provides the #transform method that
|
|
16
|
+
# adapts the canonicalize interface to the transform pipeline.
|
|
17
|
+
class CanonicalizationBase
|
|
18
|
+
class << self
|
|
19
|
+
# Canonicalization presents the transform interface (spec §6.6.1).
|
|
20
|
+
# Input: octet stream or node-set. Output: octet stream.
|
|
21
|
+
def input_type; :nodeset; end
|
|
22
|
+
|
|
23
|
+
def output_type; :octets; end
|
|
24
|
+
|
|
25
|
+
def identifier(uri)
|
|
26
|
+
Algorithms.register(:canonicalization, uri, self)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def for_uri(uri, **opts)
|
|
30
|
+
new(identifier_uri: uri, **opts)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
attr_reader :identifier_uri, :with_comments, :inclusive_namespaces,
|
|
35
|
+
:context
|
|
36
|
+
|
|
37
|
+
# `context:` is required when this algorithm is used as a transform
|
|
38
|
+
# so that octet-stream input can be parsed with the same adapter
|
|
39
|
+
# the caller used for the rest of the document.
|
|
40
|
+
def initialize(identifier_uri: nil, with_comments: nil,
|
|
41
|
+
inclusive_namespaces: [], context: nil, **_unused)
|
|
42
|
+
@identifier_uri = identifier_uri
|
|
43
|
+
@with_comments = with_comments.nil? ? uri_has_comments?(identifier_uri) : with_comments
|
|
44
|
+
@inclusive_namespaces = inclusive_namespaces || []
|
|
45
|
+
@context = context
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def canonicalize(node)
|
|
49
|
+
engine.canonicalize(
|
|
50
|
+
node,
|
|
51
|
+
with_comments: @with_comments,
|
|
52
|
+
inclusive_namespaces: @inclusive_namespaces,
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Adapt to the transform interface (spec §6.6.1). Octet-stream
|
|
57
|
+
# input is parsed using the same Moxml::Context the caller used;
|
|
58
|
+
# this preserves the byte-exact invariant across adapters.
|
|
59
|
+
def transform(input)
|
|
60
|
+
return canonicalize(input) if input.is_a?(::Moxml::Node)
|
|
61
|
+
|
|
62
|
+
if context.nil?
|
|
63
|
+
raise TransformError,
|
|
64
|
+
"canonicalization transform requires a context to parse " \
|
|
65
|
+
"octet-stream input; none was provided"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
parsed = context.parse(input.to_s)
|
|
69
|
+
canonicalize(parsed.root)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def uri_has_comments?(uri)
|
|
75
|
+
return false if uri.nil?
|
|
76
|
+
|
|
77
|
+
uri.end_with?("#WithComments")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def engine
|
|
81
|
+
raise NotImplementedError,
|
|
82
|
+
"#{self.class} must implement #engine"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "base64"
|
|
5
|
+
|
|
6
|
+
module Moxml
|
|
7
|
+
module Signature
|
|
8
|
+
module Algorithms
|
|
9
|
+
# Base class for message digest algorithms (SHA family).
|
|
10
|
+
#
|
|
11
|
+
# Subclasses MUST declare:
|
|
12
|
+
# - `identifier "http://..."` to register
|
|
13
|
+
# - either `digest_bits N` and `openssl_digest_name "SHA256"` etc.,
|
|
14
|
+
# OR override `#compute_digest`.
|
|
15
|
+
class DigestBase
|
|
16
|
+
class << self
|
|
17
|
+
attr_reader :identifier_uri, :digest_size_bits
|
|
18
|
+
|
|
19
|
+
def identifier(uri)
|
|
20
|
+
@identifier_uri = uri
|
|
21
|
+
Algorithms.register(:digest, uri, self)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def digest_bits(bits)
|
|
25
|
+
@digest_size_bits = bits
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def openssl_digest_name(name)
|
|
29
|
+
define_method(:compute_digest) do |data|
|
|
30
|
+
OpenSSL::Digest.digest(name, data)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def digest(data)
|
|
36
|
+
compute_digest(data.to_s)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def digest_base64(data)
|
|
40
|
+
Base64.strict_encode64(digest(data))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def compute_digest(_data)
|
|
44
|
+
raise NotImplementedError,
|
|
45
|
+
"#{self.class} must implement #compute_digest " \
|
|
46
|
+
"or declare openssl_digest_name"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module Moxml
|
|
6
|
+
module Signature
|
|
7
|
+
module Algorithms
|
|
8
|
+
# DSA signature methods per FIPS 186-3.
|
|
9
|
+
#
|
|
10
|
+
# Two URIs:
|
|
11
|
+
# - SHA1 (1024-bit, q=160): wire format r‖s with 20-byte halves
|
|
12
|
+
# - SHA256 (2048-bit, q=256): wire format r‖s with N-byte halves
|
|
13
|
+
# where N = byte length of q (32 for SHA-256 case)
|
|
14
|
+
#
|
|
15
|
+
# OpenSSL returns DER; we convert to raw r‖s.
|
|
16
|
+
class DsaSha < SignatureMethodBase
|
|
17
|
+
PAIRINGS = {
|
|
18
|
+
"http://www.w3.org/2000/09/xmldsig#dsa-sha1" => "SHA1",
|
|
19
|
+
"http://www.w3.org/2009/xmldsig11#dsa-sha256" => "SHA256",
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
PAIRINGS.each_key { |uri| identifier uri }
|
|
23
|
+
|
|
24
|
+
def initialize(identifier_uri:, parameters: nil)
|
|
25
|
+
super(nil)
|
|
26
|
+
@identifier_uri = identifier_uri
|
|
27
|
+
@parameters = parameters
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def compute_signature(data, key)
|
|
31
|
+
dsa = coerce_signing_key(key)
|
|
32
|
+
digest_name = self.class::PAIRINGS.fetch(@identifier_uri)
|
|
33
|
+
der_signature = dsa.sign(digest_name, data)
|
|
34
|
+
der_to_raw(der_signature, coordinate_bytes_for(dsa))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def verify_signature(data, key, signature)
|
|
38
|
+
dsa = coerce_verify_key(key)
|
|
39
|
+
digest_name = self.class::PAIRINGS.fetch(@identifier_uri)
|
|
40
|
+
n_bytes = coordinate_bytes_for(dsa)
|
|
41
|
+
der_signature = raw_to_der(signature, n_bytes)
|
|
42
|
+
dsa.verify(digest_name, der_signature, data)
|
|
43
|
+
rescue ArgumentError
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def coerce_signing_key(key)
|
|
50
|
+
case key
|
|
51
|
+
when OpenSSL::PKey::DSA
|
|
52
|
+
key
|
|
53
|
+
else
|
|
54
|
+
raise SignatureKeyError,
|
|
55
|
+
"DSA signature method requires OpenSSL::PKey::DSA, " \
|
|
56
|
+
"got #{key.class}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def coerce_verify_key(key)
|
|
61
|
+
coerce_signing_key(key)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def coordinate_bytes_for(dsa)
|
|
65
|
+
# q is the subgroup order. byte length of q.
|
|
66
|
+
q_bits = dsa.q.num_bits
|
|
67
|
+
(q_bits + 7) / 8
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def der_to_raw(der, n_bytes)
|
|
71
|
+
seq = OpenSSL::ASN1.decode(der)
|
|
72
|
+
r = seq.value[0].value
|
|
73
|
+
s = seq.value[1].value
|
|
74
|
+
i2osp(r, n_bytes) + i2osp(s, n_bytes)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def raw_to_der(raw, n_bytes)
|
|
78
|
+
raise ArgumentError, "raw signature has wrong size" if raw.bytesize != (2 * n_bytes)
|
|
79
|
+
|
|
80
|
+
r = raw.byteslice(0, n_bytes)
|
|
81
|
+
s = raw.byteslice(n_bytes, n_bytes)
|
|
82
|
+
seq = OpenSSL::ASN1::Sequence.new([
|
|
83
|
+
OpenSSL::ASN1::Integer.new(asn1_integer_value(r)),
|
|
84
|
+
OpenSSL::ASN1::Integer.new(asn1_integer_value(s)),
|
|
85
|
+
])
|
|
86
|
+
seq.to_der
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def i2osp(value, length)
|
|
90
|
+
n = value.to_i
|
|
91
|
+
raise ArgumentError, "I2OSP: integer too large" if n >= (1 << (8 * length))
|
|
92
|
+
|
|
93
|
+
bytes = Array.new(length, 0)
|
|
94
|
+
(length - 1).downto(0) do |i|
|
|
95
|
+
bytes[i] = n & 0xff
|
|
96
|
+
n >>= 8
|
|
97
|
+
end
|
|
98
|
+
bytes.pack("C*")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def asn1_integer_value(octets)
|
|
102
|
+
octets.bytes.reduce(0) { |acc, byte| (acc << 8) | byte }
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module Moxml
|
|
6
|
+
module Signature
|
|
7
|
+
module Algorithms
|
|
8
|
+
# ECDSA signature methods per FIPS 186-3, bound to a digest.
|
|
9
|
+
#
|
|
10
|
+
# Spec §6.4.3 requires support for ECDSAwithSHA256 over the P-256
|
|
11
|
+
# curve. Recommended: P-384, P-521.
|
|
12
|
+
#
|
|
13
|
+
# The wire format is base64(I2OSP(r, n_bytes) || I2OSP(s, n_bytes))
|
|
14
|
+
# where n_bytes is the byte length of the base point order
|
|
15
|
+
# (32 for P-256, 48 for P-384, 66 for P-521).
|
|
16
|
+
#
|
|
17
|
+
# OpenSSL returns a DER-encoded ASN.1 sequence of (r, s). We convert
|
|
18
|
+
# to the raw r||s form on sign, and back from raw to DER on verify.
|
|
19
|
+
class EcdsaSha < SignatureMethodBase
|
|
20
|
+
PAIRINGS = {
|
|
21
|
+
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1" => "SHA1",
|
|
22
|
+
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha224" => "SHA224",
|
|
23
|
+
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" => "SHA256",
|
|
24
|
+
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384" => "SHA384",
|
|
25
|
+
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512" => "SHA512",
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# Map OpenSSL curve name → byte length of base point order.
|
|
29
|
+
CURVE_ORDER_BYTES = {
|
|
30
|
+
"prime256v1" => 32, # P-256
|
|
31
|
+
"secp384r1" => 48, # P-384
|
|
32
|
+
"secp521r1" => 66, # P-521 (ceil(521/8) = 66)
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
PAIRINGS.each_key { |uri| identifier uri }
|
|
36
|
+
|
|
37
|
+
def initialize(identifier_uri:, parameters: nil)
|
|
38
|
+
super(nil)
|
|
39
|
+
@identifier_uri = identifier_uri
|
|
40
|
+
@parameters = parameters
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def compute_signature(data, key)
|
|
44
|
+
ec_key = coerce_key(key)
|
|
45
|
+
digest_name = self.class::PAIRINGS.fetch(@identifier_uri)
|
|
46
|
+
der_signature = ec_key.sign(digest_name, data)
|
|
47
|
+
der_to_raw(der_signature, coordinate_bytes_for(ec_key))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def verify_signature(data, key, signature)
|
|
51
|
+
ec_key = coerce_key(key)
|
|
52
|
+
digest_name = self.class::PAIRINGS.fetch(@identifier_uri)
|
|
53
|
+
n_bytes = coordinate_bytes_for(ec_key)
|
|
54
|
+
der_signature = raw_to_der(signature, n_bytes)
|
|
55
|
+
ec_key.verify(digest_name, der_signature, data)
|
|
56
|
+
rescue ArgumentError
|
|
57
|
+
false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def coerce_key(key)
|
|
63
|
+
case key
|
|
64
|
+
when OpenSSL::PKey::EC then key
|
|
65
|
+
when OpenSSL::PKey::EC::Point
|
|
66
|
+
raise SignatureKeyError,
|
|
67
|
+
"ECDSA requires an EC key, not a bare Point"
|
|
68
|
+
else
|
|
69
|
+
raise SignatureKeyError,
|
|
70
|
+
"ECDSA signature method requires OpenSSL::PKey::EC, " \
|
|
71
|
+
"got #{key.class}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def coordinate_bytes_for(ec_key)
|
|
76
|
+
curve_name = ec_key.group.curve_name
|
|
77
|
+
self.class::CURVE_ORDER_BYTES.fetch(curve_name) do
|
|
78
|
+
raise SignatureError,
|
|
79
|
+
"ECDSA curve #{curve_name.inspect} not supported; " \
|
|
80
|
+
"add it to EcdsaSha::CURVE_ORDER_BYTES"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Convert ASN.1 DER sequence of two INTEGERs to raw r‖s octets.
|
|
85
|
+
def der_to_raw(der, n_bytes)
|
|
86
|
+
seq = OpenSSL::ASN1.decode(der)
|
|
87
|
+
r = seq.value[0].value
|
|
88
|
+
s = seq.value[1].value
|
|
89
|
+
i2osp(r, n_bytes) + i2osp(s, n_bytes)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Convert raw r‖s octets back to ASN.1 DER.
|
|
93
|
+
def raw_to_der(raw, n_bytes)
|
|
94
|
+
raise ArgumentError, "raw signature has wrong size" if raw.bytesize != (2 * n_bytes)
|
|
95
|
+
|
|
96
|
+
r = raw.byteslice(0, n_bytes)
|
|
97
|
+
s = raw.byteslice(n_bytes, n_bytes)
|
|
98
|
+
seq = OpenSSL::ASN1::Sequence.new([
|
|
99
|
+
OpenSSL::ASN1::Integer.new(asn1_integer_value(r)),
|
|
100
|
+
OpenSSL::ASN1::Integer.new(asn1_integer_value(s)),
|
|
101
|
+
])
|
|
102
|
+
seq.to_der
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Integer to octet stream (I2OSP), minimal length parameter.
|
|
106
|
+
def i2osp(value, length)
|
|
107
|
+
n = value.to_i
|
|
108
|
+
raise ArgumentError, "I2OSP: integer too large" if n >= (1 << (8 * length))
|
|
109
|
+
|
|
110
|
+
bytes = Array.new(length, 0)
|
|
111
|
+
(length - 1).downto(0) do |i|
|
|
112
|
+
bytes[i] = n & 0xff
|
|
113
|
+
n >>= 8
|
|
114
|
+
end
|
|
115
|
+
bytes.pack("C*")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Interpret an octet string as a positive integer (OS2IP).
|
|
119
|
+
def asn1_integer_value(octets)
|
|
120
|
+
octets.bytes.reduce(0) { |acc, byte| (acc << 8) | byte }
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|