moxml 0.5.117 → 0.5.118

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e55a5915424bfa9e3977ed2d9d5207502c42c48eb8b1f48de5406017d8e7cfa5
4
- data.tar.gz: '01315569d92e95d97e8d466a20da3e26be9db15d876c1062e45d739179399273'
3
+ metadata.gz: 60a08183c240700f3cde7675fb3428b47340f0dba11e723309c5afb09aa988bb
4
+ data.tar.gz: 42e790020feb09861560717610f16f6e8996fb7435689eae1f989a05cb90ab0a
5
5
  SHA512:
6
- metadata.gz: 5138b1d6a1a60d5b0a6f1a6517599a6b6d05b34cf14266309a0cc9cc3082e332cb62c8cd76477363853beb7b1ae8d7ef6b3833bf17e07ee950ce7f71a68e4bdb
7
- data.tar.gz: 9b6ba4c742f717ad843eb1bdd08d58957eea374b33988b11dac6d9ece9e8a199739198057c09621c97af94462b329049916f3f0283612941adca7ac199bb73ba
6
+ metadata.gz: 4accde9025cb43cbe09661c68be8d31047f39339f035f2a80d895e3e8b03a786b7b0d492880a39d0f2f4b57b0209002e62cc79c7c71b3c4373091ceaac4481ee
7
+ data.tar.gz: 203fb7db9d8e46a7a028218dda254367cc0cb1c8ac096b516102937874ba0f14b2bbe1f06cfcb413334567722d31f94f4e00fe2850e2c3a20f1334a3bfce5f70
data/Gemfile CHANGED
@@ -9,7 +9,7 @@ gemspec
9
9
  gem "benchmark"
10
10
  gem "benchmark-ips"
11
11
  gem "get_process_mem"
12
- gem "leptris", "~> 1.9.280"
12
+ gem "leptris", "~> 1.9.320"
13
13
  gem "libxml-ruby", ">= 5.0"
14
14
  gem "nokogiri", "~> 1.18"
15
15
  gem "openssl", "~> 3.0"
data/README.adoc CHANGED
@@ -119,7 +119,7 @@ carries the plan-ABI surface, the 1.9.290 XSLT-params and
119
119
  bulk-append faces, and the 1.9.292 engine deep-copy root install).
120
120
  |===
121
121
 
122
- NOTE: moxml's dev Gemfile pins `leptris ~> 1.9.174`, `nokogiri ~> 1.18`,
122
+ NOTE: moxml's dev Gemfile pins `leptris ~> 1.9.320` (the tested pair; the gemspec itself has zero runtime dependencies). Consumers of the leptris adapter should run leptris 1.9.320+ — earlier engines carry silent wrong-answer plan-path defects (moxml#344); moxml gates degrade to the always-correct slower paths below it.
123
123
  `ox ~> 2.14`, `libxml-ruby >= 5.0`, and the vendored oga fork — the CI
124
124
  matrix tests exactly those floors.
125
125
 
