canon 0.3.116 → 0.3.117

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: 89df5f303938b98100a1d44399c7eb5fbbf793f0f91c445c4f0533a19234d5ec
4
- data.tar.gz: 1308c9228b0a6efc15e81ebef134c7495aadd100e10978a23ee09b92ad558555
3
+ metadata.gz: 223b5ab0201bbebfe8bfee1de2f23229260f574bd6d42fed426256b199f03d6c
4
+ data.tar.gz: 803eaba7a9fd850a38669c9ea2bbd567503fbc936fd39147a473d2526b3539ce
5
5
  SHA512:
6
- metadata.gz: 93cfb6bca734697c5cc79173dac20bbe53cb4fb17e72fc72e7aa726af366c187c09ae0c30e5a775d9ea8b9e94f1194b29657f9da3d44c58f6fbc116717e39909
7
- data.tar.gz: 37f713fb17fe7961cb622f42c96605cf0bb19227a6831018d0505afdc33ad2e315e54b1a65145219457e30d8b0c6d06d001869611bbe94f55b443c2cbf0e4252
6
+ metadata.gz: 24ae7e292b598242df4c98bad7d98d98815d7d28ddebd6d1ed79df4451e40017cd3840f2a3556bf4b78681ee7b6653ee91a67623d806d2aec1d8e170093a19d8
7
+ data.tar.gz: 35a5642e0e6b5b0268935d6888470614058888a3e4cc33d587ea079737a3817b9543aa63fbae986da95240a2c4c72174419eae1fba9336e871859ae6aae6ee3d
@@ -0,0 +1,50 @@
1
+ = The canonical digests (W5.3)
2
+
3
+ This document specifies canon's canonical digest API as shipped by
4
+ `Canon::Digest`. Reviewed against the implementation by
5
+ `spec/canon/docs/canonical_digests_spec.rb` (the W6.3 review-gate
6
+ pattern), with the cross-release byte pins carried by
7
+ `spec/canon/digest_stability_spec.rb`.
8
+
9
+ == 1. The law
10
+
11
+ The digest of a document is the OpenSSL hash of its CANONICAL
12
+ FORM — the form under which the format's comparison pipeline deems
13
+ documents equivalent. Two documents that compare equivalent produce
14
+ identical digests; two that compare distinct produce distinct
15
+ digests. The digest is an API over the comparison semantics, not a
16
+ new canonicalization.
17
+
18
+ == 2. Per-format canonical forms
19
+
20
+ * *XML* rides Canonical XML 1.1
21
+ (`Canon::Xml::C14n.canonicalize`, reached as
22
+ `Formatters::XmlFormatter.format(pretty: false)`). The
23
+ pretty-printed XML form preserves attribute order and is NOT
24
+ canonical; the digest lane never touches it.
25
+ * *JSON and YAML* ride the key-sorted formatter forms (the JCS
26
+ profile for JSON's canonical lane; the `[class, to_s]` TOTAL
27
+ key order for YAML).
28
+ * *HTML* rides the whitespace-law form with sorted attributes
29
+ (the W1.3 specification).
30
+
31
+ Equivalence consequences the digest inherits: attribute order is
32
+ insignificant in XML and HTML; mapping key order is insignificant
33
+ in JSON and YAML; element/sequence order is significant everywhere.
34
+
35
+ == 3. Entry points
36
+
37
+ `Canon::Digest.digest(content, format, algorithm: "sha256")`
38
+ accepts any OpenSSL digest name; `canonical_form(content, format)`
39
+ exposes the hashed bytes. Per-format shorthands
40
+ (`Canon.digest_xml`, `digest_yaml`, `digest_json`, ...) mirror the
41
+ format lane's shorthands.
42
+
43
+ == 4. Stability contract
44
+
45
+ The digests are stable across releases for as long as the
46
+ underlying canonical form is. The stability suite pins the digest
47
+ hex of a representative document per format; a canonical-form
48
+ change moves a digest and MUST fail that suite, forcing a
49
+ conscious pin update (and a release note) rather than silent drift
50
+ for downstream dedup stores.
@@ -0,0 +1,52 @@
1
+ = The JCS canonical profile (W6.1, RFC 8785)
2
+
3
+ This document specifies canon's JSON JCS canonical form as shipped
4
+ by `Canon::Json::JCS`. Reviewed against the implementation by
5
+ `spec/canon/docs/jcs_spec_review.rb` (the W6.3 review-gate
6
+ pattern).
7
+
8
+ == 1. Scope
9
+
10
+ RFC 8785 — the I-JSON Canonicalization Scheme. Input must be I-JSON
11
+ (RFC 7493): valid UTF-8 without lone surrogates, no duplicate
12
+ object keys, numbers expressible as IEEE 754 doubles. Violations
13
+ raise `Canon::Json::JCS::Error`.
14
+
15
+ == 2. The canonical laws
16
+
17
+ * *Strings* (`escape_string`): RFC 8785 §3.2.2.2 — the
18
+ JSON.stringify escaping set: short forms for `\b \t \n \f \r`
19
+ (the SHORT_ESCAPES table), two-hex lowercase `\u00xx` for the
20
+ remaining C0 controls, `\"` and `\\`; every other code point
21
+ emits verbatim as UTF-8 (DEL U+007F is NOT escaped).
22
+
23
+ * *Numbers* (`es6_number`): ECMAScript `Number::toString`
24
+ serialization — the shortest round-tripping decimal, fixed
25
+ notation inside [1e-6, 1e21), exponential outside; integers
26
+ via `ijson_integer` (the I-JSON 2^53 range).
27
+
28
+ * *Object keys* (`write_object`): sorted by their
29
+ UTF-16 code units (`utf16_code_units` — the RFC's sort is over
30
+ UTF-16 code units, not code points, so surrogate pairs and BMP
31
+ characters interleave per the RFC's examples; e.g. the key
32
+ U+1D160 (a surrogate pair, code units 0xD834 0xDD60) sorts
33
+ BEFORE U+20AC (a single BMP code unit 0x20AC), which sorts
34
+ BEFORE "a" (0x0061)).
35
+
36
+ * *Containers*: arrays comma-separated in order; objects
37
+ comma-separated in sorted key order; no whitespace anywhere.
38
+
39
+ == 3. Entry points
40
+
41
+ `canonicalize(json)` accepts a JSON string (parsed through
42
+ `JSON.parse`) or an already-parsed Ruby graph; `serialize(value)`
43
+ skips the parse. The canonical form is byte-stable across
44
+ platforms and suitable for hashing and signing.
45
+
46
+ == 4. Fixed point and parity
47
+
48
+ JCS output re-parses to the same graph, and the form is a fixed
49
+ point. The phase 02 property suite (JSON/YAML equivalence legs)
50
+ and the diff-display corpus pin the behavior; the JCS spec
51
+ vectors (RFC 8785 §3.2.3 examples) pin the number and sorting
52
+ laws.
data/lib/canon/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Canon
4
- VERSION = "0.3.116"
4
+ VERSION = "0.3.117"
5
5
  end
