moxml 0.1.25 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +9 -0
  3. data/.github/workflows/windows.yml +62 -0
  4. data/Gemfile +3 -1
  5. data/README.adoc +133 -6
  6. data/docs/signature/algorithms.md +108 -0
  7. data/docs/signature/architecture.md +148 -0
  8. data/docs/signature/c14n.md +110 -0
  9. data/docs/signature/examples.md +109 -0
  10. data/docs/signature/flows.md +132 -0
  11. data/docs/signature/quick-reference.md +115 -0
  12. data/docs/signature/security.md +143 -0
  13. data/examples/signature/auto_key_extraction.rb +24 -0
  14. data/examples/signature/enveloped_rsa.rb +45 -0
  15. data/examples/signature/hmac.rb +33 -0
  16. data/lib/compat/opal/moxml_boot.rb +5 -0
  17. data/lib/moxml/adapter/base.rb +53 -49
  18. data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
  19. data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
  20. data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
  21. data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
  22. data/lib/moxml/adapter/customized_leptris.rb +16 -0
  23. data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
  24. data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
  25. data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
  26. data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
  27. data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
  28. data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
  29. data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
  30. data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
  31. data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
  32. data/lib/moxml/adapter/customized_oga.rb +2 -0
  33. data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
  34. data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
  35. data/lib/moxml/adapter/headed_ox.rb +9 -128
  36. data/lib/moxml/adapter/leptris.rb +965 -0
  37. data/lib/moxml/adapter/libxml.rb +131 -99
  38. data/lib/moxml/adapter/nokogiri.rb +29 -8
  39. data/lib/moxml/adapter/oga.rb +91 -54
  40. data/lib/moxml/adapter/ox.rb +93 -137
  41. data/lib/moxml/adapter/rexml.rb +61 -50
  42. data/lib/moxml/adapter.rb +2 -1
  43. data/lib/moxml/attribute.rb +8 -4
  44. data/lib/moxml/attribute_resolver.rb +189 -0
  45. data/lib/moxml/c14n/attribute_handler.rb +71 -0
  46. data/lib/moxml/c14n/character_encoder.rb +29 -0
  47. data/lib/moxml/c14n/data_model.rb +145 -0
  48. data/lib/moxml/c14n/exclusive.rb +188 -0
  49. data/lib/moxml/c14n/inclusive_10.rb +42 -0
  50. data/lib/moxml/c14n/inclusive_11.rb +21 -0
  51. data/lib/moxml/c14n/namespace_context.rb +68 -0
  52. data/lib/moxml/c14n/namespace_handler.rb +71 -0
  53. data/lib/moxml/c14n/node.rb +42 -0
  54. data/lib/moxml/c14n/nodes/attribute_node.rb +50 -0
  55. data/lib/moxml/c14n/nodes/comment_node.rb +29 -0
  56. data/lib/moxml/c14n/nodes/element_node.rb +62 -0
  57. data/lib/moxml/c14n/nodes/namespace_node.rb +41 -0
  58. data/lib/moxml/c14n/nodes/processing_instruction_node.rb +30 -0
  59. data/lib/moxml/c14n/nodes/root_node.rb +22 -0
  60. data/lib/moxml/c14n/nodes/text_node.rb +29 -0
  61. data/lib/moxml/c14n/nodes.rb +17 -0
  62. data/lib/moxml/c14n/processor.rb +118 -0
  63. data/lib/moxml/c14n/writer.rb +121 -0
  64. data/lib/moxml/c14n/xml_base_handler.rb +140 -0
  65. data/lib/moxml/c14n.rb +96 -0
  66. data/lib/moxml/config.rb +29 -3
  67. data/lib/moxml/context.rb +12 -0
  68. data/lib/moxml/element.rb +29 -12
  69. data/lib/moxml/entity/reference.rb +28 -0
  70. data/lib/moxml/entity.rb +125 -0
  71. data/lib/moxml/node.rb +90 -1
  72. data/lib/moxml/sax/namespace_splitter.rb +5 -2
  73. data/lib/moxml/signature/algorithms/base64_transform.rb +40 -0
  74. data/lib/moxml/signature/algorithms/canonicalization_base.rb +87 -0
  75. data/lib/moxml/signature/algorithms/digest_base.rb +51 -0
  76. data/lib/moxml/signature/algorithms/dsa_sha.rb +107 -0
  77. data/lib/moxml/signature/algorithms/ecdsa_sha.rb +125 -0
  78. data/lib/moxml/signature/algorithms/enveloped_signature_transform.rb +79 -0
  79. data/lib/moxml/signature/algorithms/exc_c14n_10.rb +23 -0
  80. data/lib/moxml/signature/algorithms/hmac_sha.rb +117 -0
  81. data/lib/moxml/signature/algorithms/inclusive_c14n_10.rb +21 -0
  82. data/lib/moxml/signature/algorithms/inclusive_c14n_11.rb +23 -0
  83. data/lib/moxml/signature/algorithms/rsa_pkcs1_sha.rb +56 -0
  84. data/lib/moxml/signature/algorithms/sha1.rb +13 -0
  85. data/lib/moxml/signature/algorithms/sha224.rb +13 -0
  86. data/lib/moxml/signature/algorithms/sha256.rb +13 -0
  87. data/lib/moxml/signature/algorithms/sha384.rb +13 -0
  88. data/lib/moxml/signature/algorithms/sha512.rb +13 -0
  89. data/lib/moxml/signature/algorithms/signature_method_base.rb +58 -0
  90. data/lib/moxml/signature/algorithms/transform_base.rb +40 -0
  91. data/lib/moxml/signature/algorithms.rb +113 -0
  92. data/lib/moxml/signature/errors.rb +99 -0
  93. data/lib/moxml/signature/key_extractor.rb +172 -0
  94. data/lib/moxml/signature/model/algorithm_method.rb +18 -0
  95. data/lib/moxml/signature/model/digest_method.rb +15 -0
  96. data/lib/moxml/signature/model/key/dsa_key_value.rb +28 -0
  97. data/lib/moxml/signature/model/key/ec_key_value.rb +22 -0
  98. data/lib/moxml/signature/model/key/rsa_key_value.rb +19 -0
  99. data/lib/moxml/signature/model/key/x509_data.rb +32 -0
  100. data/lib/moxml/signature/model/key/x509_digest.rb +19 -0
  101. data/lib/moxml/signature/model/key/x509_issuer_serial.rb +19 -0
  102. data/lib/moxml/signature/model/key_info.rb +21 -0
  103. data/lib/moxml/signature/model/key_value.rb +20 -0
  104. data/lib/moxml/signature/model/object_element.rb +18 -0
  105. data/lib/moxml/signature/model/reference.rb +22 -0
  106. data/lib/moxml/signature/model/signature.rb +21 -0
  107. data/lib/moxml/signature/model/signature_value.rb +16 -0
  108. data/lib/moxml/signature/model/signed_info.rb +20 -0
  109. data/lib/moxml/signature/model/transform.rb +16 -0
  110. data/lib/moxml/signature/model/transforms.rb +36 -0
  111. data/lib/moxml/signature/model.rb +35 -0
  112. data/lib/moxml/signature/parser.rb +270 -0
  113. data/lib/moxml/signature/reference_resolver.rb +79 -0
  114. data/lib/moxml/signature/reference_result.rb +22 -0
  115. data/lib/moxml/signature/serializer.rb +163 -0
  116. data/lib/moxml/signature/signer.rb +73 -0
  117. data/lib/moxml/signature/single_verification_result.rb +31 -0
  118. data/lib/moxml/signature/transform_pipeline.rb +106 -0
  119. data/lib/moxml/signature/verification_result.rb +27 -0
  120. data/lib/moxml/signature/verifier.rb +129 -0
  121. data/lib/moxml/signature.rb +94 -0
  122. data/lib/moxml/version.rb +1 -1
  123. data/lib/moxml/xml_emitter.rb +71 -0
  124. data/lib/moxml/xpath/compiler.rb +60 -7
  125. data/lib/moxml/xpath/parser.rb +22 -15
  126. data/lib/moxml.rb +5 -0
  127. data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
  128. data/reference-docs/w3c-xmldsig-core.md +400 -0
  129. data/spec/examples/readme_examples_spec.rb +0 -4
  130. data/spec/examples/xpath_examples_spec.rb +0 -11
  131. data/spec/fixtures/xmldsig/keys/rsa_private.pem +28 -0
  132. data/spec/fixtures/xmldsig/keys/rsa_public.pem +9 -0
  133. data/spec/fixtures/xmldsig/keys/rsa_ref.pem +15 -0
  134. data/spec/fixtures/xmldsig/keys/rsa_ref.pub +6 -0
  135. data/spec/fixtures/xmldsig/sign2-doc.xml +6 -0
  136. data/spec/fixtures/xmldsig/sign2-result.xml +25 -0
  137. data/spec/fixtures/xmldsig/sign3-result.xml +39 -0
  138. data/spec/integration/all_adapters_spec.rb +17 -0
  139. data/spec/integration/sax_parity_spec.rb +58 -0
  140. data/spec/integration/shared_examples/edge_cases.rb +0 -10
  141. data/spec/integration/shared_examples/integration_workflows.rb +0 -10
  142. data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
  143. data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
  144. data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +0 -8
  145. data/spec/moxml/adapter/leptris_spec.rb +75 -0
  146. data/spec/moxml/adapter/ox_spec.rb +20 -6
  147. data/spec/moxml/adapter/platform_spec.rb +15 -2
  148. data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
  149. data/spec/moxml/attribute_resolver_spec.rb +108 -0
  150. data/spec/moxml/c14n/api_spec.rb +117 -0
  151. data/spec/moxml/c14n/c14n_spec.rb +88 -0
  152. data/spec/moxml/c14n/comments_pis_spec.rb +65 -0
  153. data/spec/moxml/c14n/inclusive10_spec.rb +60 -0
  154. data/spec/moxml/c14n/namespace_edge_cases_spec.rb +88 -0
  155. data/spec/moxml/c14n/xml_attributes_spec.rb +54 -0
  156. data/spec/moxml/doctype_spec.rb +1 -1
  157. data/spec/moxml/entity_spec.rb +76 -0
  158. data/spec/moxml/node_spec.rb +104 -0
  159. data/spec/moxml/sax_entity_parity_spec.rb +358 -0
  160. data/spec/moxml/signature/algorithms/base64_transform_spec.rb +26 -0
  161. data/spec/moxml/signature/algorithms/digest_base_spec.rb +72 -0
  162. data/spec/moxml/signature/algorithms/dsa_sha_spec.rb +46 -0
  163. data/spec/moxml/signature/algorithms/ecdsa_sha_spec.rb +72 -0
  164. data/spec/moxml/signature/algorithms/enveloped_signature_transform_spec.rb +40 -0
  165. data/spec/moxml/signature/algorithms/hmac_sha_spec.rb +71 -0
  166. data/spec/moxml/signature/algorithms/rsa_pkcs1_sha_spec.rb +55 -0
  167. data/spec/moxml/signature/algorithms_spec.rb +76 -0
  168. data/spec/moxml/signature/cross_verify_spec.rb +64 -0
  169. data/spec/moxml/signature/edge_cases_spec.rb +284 -0
  170. data/spec/moxml/signature/fixtures_spec.rb +80 -0
  171. data/spec/moxml/signature/key_extractor_spec.rb +96 -0
  172. data/spec/moxml/signature/model_spec.rb +54 -0
  173. data/spec/moxml/signature/round_trip_spec.rb +113 -0
  174. data/spec/moxml/xml_emitter_spec.rb +64 -0
  175. data/spec/moxml/xpath/axes_spec.rb +72 -0
  176. data/spec/moxml/xpath/parser_spec.rb +6 -0
  177. data/spec/moxml/xpath_capabilities_spec.rb +5 -3
  178. data/spec/performance/benchmark_spec.rb +13 -6
  179. data/spec/performance/thread_safety_spec.rb +0 -4
  180. data/spec/spec_helper.rb +5 -1
  181. metadata +130 -2
@@ -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
- DEFAULT_ADAPTER = :nokogiri
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
- detect_loaded_adapter || DEFAULT_ADAPTER
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
- adapter.set_attribute(@native, name, normalize_xml_value(value))
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
- val = adapter.get_attribute_value(@native, name)
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
- native_attr = adapter.get_attribute(@native, name)
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
- adapter.remove_attribute(@native, name)
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
- @namespaces = nil
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
- @namespaces = nil
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
- adapter.in_scope_namespaces(@native).map do |ns|
123
- Namespace.new(ns, context)
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