@@ -0,0 +1,39 @@
1
+ # Canonicalization (C14N)
2
+
3
+ Canonicalization is the load-bearing primitive for XML signature and a
4
+ top-level moxml feature, sibling to `Moxml::XPath`, `Moxml::Builder`,
5
+ and `Moxml::SAX`. Two documents that differ only in surface
6
+ representation (whitespace, attribute order, namespace prefix choice)
7
+ produce identical canonical bytes.
8
+
9
+ ## Pages
10
+
11
+ - [Algorithms](algorithms.md) — Inclusive C14N 1.0/1.1 (canon-port) and
12
+ Exclusive C14N 1.0 (moxml-native), the data model, output invariants,
13
+ and adapter delegation.
14
+ - [API](api.md) — the full public surface: canonicalize, exclusive,
15
+ subset, digests, and the comparison helper.
16
+ - [Examples](examples.md) — patterns: whole-document, subset,
17
+ cross-adapter verification, digests.
18
+
19
+ ## Quick start
20
+
21
+ ```ruby
22
+ # Canonical octets (inclusive C14N 1.0)
23
+ Moxml::C14n.canonicalize(node_or_xml)
24
+
25
+ # Exclusive C14N 1.0 (signatures over moved subdocuments)
26
+ Moxml::C14n.canonicalize_exclusive(node_or_xml,
27
+ inclusive_namespaces: ["foo"])
28
+
29
+ # Node-set subset canonicalization (same-document references)
30
+ Moxml::C14n.canonicalize_subset(node_or_xml, "//signed")
31
+
32
+ # Canonical comparison (testing, regression, signature debugging)
33
+ Moxml::C14n.same_canonical_form?(left, right)
34
+ ```
35
+
36
+ `node_or_xml` accepts a `Moxml::Node`, `Moxml::Document`, or XML
37
+ `String`. Engines without a native canonicalizer take the pure-Ruby
38
+ path — results are byte-identical across adapters, which is what keeps
39
+ cross-adapter signature verification sound.
@@ -0,0 +1,65 @@
1
+ # C14N algorithms
2
+
3
+ ## Inclusive C14N 1.0 / 1.1
4
+
5
+ - W3C: <https://www.w3.org/TR/xml-c14n/>, <https://www.w3.org/TR/xml-c14n11/>
6
+ - Implementation: **ported from `lutaml/canon`** (~1,200 lines)
7
+ - Files: `lib/moxml/c14n/inclusive_10.rb`, `inclusive_11.rb`,
8
+ `data_model.rb`, `processor.rb`, `namespace_handler.rb`,
9
+ `attribute_handler.rb`, `xml_base_handler.rb`, `character_encoder.rb`,
10
+ `node.rb`, `nodes/*.rb`
11
+ - "Attracts" ancestor context: at the apex, every in-scope namespace is
12
+ rendered, including those inherited from outside the canonicalization
13
+ subset.
14
+
15
+ ## Exclusive C14N 1.0
16
+
17
+ - W3C: <https://www.w3.org/TR/xml-exc-c14n/>
18
+ - Implementation: **moxml-native** (canon does not implement exclusive)
19
+ - Files: `lib/moxml/c14n/exclusive.rb`, `writer.rb`, `namespace_context.rb`
20
+ - "Repels" ancestor context: only namespaces visibly used by the apex
21
+ element's qualified name or attributes are rendered. Keeps signatures
22
+ valid when subdocuments move between XML contexts (e.g., into a SOAP
23
+ envelope).
24
+
25
+ ## Adapter delegation
26
+
27
+ Adapters whose engines ship a native canonicalizer delegate through a
28
+ lazy byte-safety probe — the pure-Ruby path remains the fallback and
29
+ the arbiter: all routes produce byte-identical output (pinned by
30
+ `spec/moxml/c14n/adapter_delegation_spec.rb`).
31
+
32
+ - **leptris**: engine-delegated inclusive C14N — an order of magnitude
33
+ faster than the Ruby path on large documents.
34
+ - **nokogiri**: libxml2 native canonicalization.
35
+
36
+ ## Data model
37
+
38
+ The canon-derived inclusive engines walk an intermediate data model
39
+ (`Moxml::C14n::Nodes::*`) rather than the live `Moxml::Node` tree,
40
+ because canonicalization needs:
41
+
42
+ - **Node-set membership flags** for subset canonicalization (spec §3).
43
+ - **Sorted namespace and attribute axes** per spec §2.3 / §2.4.
44
+ - **xml:base fixup** per RFC 3986 with C14N 1.1 modifications.
45
+ - **xml:\* inheritable attribute resolution** (xml:lang, xml:space)
46
+ from omitted ancestors.
47
+
48
+ The data model is built from `Moxml::Node` via `Moxml::C14n::DataModel`.
49
+
50
+ ## Output invariants
51
+
52
+ - UTF-8 encoded, no BOM
53
+ - NFC characters preserved
54
+ - Document-order traversal
55
+ - Entity references: `&` → `&amp;`, `<` → `&lt;`, `>` → `&gt;`
56
+ - Attribute values: also escape `"`, tab, LF, CR
57
+ - Empty elements expanded: `<foo/>` → `<foo></foo>`
58
+ - Line endings normalized to LF (the parser already does this)
59
+
60
+ ## Cross-verification
61
+
62
+ The libxmlsec1-produced fixtures in `spec/fixtures/xmldsig/` verify
63
+ byte-exact against our C14N output — the implementation matches
64
+ libxml2's C-based canonicalization for the cases the Ruby reference
65
+ exercises.
data/docs/c14n/api.md ADDED
@@ -0,0 +1,62 @@
1
+ # C14N API
2
+
3
+ `node_or_xml` accepts a `Moxml::Node`, `Moxml::Document`, or XML
4
+ `String` on every entry point.
5
+
6
+ ## Convenience methods
7
+
8
+ ```ruby
9
+ # Inclusive C14N 1.0
10
+ Moxml::C14n.canonicalize(node_or_xml, with_comments: false)
11
+
12
+ # Exclusive C14N 1.0
13
+ Moxml::C14n.canonicalize_exclusive(
14
+ node_or_xml,
15
+ with_comments: false,
16
+ inclusive_namespaces: [], # InclusiveNamespacesPrefixList parameter
17
+ )
18
+
19
+ # Inclusive C14N 1.1
20
+ Moxml::C14n.canonicalize_inclusive11(node_or_xml, with_comments: false)
21
+
22
+ # Node-set subset canonicalization: only nodes matched by the XPath
23
+ # render (position-path matching — see docs on enveloped signatures)
24
+ Moxml::C14n.canonicalize_subset(node_or_xml, "//signed",
25
+ with_comments: false)
26
+
27
+ # Canonical comparison: do two inputs share one canonical form?
28
+ # (inclusive C14N 1.0, comments dropped, on both sides)
29
+ Moxml::C14n.same_canonical_form?(left, right) # nodes or raw strings
30
+ ```
31
+
32
+ All canonicalize methods return canonical UTF-8 octet strings.
33
+
34
+ ## Direct engine access
35
+
36
+ Used by the signature algorithms:
37
+
38
+ ```ruby
39
+ Moxml::C14n::Inclusive10.new.canonicalize(node, with_comments:,
40
+ inclusive_namespaces:)
41
+ Moxml::C14n::Inclusive11.new.canonicalize(node, with_comments:,
42
+ inclusive_namespaces:)
43
+ Moxml::C14n::Exclusive.new.canonicalize(node, with_comments:,
44
+ inclusive_namespaces:)
45
+ ```
46
+
47
+ ## Digests
48
+
49
+ `Node#digest` (engine-delegated where available) computes the canonical
50
+ digest of a subtree:
51
+
52
+ ```ruby
53
+ node.digest # SHA-256 over inclusive C14N 1.0
54
+ node.digest(drop_ws_text: true)
55
+ ```
56
+
57
+ ## Backward-compat escape helpers
58
+
59
+ ```ruby
60
+ Moxml::C14n.escape_text("a & b < c > d")
61
+ Moxml::C14n.escape_attribute(%(a"b\tc\nd))
62
+ ```
@@ -0,0 +1,47 @@
1
+ # C14N examples
2
+
3
+ ## Whole-document canonicalization
4
+
5
+ ```ruby
6
+ doc = Moxml.new.parse(xml)
7
+ Moxml::C14n.canonicalize(doc)
8
+ # => "<?xml version=\"1.0\"?>\n<a b=\"1\">text</a>\n"
9
+ ```
10
+
11
+ ## Subset canonicalization (enveloped signature shape)
12
+
13
+ Only nodes matched by the XPath expression render — the same-document
14
+ reference pattern:
15
+
16
+ ```ruby
17
+ Moxml::C14n.canonicalize_subset(doc, "//ds:SignedInfo",
18
+ { "ds" => "http://www.w3.org/2000/09/xmldsig#" })
19
+ ```
20
+
21
+ ## Canonical comparison
22
+
23
+ Testing two documents for semantic equality — whitespace, attribute
24
+ order, and prefix choice are eliminated before comparison:
25
+
26
+ ```ruby
27
+ Moxml::C14n.same_canonical_form?(v1_document, v2_document) # => true
28
+ Moxml::C14n.same_canonical_form?(fixture_xml, output_string) # => true
29
+ ```
30
+
31
+ ## Cross-adapter verification
32
+
33
+ A signature produced with one adapter must verify with another — the
34
+ canonical bytes are byte-identical across adapters:
35
+
36
+ ```ruby
37
+ %i[leptris nokogiri rexml ox libxml].map do |adapter|
38
+ Moxml.new(adapter).parse(xml).then { |d| Moxml::C14n.canonicalize(d) }
39
+ end.uniq.size # => 1
40
+ ```
41
+
42
+ ## Digest of a subtree
43
+
44
+ ```ruby
45
+ signed_element.digest
46
+ # engine-delegated SHA-256 over the subtree's canonical form
47
+ ```
@@ -1,4 +1,4 @@
1
- # Canonicalization (C14N)
1
+ # Canonicalization (C14N) in XML signature
2
2
 