@@ -55,12 +55,30 @@ module Canon
55
55
  # — every node in the subtrees rooted at pfx:name. Returns
56
56
  # [prefix, local_name], or nil for anything else.
57
57
  def self.c14n_selection_idiom(xpath)
58
- # the suite's .xpath files are XML documents wrapping the
59
- # expression (with prefix declarations and inline comments)
60
- text = xpath.sub(/\A<\?xml[^?]*\?>/, "")
61
- .gsub(/<!--[\s\S]*?-->/, "")
62
- .gsub(/<[^>]*>/, " ")
63
- .squeeze(" ")
58
+ # The suite's .xpath files are XML documents wrapping the
59
+ # expression (with prefix declarations and inline comments).
60
+ # Extraction is index-based string surgery — the CodeQL
61
+ # polynomial-redos query flags the gsub-chain form.
62
+ close_tag = xpath.rindex("</XPath>")
63
+ text = close_tag ? xpath[0...close_tag] : xpath.dup
64
+ # drop the XML declaration and the <XPath> open tag: each
65
+ # ends at the first '>' at/after its '<'
66
+ if (decl = text.index("<"))
67
+ gt = text.index(">", decl)
68
+ if (xopen = text.index("<XPath", decl)) && (xopen < gt)
69
+ gt = text.index(">", xopen)
70
+ end
71
+ text = text[(gt + 1)..] if gt
72
+ end
73
+ # strip <!-- --> comments by index (no regex backtracking)
74
+ loop do
75
+ open_c = text.index("<!--")
76
+ break unless open_c
77
+
78
+ close_c = text.index("-->", open_c + 4)
79
+ text = close_c ? text[0...open_c] + text[(close_c + 3)..] : text[0...open_c]
80
+ end
81
+ text = text.squeeze(" ")
64
82
  m = text.match(%r{\(//\.\s*\|\s*//@\*\s*\|\s*//namespace::\*\)\s*\[\s*ancestor-or-self::([A-Za-z_][\w.-]*)(?::([\w.-]+))?\s*\]\s*\z})
65
83
  return nil unless m
66
84
 
@@ -30,16 +30,33 @@ class PerformanceComparator
30
30
  DIM = "\e[2m"
31
31
  CLEAR = "\e[0m"
32
32
 
33
+ BEST_OF = Integer(ENV.fetch("CANON_PERF_BEST_OF", "3"))
34
+
33
35
  def run
34
36
  clone_base
35
- current = run_report(REPO_ROOT, "current")
36
- base = run_report(base_clone_dir, "base (#{DEFAULT_BASE})")
37
+ current = best_of(REPO_ROOT, "current")
38
+ base = best_of(base_clone_dir, "base (#{DEFAULT_BASE})")
37
39
  print_report(current, base)
38
40
  exit(1) if regressions?(current, base)
39
41
  ensure
40
42
  cleanup
41
43
  end
42
44
 
45
+ # The html_parsing lanes drift up to ~27% BETWEEN SAME-CONTENT runs
46
+ # on shared runners (main failed against itself: run 40047205814) —
47
+ # single-run comparisons measure the runner, not the code. Each side
48
+ # runs BEST_OF times; per benchmark, the run with the highest upper
49
+ # bound stands (the capability estimate a fair gate compares).
50
+ def best_of(working_dir, label)
51
+ runs = Array.new(BEST_OF) { |i| run_report(working_dir, "#{label} ##{i + 1}") }
52
+ runs.first.fetch("benchmarks").keys.each_with_object({}) do |bench, merged|
53
+ best = runs.max_by { |r| r.fetch("benchmarks").fetch(bench).fetch("upper").to_f }
54
+ merged[bench] = best.fetch("benchmarks").fetch(bench)
55
+ end.then do |benchmarks|
56
+ runs.first.merge("benchmarks" => benchmarks)
57
+ end
58
+ end
59
+
43
60
  private
44
61
 
45
62
  def clone_base
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: canon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.116
4
+ version: 0.3.117
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -173,8 +173,10 @@ files:
173
173
  - docs/Gemfile
174
174
  - docs/INDEX.adoc
175
175
  - docs/_config.yml
176
+ - docs/_references/canonical-digests.adoc
176
177
  - docs/_references/comparison-theory.adoc
177
178
  - docs/_references/html-canonical-form.adoc
179
+ - docs/_references/jcs-rfc8785.adoc
178
180
  - docs/_references/yaml-canonical-form.adoc
179
181
  - docs/advanced/diff-classification.adoc
180
182
  - docs/advanced/diff-pipeline.adoc