Class: Canon::Xml::C14n

Inherits:
Object
  • Object
show all
Defined in:
lib/canon/xml/c14n.rb

Overview

XML Canonicalization 1.1 implementation Per W3C Recommendation: https://www.w3.org/TR/xml-c14n11/

Constant Summary collapse

RELATIVE_NS_DECL =

Canon::Error parity with the Ruby lane, whose parse rejects relative namespace URIs. A declaration-shaped scan; a relative-URI-shaped string inside CDATA is the only false positive this can produce.

/xmlns(?::[\w.-]+)?="([^"]*)"/
URI_SCHEME =
%r{\A[a-zA-Z][a-zA-Z0-9+.-]*:}

Class Method Summary collapse

Class Method Details

.canonicalize(xml, with_comments: false) ⇒ String

Canonicalize an XML document

Parameters:

  • xml (String)

    XML document as string

  • with_comments (Boolean) (defaults to: false)

    Include comments in canonical form

Returns:

  • (String)

    Canonical form in UTF-8



12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/canon/xml/c14n.rb', line 12

def self.canonicalize(xml, with_comments: false)
  if (native = native_canonicalize(xml, with_comments))
    return native
  end

  # Build XPath data model
  root_node = DataModel.from_xml(xml)

  # Process to canonical form
  processor = Processor.new(with_comments: with_comments)
  processor.process(root_node)
end

.canonicalize_subset(xml, xpath, with_comments: false) ⇒ String

Canonicalize a document subset selected by XPath expression.

Implements W3C C14N 1.1 subset canonicalization:

  1. Evaluates XPath against the document tree
  2. Marks matched nodes as the node-set
  3. Renders canonical form for only the selected nodes, with namespace and attribute inheritance from excluded ancestors

Parameters:

  • xml (String)

    XML document as string

  • xpath (String)

    XPath expression for subset selection

  • with_comments (Boolean) (defaults to: false)

    Include comments in canonical form

Returns:

  • (String)

    Canonical form in UTF-8



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/canon/xml/c14n.rb', line 108

def self.canonicalize_subset(xml, xpath, with_comments: false)
  root_node = DataModel.from_xml(xml)

  # Mark all nodes as NOT in the node-set initially
  mark_all_nodes(root_node, false)

  # Evaluate XPath and mark matched nodes
  matched = XPathEngine.evaluate(root_node, xpath)

  # If XPath matches root or is empty, fall back to full canonicalization
  if matched.empty?
    mark_all_nodes(root_node, true)
  else
    # Mark matched nodes and their ancestors/descendants
    mark_subset(root_node, matched)
  end

  # Process to canonical form
  processor = Processor.new(with_comments: with_comments)
  processor.process(root_node)
end

.native_c14n_available?Boolean

Returns:

  • (Boolean)


85
86
87
88
89
90
# File 'lib/canon/xml/c14n.rb', line 85

def self.native_c14n_available?
  return @native_c14n_available unless @native_c14n_available.nil?

  @native_c14n_available = defined?(::Leptris::XML::FFI::C14N_1_1) &&
    ::Leptris::XML::FFI.constants.include?(:C14N_MODE_CANONICAL)
end

.native_canonicalize(xml, with_comments) ⇒ Object

leptris' C-side C14N 1.1 — 23x faster than the Ruby processor through canon's own API (1MB document) and the DEFAULT lane since libleptris 1.9.178 (gem 1.9.178.0): the leptris#1117 families are closed by the native parse-policy knob (noblanks: true — same compact bytes as the Ruby lane's parse) and by the canonicalizer itself (whitespace-only PI data dropped, document-level "\n" separators inserted). The one residual byte difference vs the Ruby lane is intentional and a correctness improvement: document-level PIs now serialize in document order (REC-xml-c14n 2.1) instead of the Ruby lane's non-conformant root-first reorder. Relative namespace URIs raise exactly as the Ruby lane does. Comments mode keeps the Ruby path regardless (the native seam exposes no with-comments form). CANON_C14N_BACKEND=ruby forces the stdlib lane.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/canon/xml/c14n.rb', line 40

def self.native_canonicalize(xml, with_comments)
  return nil if with_comments
  return nil if RUBY_ENGINE == "opal"
  return nil if ENV["CANON_C14N_BACKEND"].to_s.casecmp("ruby").zero?
  return nil unless Canon::XmlBackend.moxml? &&
    Canon::XmlParsing.moxml_adapter_name == :leptris
  return nil unless native_c14n_available?

  validate_relative_namespaces!(xml)

  doc = Canon::XmlParsing.moxml_context.parse(xml,
                                              readonly: true,
                                              strict: false,
                                              noblanks: true)
  begin
    doc.native.c14n(::Leptris::XML::FFI::C14N_1_1, nil,
                    mode: ::Leptris::XML::FFI::C14N_MODE_CANONICAL)
  ensure
    doc.free
  end
rescue StandardError
  # Malformed inputs are the Ruby path's domain (recovery parse
  # + parse_errors surfacing), not the native lane's.
  nil
end

.reset_native_probe!Object



92
93
94
# File 'lib/canon/xml/c14n.rb', line 92

def self.reset_native_probe!
  @native_c14n_available = nil
end

.validate_relative_namespaces!(xml) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
# File 'lib/canon/xml/c14n.rb', line 73

def self.validate_relative_namespaces!(xml)
  return unless xml.is_a?(String)

  xml.scan(RELATIVE_NS_DECL) do |(uri)|
    next if uri.nil? || uri.empty?

    unless uri.match?(URI_SCHEME)
      raise Canon::Error, "Relative namespace URI not allowed: #{uri}"
    end
  end
end