3
3
  Canonicalization is the load-bearing primitive for XML signature.
4
4
  Two documents that differ only in surface representation (whitespace,
@@ -6,7 +6,9 @@ attribute order, namespace prefix choice) must produce identical
6
6
  canonical bytes — otherwise signatures won't verify.
7
7
 
8
8
  `Moxml::C14n` is a top-level moxml feature, sibling to `Moxml::XPath`,
9
- `Moxml::Builder`, and `Moxml::SAX`.
9
+ `Moxml::Builder`, and `Moxml::SAX`. The full C14N documentation tree
10
+ lives at [docs/c14n/](../c14n/README.md) — this page covers the
11
+ signature-specific view.
10
12
 
11
13
  ## Algorithms
12
14
 
@@ -97,10 +99,10 @@ and it's much faster than pure Ruby. But:
97
99
  3. **Canon already had the mature pure-Ruby implementation.** Porting it
98
100
  was less work than building a hybrid adapter-delegated system.
99
101
 
100
- **Future work:** add an optional adapter-level C14N delegation. The
101
- Nokogiri adapter could expose `canonicalize(node, ...)` and the
102
- `Moxml::C14n` top-level method would delegate when available. This is
103
- documented in TODO.complete/19.
102
+ **Shipped:** optional adapter-level C14N delegation — the Nokogiri
103
+ (libxml2) and leptris (engine) adapters expose native canonicalization
104
+ behind a lazy byte-safety probe, byte-identical with the pure-Ruby
105
+ path (see [docs/c14n/algorithms.md](../c14n/algorithms.md#adapter-delegation)).
104
106
 
105
107
  ## Cross-verification
106
108
 
@@ -725,7 +725,16 @@ module Moxml
725
725
  # exactly the plan's shape, with no wrapper minting at all.
726
726
  # Marker-bearing documents stay on the generic path (the bulk
727
727
  # stream has no marker split).
728
- NATIVE_PLAN_ROWS = true
728
+ # moxml#344: engines 1.9.314-1.9.319 mis-match on the plan
729
+ # path — child-row exact-URI ns_uri pointed into build
730
+ # buffers (rows vanish after GC, leptris#1585) and plain
731
+ # attribute rows did not leniently match namespace-qualified
732
+ # wire attributes (nil captures, leptris#1586). Both are
733
+ # SILENT wrong answers, so the gate names the fixed engine:
734
+ # below 1.9.320.0 the plan faces degrade to the
735
+ # always-correct generic wrapper walk.
736
+ NATIVE_PLAN_ROWS =
737
+ Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.320.0")
729
738
 
730
739
  # leptris-ruby#272 (binding 1.9.201.1): Native.plan_structs —
731
740
  # the C struct executor behind Moxml::StructPlan. The floor
@@ -735,9 +744,12 @@ module Moxml
735
744
  # below the fix. The probe guards the version gate: a
736
745
  # numerically newer lockstep can publish without the face
737
746
  # (1.9.199.0 shipped without it), and the gate alone would
738
- # NoMethodError.
747
+ # NoMethodError. Floor also carries moxml#344: 1.9.314-1.9.319
748
+ # mis-match on the plan path (leptris#1585/#1586, silent nil
749
+ # captures) — the struct executor degrades to the Ruby Plan
750
+ # fallback below the fix, same as the varargs-mint floor.
739
751
  NATIVE_PLAN_STRUCTS =
740
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.201.1") &&
752
+ Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.320.0") &&
741
753
  (!defined?(::Leptris::XML::Native) ||
742
754
  ::Leptris::XML::Native.respond_to?(:plan_structs))
743
755
 
@@ -781,7 +793,7 @@ module Moxml
781
793
  end
782
794
 
783
795
  def self.plan_rows(native)
784
- return nil unless NATIVE_READ_LAYER
796
+ return nil unless NATIVE_READ_LAYER && NATIVE_PLAN_ROWS
785
797
 
786
798
  doc = if native.is_a?(::Leptris::XML::Document)
787
799
  native
data/lib/moxml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.117"
4
+ VERSION = "0.5.118"
5
5
  end
@@ -77,4 +77,28 @@ RSpec.describe "Moxml::Plan adapter conformance" do
77
77
  )
78
78
  end
79
79
  end
80
+
81
+ describe "plan-gate floors (moxml#344)" do
82
+ let(:gate_ctx) { Moxml.new(:leptris) }
83
+
84
+ it "arms both plan faces on engines carrying the #1585/#1586 fixes" do
85
+ skip "testing engine predates the floor" unless
86
+ Gem::Version.new(Leptris::VERSION) >= Gem::Version.new("1.9.320.0")
87
+
88
+ expect(Moxml::Adapter::Leptris::NATIVE_PLAN_ROWS).to be(true)
89
+ expect(Moxml::Adapter::Leptris::NATIVE_PLAN_STRUCTS).to be(true)
90
+ end
91
+
92
+ it "captures namespace-qualified wire attributes (leptris#1586)" do
93
+ skip "plan faces degraded below 1.9.320 (moxml#344)" unless
94
+ Moxml::Adapter::Leptris::NATIVE_PLAN_ROWS
95
+
96
+ plan = Moxml::Plan.new do
97
+ on("e") { |attrs| @captured = attrs }
98
+ end
99
+ plan.parse('<r xmlns:w="urn:w"><e w:name="Aptos"/></r>', gate_ctx)
100
+ captured = plan.instance_variable_get(:@captured)
101
+ expect(captured).to include("w:name" => "Aptos")
102
+ end
103
+ end
80
104
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.117
4
+ version: 0.5.118
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-07 00:00:00.000000000 Z
11
+ date: 2026-10-08 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  Moxml is a unified XML manipulation library that provides a common API
@@ -89,6 +89,10 @@ files:
89
89
  - docs/_tutorials/index.adoc
90
90
  - docs/_tutorials/namespace-handling.adoc
91
91
  - docs/_tutorials/xpath-queries.adoc
92
+ - docs/c14n/README.md
93
+ - docs/c14n/algorithms.md
94
+ - docs/c14n/api.md
95
+ - docs/c14n/examples.md
92
96
  - docs/index.adoc
93
97
  - docs/signature/algorithms.md
94
98
  - docs/signature/architecture.md