moxml 0.1.25 → 0.1.26

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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +9 -0
  3. data/README.adoc +64 -0
  4. data/docs/signature/algorithms.md +108 -0
  5. data/docs/signature/architecture.md +148 -0
  6. data/docs/signature/c14n.md +110 -0
  7. data/docs/signature/examples.md +109 -0
  8. data/docs/signature/flows.md +132 -0
  9. data/docs/signature/quick-reference.md +115 -0
  10. data/docs/signature/security.md +143 -0
  11. data/examples/signature/auto_key_extraction.rb +24 -0
  12. data/examples/signature/enveloped_rsa.rb +45 -0
  13. data/examples/signature/hmac.rb +33 -0
  14. data/lib/moxml/c14n/attribute_handler.rb +71 -0
  15. data/lib/moxml/c14n/character_encoder.rb +29 -0
  16. data/lib/moxml/c14n/data_model.rb +145 -0
  17. data/lib/moxml/c14n/exclusive.rb +188 -0
  18. data/lib/moxml/c14n/inclusive_10.rb +42 -0
  19. data/lib/moxml/c14n/inclusive_11.rb +21 -0
  20. data/lib/moxml/c14n/namespace_context.rb +68 -0
  21. data/lib/moxml/c14n/namespace_handler.rb +71 -0
  22. data/lib/moxml/c14n/node.rb +42 -0
  23. data/lib/moxml/c14n/nodes/attribute_node.rb +50 -0
  24. data/lib/moxml/c14n/nodes/comment_node.rb +29 -0
  25. data/lib/moxml/c14n/nodes/element_node.rb +62 -0
  26. data/lib/moxml/c14n/nodes/namespace_node.rb +41 -0
  27. data/lib/moxml/c14n/nodes/processing_instruction_node.rb +30 -0
  28. data/lib/moxml/c14n/nodes/root_node.rb +22 -0
  29. data/lib/moxml/c14n/nodes/text_node.rb +29 -0
  30. data/lib/moxml/c14n/nodes.rb +17 -0
  31. data/lib/moxml/c14n/processor.rb +118 -0
  32. data/lib/moxml/c14n/writer.rb +121 -0
  33. data/lib/moxml/c14n/xml_base_handler.rb +140 -0
  34. data/lib/moxml/c14n.rb +96 -0
  35. data/lib/moxml/signature/algorithms/base64_transform.rb +40 -0
  36. data/lib/moxml/signature/algorithms/canonicalization_base.rb +87 -0
  37. data/lib/moxml/signature/algorithms/digest_base.rb +51 -0
  38. data/lib/moxml/signature/algorithms/dsa_sha.rb +107 -0
  39. data/lib/moxml/signature/algorithms/ecdsa_sha.rb +125 -0
  40. data/lib/moxml/signature/algorithms/enveloped_signature_transform.rb +79 -0
  41. data/lib/moxml/signature/algorithms/exc_c14n_10.rb +23 -0
  42. data/lib/moxml/signature/algorithms/hmac_sha.rb +117 -0
  43. data/lib/moxml/signature/algorithms/inclusive_c14n_10.rb +21 -0
  44. data/lib/moxml/signature/algorithms/inclusive_c14n_11.rb +23 -0
  45. data/lib/moxml/signature/algorithms/rsa_pkcs1_sha.rb +56 -0
  46. data/lib/moxml/signature/algorithms/sha1.rb +13 -0
  47. data/lib/moxml/signature/algorithms/sha224.rb +13 -0
  48. data/lib/moxml/signature/algorithms/sha256.rb +13 -0
  49. data/lib/moxml/signature/algorithms/sha384.rb +13 -0
  50. data/lib/moxml/signature/algorithms/sha512.rb +13 -0
  51. data/lib/moxml/signature/algorithms/signature_method_base.rb +58 -0
  52. data/lib/moxml/signature/algorithms/transform_base.rb +40 -0
  53. data/lib/moxml/signature/algorithms.rb +113 -0
  54. data/lib/moxml/signature/errors.rb +99 -0
  55. data/lib/moxml/signature/key_extractor.rb +172 -0
  56. data/lib/moxml/signature/model/algorithm_method.rb +18 -0
  57. data/lib/moxml/signature/model/digest_method.rb +15 -0
  58. data/lib/moxml/signature/model/key/dsa_key_value.rb +29 -0
  59. data/lib/moxml/signature/model/key/ec_key_value.rb +22 -0
  60. data/lib/moxml/signature/model/key/rsa_key_value.rb +19 -0
  61. data/lib/moxml/signature/model/key/x509_data.rb +32 -0
  62. data/lib/moxml/signature/model/key/x509_digest.rb +19 -0
  63. data/lib/moxml/signature/model/key/x509_issuer_serial.rb +19 -0
  64. data/lib/moxml/signature/model/key_info.rb +21 -0
  65. data/lib/moxml/signature/model/key_value.rb +20 -0
  66. data/lib/moxml/signature/model/object_element.rb +18 -0
  67. data/lib/moxml/signature/model/reference.rb +22 -0
  68. data/lib/moxml/signature/model/signature.rb +21 -0
  69. data/lib/moxml/signature/model/signature_value.rb +16 -0
  70. data/lib/moxml/signature/model/signed_info.rb +20 -0
  71. data/lib/moxml/signature/model/transform.rb +16 -0
  72. data/lib/moxml/signature/model/transforms.rb +36 -0
  73. data/lib/moxml/signature/model.rb +35 -0
  74. data/lib/moxml/signature/parser.rb +270 -0
  75. data/lib/moxml/signature/reference_resolver.rb +79 -0
  76. data/lib/moxml/signature/reference_result.rb +22 -0
  77. data/lib/moxml/signature/serializer.rb +163 -0
  78. data/lib/moxml/signature/signer.rb +73 -0
  79. data/lib/moxml/signature/single_verification_result.rb +31 -0
  80. data/lib/moxml/signature/transform_pipeline.rb +106 -0
  81. data/lib/moxml/signature/verification_result.rb +27 -0
  82. data/lib/moxml/signature/verifier.rb +129 -0
  83. data/lib/moxml/signature.rb +94 -0
  84. data/lib/moxml/version.rb +1 -1
  85. data/lib/moxml.rb +2 -0
  86. data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
  87. data/reference-docs/w3c-xmldsig-core.md +400 -0
  88. data/spec/fixtures/xmldsig/keys/rsa_private.pem +28 -0
  89. data/spec/fixtures/xmldsig/keys/rsa_public.pem +9 -0
  90. data/spec/fixtures/xmldsig/keys/rsa_ref.pem +15 -0
  91. data/spec/fixtures/xmldsig/keys/rsa_ref.pub +6 -0
  92. data/spec/fixtures/xmldsig/sign2-doc.xml +6 -0
  93. data/spec/fixtures/xmldsig/sign2-result.xml +25 -0
  94. data/spec/fixtures/xmldsig/sign3-result.xml +39 -0
  95. data/spec/moxml/c14n/api_spec.rb +117 -0
  96. data/spec/moxml/c14n/c14n_spec.rb +88 -0
  97. data/spec/moxml/c14n/comments_pis_spec.rb +65 -0
  98. data/spec/moxml/c14n/inclusive10_spec.rb +60 -0
  99. data/spec/moxml/c14n/namespace_edge_cases_spec.rb +88 -0
  100. data/spec/moxml/c14n/xml_attributes_spec.rb +54 -0
  101. data/spec/moxml/signature/algorithms/base64_transform_spec.rb +26 -0
  102. data/spec/moxml/signature/algorithms/digest_base_spec.rb +72 -0
  103. data/spec/moxml/signature/algorithms/dsa_sha_spec.rb +46 -0
  104. data/spec/moxml/signature/algorithms/ecdsa_sha_spec.rb +72 -0
  105. data/spec/moxml/signature/algorithms/enveloped_signature_transform_spec.rb +40 -0
  106. data/spec/moxml/signature/algorithms/hmac_sha_spec.rb +71 -0
  107. data/spec/moxml/signature/algorithms/rsa_pkcs1_sha_spec.rb +55 -0
  108. data/spec/moxml/signature/algorithms_spec.rb +76 -0
  109. data/spec/moxml/signature/cross_verify_spec.rb +64 -0
  110. data/spec/moxml/signature/edge_cases_spec.rb +284 -0
  111. data/spec/moxml/signature/fixtures_spec.rb +80 -0
  112. data/spec/moxml/signature/key_extractor_spec.rb +96 -0
  113. data/spec/moxml/signature/model_spec.rb +54 -0
  114. data/spec/moxml/signature/round_trip_spec.rb +113 -0
  115. metadata +111 -2
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module C14n
5
+ # Builds a C14N data model from a Moxml::Node tree (or XML string).
6
+ #
7
+ # The data model exists because canonicalization needs:
8
+ # - sorted namespace and attribute axes (spec §2.3, §2.4)
9
+ # - in_node_set flags for subset canonicalization (spec §3)
10
+ # - xml:* inheritable attribute resolution
11
+ #
12
+ # Ported from canon (lutaml/canon) — adapted to build directly from
13
+ # Moxml::Node rather than via a separate Nokogiri pass. Matches
14
+ # canon's document-level node iteration (PIs and comments outside
15
+ # the document root element).
16
+ class DataModel
17
+ def self.from_xml(xml_string)
18
+ from_node(::Moxml.parse(xml_string))
19
+ end
20
+
21
+ def self.from_node(moxml_node)
22
+ return build_from_document(moxml_node) if moxml_node.is_a?(::Moxml::Document)
23
+
24
+ root = Nodes::RootNode.new
25
+ root.add_child(build_node(moxml_node))
26
+ root
27
+ end
28
+
29
+ # Build from a Moxml::Document. Matches canon's Nokogiri path:
30
+ # the root element is added first, then all other document-level
31
+ # children (PIs, comments) in document order.
32
+ def self.build_from_document(document)
33
+ root = Nodes::RootNode.new
34
+
35
+ if document.root
36
+ root.add_child(build_element_node(document.root))
37
+ # Iterate ALL document children — not just the root element.
38
+ # This captures PIs and comments that appear outside the
39
+ # document element, which are part of the canonical form.
40
+ document.children.each do |child|
41
+ next if child.equal?(document.root)
42
+ next if child.is_a?(::Moxml::Element)
43
+
44
+ built = build_node(child)
45
+ root.add_child(built) if built
46
+ end
47
+ end
48
+
49
+ root
50
+ end
51
+
52
+ def self.build_node(moxml_node)
53
+ case moxml_node
54
+ when ::Moxml::Element then build_element_node(moxml_node)
55
+ when ::Moxml::Text then build_text_node(moxml_node)
56
+ when ::Moxml::Comment then build_comment_node(moxml_node)
57
+ when ::Moxml::ProcessingInstruction then build_pi_node(moxml_node)
58
+ end
59
+ end
60
+
61
+ def self.build_element_node(moxml_element)
62
+ ns = moxml_element.namespace
63
+ element = Nodes::ElementNode.new(
64
+ name: moxml_element.name,
65
+ namespace_uri: ns&.uri,
66
+ prefix: ns&.prefix,
67
+ )
68
+
69
+ build_namespace_nodes(moxml_element, element)
70
+ build_attribute_nodes(moxml_element, element)
71
+
72
+ moxml_element.children.each do |child|
73
+ built = build_node(child)
74
+ element.add_child(built) if built
75
+ end
76
+
77
+ element
78
+ end
79
+
80
+ def self.build_namespace_nodes(moxml_element, element)
81
+ moxml_element.in_scope_namespaces.each do |ns|
82
+ element.add_namespace(
83
+ Nodes::NamespaceNode.new(prefix: ns.prefix || "", uri: ns.uri),
84
+ )
85
+ end
86
+
87
+ return if element.namespace_nodes.any? { |n| n.prefix == "xml" }
88
+
89
+ element.add_namespace(
90
+ Nodes::NamespaceNode.new(prefix: "xml", uri: XML_URI),
91
+ )
92
+ end
93
+
94
+ def self.build_attribute_nodes(moxml_element, element)
95
+ moxml_element.attributes.each do |attr|
96
+ ns = attr.namespace
97
+ element.add_attribute(
98
+ Nodes::AttributeNode.new(
99
+ name: attr.name,
100
+ value: attr.value,
101
+ namespace_uri: ns&.uri,
102
+ prefix: ns&.prefix,
103
+ ),
104
+ )
105
+ end
106
+ end
107
+
108
+ def self.build_text_node(moxml_text)
109
+ Nodes::TextNode.new(value: moxml_text.content)
110
+ end
111
+
112
+ def self.build_comment_node(moxml_comment)
113
+ Nodes::CommentNode.new(value: moxml_comment.content)
114
+ end
115
+
116
+ def self.build_pi_node(moxml_pi)
117
+ Nodes::ProcessingInstructionNode.new(
118
+ target: moxml_pi.target || moxml_pi.name,
119
+ data: moxml_pi.content || "",
120
+ )
121
+ end
122
+
123
+ def self.mark_all(node, value)
124
+ node.in_node_set = value
125
+ node.children.each { |child| mark_all(child, value) }
126
+ end
127
+
128
+ def self.mark_subset(root_node, matched)
129
+ matched.each { |node| mark_node_and_descendants(node) }
130
+ root_node.in_node_set = true
131
+ end
132
+
133
+ def self.mark_node_and_descendants(node)
134
+ node.in_node_set = true
135
+ node.children.each { |child| mark_node_and_descendants(child) }
136
+ end
137
+
138
+ private_class_method :build_from_document, :build_node,
139
+ :build_element_node, :build_namespace_nodes,
140
+ :build_attribute_nodes, :build_text_node,
141
+ :build_comment_node, :build_pi_node,
142
+ :mark_node_and_descendants
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module C14n
5
+ # Exclusive XML Canonicalization 1.0 (https://www.w3.org/TR/xml-exc-c14n/)
6
+ #
7
+ # Renders only the namespaces visibly utilized by each element's
8
+ # qualified name and attributes (plus any in the inclusive prefix
9
+ # list), as opposed to inclusive C14N which renders every in-scope
10
+ # namespace on every element.
11
+ #
12
+ # Output is UTF-8 octets with no BOM.
13
+ class Exclusive
14
+ # `node`: a Moxml::Node — element, document, text, comment, or PI.
15
+ # `with_comments:`: include comment nodes in output.
16
+ # `inclusive_namespaces`: list of prefixes to always include even if
17
+ # not visibly utilized (InclusiveNamespacesPrefixList parameter).
18
+ def canonicalize(node, with_comments: false, inclusive_namespaces: [])
19
+ writer = Writer.new
20
+ context = NamespaceContext.new
21
+ render(node, writer, context, with_comments, inclusive_namespaces)
22
+ writer.output
23
+ end
24
+
25
+ private
26
+
27
+ def render(node, writer, context, with_comments, inclusive)
28
+ case node_type(node)
29
+ when :document
30
+ node.children.each { |c| render(c, writer, context, with_comments, inclusive) }
31
+ when :element
32
+ render_element(node, writer, context, with_comments, inclusive)
33
+ when :text, :cdata
34
+ writer.text(node.content)
35
+ when :comment
36
+ writer.comment(node.content) if with_comments
37
+ when :processing_instruction
38
+ target, content = pi_target_and_content(node)
39
+ writer.processing_instruction(target, content)
40
+ end
41
+ end
42
+
43
+ def render_element(element, writer, context, with_comments, inclusive)
44
+ declared = bindings_declared_on(element)
45
+ in_scope = bindings_in_scope_on(element)
46
+ context.push(in_scope)
47
+
48
+ prefix = element_namespace_prefix(element)
49
+ local = element_name(element)
50
+
51
+ visible_prefixes = visibly_used_prefixes(element, prefix)
52
+ inclusive.each { |p| visible_prefixes.add(p) }
53
+ visible_prefixes.add("xml") if visibly_uses_xml(element)
54
+
55
+ namespaces_to_render = visible_prefixes.map do |p|
56
+ [p, context.uri_for(p)]
57
+ end.reject { |(_, uri)| uri.nil? }
58
+
59
+ writer.open_rendered_frame(element)
60
+ writer.open_tag(prefix, local)
61
+ render_namespaces(writer, element, namespaces_to_render)
62
+ render_attributes(writer, element)
63
+ writer.close_tag_open
64
+
65
+ # Switch context from in-scope (apex lookup) to declared-only so
66
+ # children build their own in-scope view by pushing their declared
67
+ # set on top of the parent's declared set.
68
+ context.pop
69
+ context.push(declared)
70
+
71
+ element.children.each { |c| render(c, writer, context, with_comments, inclusive) }
72
+
73
+ writer.close_tag(prefix, local)
74
+ writer.close_rendered_frame
75
+ context.pop
76
+ end
77
+
78
+ def render_namespaces(writer, element, namespaces)
79
+ already_rendered = writer.rendered_namespaces_for(element)
80
+ sorted = namespaces
81
+ .reject { |(prefix, _)| already_rendered.include?(prefix) }
82
+ .sort_by { |(prefix, _)| prefix.to_s }
83
+ sorted.each do |(prefix, uri)|
84
+ writer.namespace(prefix, uri)
85
+ writer.mark_rendered(element, prefix, uri)
86
+ end
87
+ end
88
+
89
+ def render_attributes(writer, element)
90
+ attrs = collect_attributes(element)
91
+ sorted = attrs.sort_by { |a| [a[:ns_uri] || "", a[:local]] }
92
+ sorted.each { |a| writer.attribute(a[:expanded], a[:value]) }
93
+ end
94
+
95
+ def collect_attributes(element)
96
+ attrs = []
97
+ element.attributes.each do |attr|
98
+ ns = attr.namespace
99
+ attrs << {
100
+ ns_uri: ns&.uri,
101
+ ns_prefix: ns&.prefix,
102
+ local: attr.name,
103
+ expanded: attr_expanded_name(attr),
104
+ value: attr.value,
105
+ }
106
+ end
107
+ attrs
108
+ end
109
+
110
+ def attr_expanded_name(attr)
111
+ ns = attr.namespace
112
+ prefix = ns&.prefix
113
+ if prefix && !prefix.empty?
114
+ "#{prefix}:#{attr.name}"
115
+ else
116
+ attr.name
117
+ end
118
+ end
119
+
120
+ def visibly_used_prefixes(element, element_prefix)
121
+ set = Set.new
122
+ # The element's qualified name visibly uses its own namespace.
123
+ # Default namespace (prefix == "" or nil) is included so it gets
124
+ # rendered on the apex element when the apex uses default ns.
125
+ if element.namespace_uri
126
+ set.add(element_prefix || "")
127
+ end
128
+ element.attributes.each do |attr|
129
+ ns = attr.namespace
130
+ prefix = ns&.prefix
131
+ set.add(prefix || "") if ns&.uri
132
+ end
133
+ set
134
+ end
135
+
136
+ def visibly_uses_xml(element)
137
+ element.attributes.any? { |a| a.namespace&.prefix == "xml" }
138
+ end
139
+
140
+ def bindings_declared_on(element)
141
+ bindings = {}
142
+ element.namespaces.each do |ns|
143
+ prefix = ns.prefix
144
+ prefix = "" if prefix.nil? || prefix == "xmlns"
145
+ bindings[prefix] = ns.uri
146
+ end
147
+ bindings
148
+ end
149
+
150
+ # In-scope namespaces (declared + inherited from ancestors).
151
+ # Used at the apex to find URIs for visibly-used prefixes whose
152
+ # declarations live on ancestors.
153
+ def bindings_in_scope_on(element)
154
+ bindings = {}
155
+ element.in_scope_namespaces.each do |ns|
156
+ prefix = ns.prefix
157
+ prefix = "" if prefix.nil? || prefix == "xmlns"
158
+ bindings[prefix] = ns.uri
159
+ end
160
+ bindings
161
+ end
162
+
163
+ def element_namespace_prefix(element)
164
+ ns = element.namespace
165
+ ns&.prefix
166
+ end
167
+
168
+ def element_name(element)
169
+ element.name
170
+ end
171
+
172
+ def pi_target_and_content(node)
173
+ [node.name, node.text]
174
+ end
175
+
176
+ def node_type(node)
177
+ return :document if node.is_a?(::Moxml::Document)
178
+ return :element if node.is_a?(::Moxml::Element)
179
+ return :text if node.is_a?(::Moxml::Text)
180
+ return :cdata if node.is_a?(::Moxml::Cdata)
181
+ return :comment if node.is_a?(::Moxml::Comment)
182
+ return :processing_instruction if node.is_a?(::Moxml::ProcessingInstruction)
183
+
184
+ :unknown
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module C14n
5
+ # Inclusive Canonical XML 1.0 (https://www.w3.org/TR/xml-c14n/)
6
+ #
7
+ # Unlike exclusive C14N, inclusive C14N "attracts" ancestor context:
8
+ # at the apex element, ALL in-scope namespaces are rendered, including
9
+ # those inherited from ancestors outside the canonicalization subset.
10
+ #
11
+ # xml:* inheritable attributes (xml:lang, xml:space) are also inherited
12
+ # from the nearest ancestor in which they are declared.
13
+ #
14
+ # Implementation: delegates to the canon-derived Processor pipeline
15
+ # (DataModel + NamespaceHandler + AttributeHandler + XmlBaseHandler).
16
+ class Inclusive10
17
+ # rubocop:disable Lint/UnusedMethodArgument -- signature must match Exclusive
18
+ def canonicalize(node_or_xml, with_comments: false, inclusive_namespaces: [])
19
+ # rubocop:enable Lint/UnusedMethodArgument
20
+ root = coerce_to_data_model(node_or_xml)
21
+ Processor.new(with_comments: with_comments).process(root)
22
+ end
23
+
24
+ private
25
+
26
+ def coerce_to_data_model(node_or_xml)
27
+ return node_or_xml if node_or_xml.is_a?(Nodes::RootNode)
28
+
29
+ case node_or_xml
30
+ when ::Moxml::Document, ::Moxml::Node
31
+ DataModel.from_node(node_or_xml)
32
+ when String
33
+ DataModel.from_xml(node_or_xml)
34
+ else
35
+ raise ArgumentError,
36
+ "Inclusive10#canonicalize expects a Moxml::Node, " \
37
+ "Moxml::Document, or XML String; got #{node_or_xml.class}"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module C14n
5
+ # Canonical XML 1.1 (https://www.w3.org/TR/xml-c14n11/).
6
+ #
7
+ # In this implementation, Inclusive11 is a thin alias of Inclusive10.
8
+ # Both delegate to the canon-derived Processor. The W3C 1.1 additions
9
+ # (notations, entity references in DTD internal subset, XML 1.1 line
10
+ # ending) are rarely encountered in modern XML signature practice.
11
+ class Inclusive11
12
+ def canonicalize(node_or_xml, with_comments: false, inclusive_namespaces: [])
13
+ Inclusive10.new.canonicalize(
14
+ node_or_xml,
15
+ with_comments: with_comments,
16
+ inclusive_namespaces: inclusive_namespaces,
17
+ )
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module C14n
5
+ # Tracks in-scope namespace URIs along the ancestor chain of the
6
+ # element currently being canonicalized. The canonicalizer pushes
7
+ # when entering an element and pops when leaving.
8
+ #
9
+ # `xml` prefix is always bound to the XML namespace per XML 1.0 spec.
10
+ class NamespaceContext
11
+ DEFAULT_XML_BINDING = { "xml" => C14n::XML_URI }.freeze
12
+
13
+ attr_reader :bindings
14
+
15
+ def initialize(initial = {})
16
+ @stack = []
17
+ @bindings = DEFAULT_XML_BINDING.dup
18
+ merge(initial)
19
+ end
20
+
21
+ def push(bindings_to_apply)
22
+ applied = {}
23
+ bindings_to_apply.each do |prefix, uri|
24
+ prev = @bindings[prefix]
25
+ applied[prefix] = prev
26
+ @bindings[prefix] = uri
27
+ end
28
+ @stack.push(applied)
29
+ self
30
+ end
31
+
32
+ def pop
33
+ applied = @stack.pop
34
+ applied.each do |prefix, prev|
35
+ if prev.nil?
36
+ @bindings.delete(prefix)
37
+ @bindings[prefix] = C14n::XML_URI if prefix == "xml"
38
+ else
39
+ @bindings[prefix] = prev
40
+ end
41
+ end
42
+ self
43
+ end
44
+
45
+ def uri_for(prefix)
46
+ @bindings[prefix]
47
+ end
48
+
49
+ def binding_for(prefix)
50
+ @bindings[prefix]
51
+ end
52
+
53
+ def include?(prefix)
54
+ @bindings.key?(prefix)
55
+ end
56
+
57
+ def to_h
58
+ @bindings.dup
59
+ end
60
+
61
+ private
62
+
63
+ def merge(hash)
64
+ hash.each { |prefix, uri| @bindings[prefix] = uri }
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module C14n
5
+ # Namespace axis handler for inclusive C14N.
6
+ # Implements W3C C14N 1.0 §2.3 / 1.1 §2.3 namespace rendering rules
7
+ # for the document subset (full and partial).
8
+ class NamespaceHandler
9
+ attr_reader :encoder
10
+
11
+ def initialize(encoder)
12
+ @encoder = encoder
13
+ end
14
+
15
+ def process_namespaces(element, output, parent_element = nil)
16
+ return unless element.in_node_set?
17
+
18
+ namespaces = element.sorted_namespace_nodes.select(&:in_node_set?)
19
+
20
+ if should_emit_empty_default_namespace?(element, namespaces, parent_element)
21
+ output << ' xmlns=""'
22
+ end
23
+
24
+ namespaces.each do |ns|
25
+ next if should_skip_namespace?(ns, parent_element)
26
+
27
+ output << " "
28
+ output << (ns.default_namespace? ? "xmlns" : "xmlns:#{ns.prefix}")
29
+ output << '="'
30
+ output << encoder.encode_attribute(ns.uri)
31
+ output << '"'
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ # Emit xmlns="" if the element's nearest in-set ancestor had a non-empty
38
+ # default namespace and this element does not.
39
+ def should_emit_empty_default_namespace?(element, namespaces, parent_element)
40
+ return false unless element.in_node_set?
41
+ return false if namespaces.first&.default_namespace?
42
+ return false unless parent_element
43
+
44
+ parent_default_ns = parent_element.namespace_nodes.find do |ns|
45
+ ns.default_namespace? && ns.in_node_set?
46
+ end
47
+
48
+ parent_default_ns && !parent_default_ns.uri.empty?
49
+ end
50
+
51
+ def should_skip_namespace?(namespace, parent_element)
52
+ # The xml namespace is implicit, never rendered.
53
+ return true if namespace.xml_namespace?
54
+ # Skip namespaces already declared (with same URI) by an ancestor.
55
+ return true if namespace_declared_by_ancestor?(namespace, parent_element)
56
+
57
+ false
58
+ end
59
+
60
+ def namespace_declared_by_ancestor?(namespace, parent_element)
61
+ return false unless parent_element
62
+
63
+ parent_ns = parent_element.namespace_nodes.find do |candidate|
64
+ candidate.prefix == namespace.prefix && candidate.in_node_set?
65
+ end
66
+
67
+ parent_ns && parent_ns.uri == namespace.uri
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module C14n
5
+ # Base class for all C14N data-model nodes.
6
+ # Ported from canon ( lutaml/canon ) — keeps the mature node-set
7
+ # semantics needed for subset canonicalization (spec §3).
8
+ class Node
9
+ attr_reader :parent, :children
10
+
11
+ def initialize
12
+ @parent = nil
13
+ @children = []
14
+ @in_node_set = true
15
+ end
16
+
17
+ def add_child(child)
18
+ child.parent = self
19
+ @children << child
20
+ end
21
+
22
+ def in_node_set?
23
+ @in_node_set
24
+ end
25
+
26
+ def in_node_set=(value)
27
+ @in_node_set = value
28
+ end
29
+
30
+ # Return the text content of this node and all descendants.
31
+ # ElementNode concatenates children's text_content; other nodes
32
+ # (TextNode, CommentNode, etc.) return their value.
33
+ def text_content
34
+ children.map(&:text_content).join
35
+ end
36
+
37
+ protected
38
+
39
+ attr_writer :parent
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module C14n
5
+ module Nodes
6
+ # Attribute node.
7
+ class AttributeNode < Node
8
+ attr_reader :name, :value, :namespace_uri, :prefix
9
+
10
+ def initialize(name:, value:, namespace_uri: nil, prefix: nil)
11
+ super()
12
+ @name = name
13
+ @value = value
14
+ @namespace_uri = namespace_uri
15
+ @prefix = prefix
16
+ end
17
+
18
+ def node_type
19
+ :attribute
20
+ end
21
+
22
+ def local_name
23
+ name
24
+ end
25
+
26
+ def qname
27
+ prefix.nil? || prefix.empty? ? name : "#{prefix}:#{name}"
28
+ end
29
+
30
+ # xml:* attributes (lang, space, base, id).
31
+ def xml_attribute?
32
+ namespace_uri == Moxml::C14n::XML_URI
33
+ end
34
+
35
+ # xml:lang and xml:space are inheritable per C14N 1.1 §2.4.
36
+ def simple_inheritable?
37
+ xml_attribute? && %w[lang space].include?(name)
38
+ end
39
+
40
+ def xml_id?
41
+ xml_attribute? && name == "id"
42
+ end
43
+
44
+ def xml_base?
45
+ xml_attribute? && name == "base"
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module C14n
5
+ module Nodes
6
+ # Comment node.
7
+ class CommentNode < Node
8
+ attr_reader :value
9
+
10
+ def initialize(value:)
11
+ super()
12
+ @value = value
13
+ end
14
+
15
+ def name
16
+ "comment"
17
+ end
18
+
19
+ def node_type
20
+ :comment
21
+ end
22
+
23
+ def text_content
24
+ @value
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end