Module: Canon::Xml::DigestGate

Defined in:
lib/canon/xml/digest_gate.rb

Overview

Document-level Merkle-digest equivalence gate over leptris' content-defined subtree digests (libleptris #869, leptris-ruby Node#digest since 1.9.144).

Equal root digests prove the root subtrees carry identical content (whitespace-only nodes dropped), and every canon match behavior is a relaxation of content identity — so equal digests imply equivalence under any option combination EXCEPT the two restrictions that compare things the digest deliberately ignores (attribute order, comments). The caller excludes those; this module answers the digest question only.

A digest miss costs two readonly engine parses (no field materialization, no canon tree) — a fraction of the comparison it precedes. Any parse failure answers false and lets the full pipeline surface the error.

Class Method Summary collapse

Class Method Details

.available?Boolean

Returns:

  • (Boolean)


46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/canon/xml/digest_gate.rb', line 46

def available?
  return false if RUBY_ENGINE == "opal"
  return false unless Canon::XmlBackend.moxml? &&
    Canon::XmlParsing.moxml_adapter_name == :leptris

  # moxml Node#digest (moxml#173) wraps leptris Node#digest;
  # feature-detect on a throwaway document.
  doc = Canon::XmlParsing.moxml_context.parse("<r/>", readonly: true,
                                                      strict: false)
  root = doc.root
  digestable = !root.digest(drop_ws_text: true).nil?
  doc.free
  digestable
rescue StandardError
  false
end

.certify(xml1, xml2) ⇒ Object

The shared fingerprint ([root digest, doc-level skeleton]) when both documents digest identically AND both parse clean of recover errors; nil otherwise. The verbose lane certifies before skipping the pipeline: error-bearing documents decline so the pipeline can produce the identical verbose report (parse-error banner included) it always has.



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/canon/xml/digest_gate.rb', line 102

def certify(xml1, xml2)
  return nil unless xml1.is_a?(String) && xml2.is_a?(String)

  begin
    return nil if xml1.include?("xml:space") || xml2.include?("xml:space")
  rescue Encoding::CompatibilityError
    return nil
  end

  context = Canon::XmlParsing.moxml_context
  begin
    left = fingerprint(context, xml1, clean: true)
    right = fingerprint(context, xml2, clean: true)
    return nil if left.nil? || left != right

    left
  rescue StandardError
    nil
  end
end

.equal?(xml1, xml2) ⇒ Boolean

True when both documents' root subtrees digest identically AND their document-level skeletons (prolog/epilog comments and PIs — the only doc-level nodes canon compares; doctype and whitespace-only text are excluded) serialize identically. Anything else — parse failure, missing root, digest unavailable — is false: the caller falls through to the full pipeline, which surfaces the real parse errors.

Returns:

  • (Boolean)


70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/canon/xml/digest_gate.rb', line 70

def equal?(xml1, xml2)
  return false unless xml1.is_a?(String) && xml2.is_a?(String)

  begin
    # xml:space documents carry attribute-scoped whitespace canon
    # makes normative and the digest cannot see — decline them.
    # The scan also declines non-ASCII-compatible encodings
    # (include? raises) — those are the full pipeline's to
    # normalize.
    return false if xml1.include?("xml:space") || xml2.include?("xml:space")
  rescue Encoding::CompatibilityError
    return false
  end

  context = Canon::XmlParsing.moxml_context
  begin
    left = fingerprint(context, xml1)
    right = fingerprint(context, xml2)
    !left.nil? && left == right
  rescue StandardError
    # Any engine-level surprise (encodings, broken input) is the
    # full pipeline's domain — it normalizes and surfaces errors.
    false
  end
end

.fingerprint(context, xml, clean: false) ⇒ Object

[root digest, doc-level skeleton] or nil when unparseable. With clean: a document carrying recover errors also answers nil — only certify uses that; the boolean lane keeps the historical verdict for recovered-equal pairs.



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/canon/xml/digest_gate.rb', line 127

def fingerprint(context, xml, clean: false)
  doc = context.parse(xml, readonly: true, strict: false)
  root = doc.root
  return nil unless root

  if clean
    return nil if doc.parse_errors.any?
    return nil if recover_diags?(doc)
  end

  skeleton = doc.children.filter_map do |child|
    next if Canon::XmlParsing.same_engine_node?(child, root)

    case child
    when Moxml::Comment, Moxml::ProcessingInstruction then child.to_s
    when Moxml::Text then child.content.strip.empty? ? nil : child.to_s
    end
  end
  digest = root.digest(drop_ws_text: true)
  # nil digest means the adapter has no Merkle support — never
  # treat two nils as equal (that would false-positive under
  # Opal / non-leptris backends where every tree digests nil).
  return nil if digest.nil?

  [digest, skeleton]
ensure
  doc&.free
end

.recover_diags?(doc) ⇒ Boolean

Returns:

  • (Boolean)


38
39
40
41
42
43
44
# File 'lib/canon/xml/digest_gate.rb', line 38

def recover_diags?(doc)
  return false unless recover_diags_available?

  native = doc.native
  native = native.c_ptr if native.respond_to?(:c_ptr)
  ::Leptris::XML::FFI.leptris_document_parse_diag_count(native).positive?
end

.recover_diags_available?Boolean

libleptris 1.9.205+ records duplicate-attribute recover events on the parsed document (#1200): read through the FFI diag counter. Evaluated lazily at call time — a load-time constant would freeze before leptris loads (defined? never resolves pending autoloads). When the adapter is not leptris (or the binding predates the surface), the verbose lane falls back to the SAX probe for its error scan.

Returns:

  • (Boolean)


31
32
33
34
35
36
# File 'lib/canon/xml/digest_gate.rb', line 31

def recover_diags_available?
  defined?(::Leptris::XML::FFI) &&
    ::Leptris::XML::FFI.respond_to?(:leptris_document_parse_diag_count)
rescue StandardError
  false
end