canon 0.3.107 → 0.3.108
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 +4 -4
- data/lib/canon/json/jcs.rb +185 -0
- data/lib/canon/json.rb +7 -0
- data/lib/canon/version.rb +1 -1
- data/lib/canon.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 58b0a315d811eb36769d72ef0949180ae5c6eb3b23e4732887783d36483dc8f8
|
|
4
|
+
data.tar.gz: 739ec6e878fb031f6ca8814587f3763952422679858a992d66b45a541e85600f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0a57c1c8062483a762edec9caaef0ee8432f03f88b518163ab9b7836b52c6c671025bf9db6525e870362976c380e1fe0ba7487eb1447e4cdaf2f88538744e89d
|
|
7
|
+
data.tar.gz: 17a52ab1a74f3d34d34fcf9f36fd5c76104be437e5e9bd16d74500306d539863a0233048ddde79078be6fb993e25b3530287006cf735007945200cef29acdaeb
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Canon
|
|
6
|
+
module Json
|
|
7
|
+
# RFC 8785 — the I-JSON Canonicalization Scheme (JCS).
|
|
8
|
+
#
|
|
9
|
+
# Produces the canonical JSON form the RFC defines: UTF-8, object
|
|
10
|
+
# keys sorted by their UTF-16 code units, ECMAScript
|
|
11
|
+
# `Number::toString` number serialization, RFC 8785 §3.2.2.2
|
|
12
|
+
# string escaping, and no insignificant whitespace. The result is
|
|
13
|
+
# byte-stable across platforms and suitable for hashing and
|
|
14
|
+
# signing.
|
|
15
|
+
#
|
|
16
|
+
# Input must be I-JSON (RFC 7493): valid UTF-8 without lone
|
|
17
|
+
# surrogates, no duplicate object keys, numbers expressible as
|
|
18
|
+
# IEEE 754 doubles. Violations raise {Canon::Json::JCS::Error}.
|
|
19
|
+
module JCS
|
|
20
|
+
class Error < ::Canon::Error; end
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# @param json [String, Hash, Array] an I-JSON document or an
|
|
25
|
+
# already-parsed Ruby object graph
|
|
26
|
+
# @return [String] the RFC 8785 canonical form
|
|
27
|
+
def canonicalize(json)
|
|
28
|
+
doc = json.is_a?(String) ? ::JSON.parse(json) : json
|
|
29
|
+
serialize(doc)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def serialize(value)
|
|
33
|
+
out = +""
|
|
34
|
+
write(value, out)
|
|
35
|
+
out
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def write(value, out)
|
|
39
|
+
case value
|
|
40
|
+
when Hash
|
|
41
|
+
write_object(value, out)
|
|
42
|
+
when Array
|
|
43
|
+
out << "["
|
|
44
|
+
value.each_with_index do |item, i|
|
|
45
|
+
out << "," if i.positive?
|
|
46
|
+
write(item, out)
|
|
47
|
+
end
|
|
48
|
+
out << "]"
|
|
49
|
+
when String
|
|
50
|
+
out << escape_string(value)
|
|
51
|
+
when Integer
|
|
52
|
+
out << ijson_integer(value)
|
|
53
|
+
when Float
|
|
54
|
+
out << es6_number(value)
|
|
55
|
+
when true then out << "true"
|
|
56
|
+
when false then out << "false"
|
|
57
|
+
when nil then out << "null"
|
|
58
|
+
else
|
|
59
|
+
raise Error, "JCS input must be JSON data; got #{value.class}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def write_object(hash, out)
|
|
64
|
+
out << "{"
|
|
65
|
+
hash.keys.sort_by { |key| utf16_code_units(key) }
|
|
66
|
+
.each_with_index do |key, i|
|
|
67
|
+
out << "," if i.positive?
|
|
68
|
+
out << escape_string(key) << ":"
|
|
69
|
+
write(hash[key], out)
|
|
70
|
+
end
|
|
71
|
+
out << "}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# RFC 8785 §3.2.2.2 — the JSON.stringify escaping set: short
|
|
75
|
+
# forms for \b \t \n \f \r, two-hex lowercase \u00xx for the
|
|
76
|
+
# remaining C0 controls, \" and \\; every other code point is
|
|
77
|
+
# emitted verbatim as UTF-8 (DEL U+007F is NOT escaped).
|
|
78
|
+
SHORT_ESCAPES = {
|
|
79
|
+
0x08 => "\\b", 0x09 => "\\t", 0x0A => "\\n",
|
|
80
|
+
0x0C => "\\f", 0x0D => "\\r"
|
|
81
|
+
}.freeze
|
|
82
|
+
|
|
83
|
+
def escape_string(string)
|
|
84
|
+
out = +"\""
|
|
85
|
+
string.each_codepoint do |cp|
|
|
86
|
+
out << case cp
|
|
87
|
+
when 0x22 then "\\\""
|
|
88
|
+
when 0x5C then "\\\\"
|
|
89
|
+
when 0x08, 0x09, 0x0A, 0x0C, 0x0D then SHORT_ESCAPES[cp]
|
|
90
|
+
when 0x00..0x1F then format("\\u%04x", cp)
|
|
91
|
+
else
|
|
92
|
+
cp.chr(Encoding::UTF_8)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
out << "\""
|
|
96
|
+
rescue ArgumentError
|
|
97
|
+
raise Error, "JCS input must be valid UTF-8 without lone surrogates"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# RFC 8785 §2.1 (I-JSON): numbers must be expressible as IEEE
|
|
101
|
+
# 754 doubles; anything wider is a contract violation, not a
|
|
102
|
+
# rounding opportunity.
|
|
103
|
+
def ijson_integer(value)
|
|
104
|
+
if value.abs > 9_007_199_254_740_992 # 2**53
|
|
105
|
+
raise Error,
|
|
106
|
+
"JCS input must be I-JSON: #{value} exceeds the IEEE 754 " \
|
|
107
|
+
"double range for exact integers"
|
|
108
|
+
end
|
|
109
|
+
value.to_s
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# ECMA-262 `Number::toString` (the JSON.stringify number shape):
|
|
113
|
+
# shortest-round-trip digits formatted per the spec's fixed /
|
|
114
|
+
# exponential cases, with the boundaries 1e-6 (inclusive fixed
|
|
115
|
+
# on the small side) and 1e21 (exclusive fixed on the large
|
|
116
|
+
# side). -0.0 serializes as "0"; non-finite values are not
|
|
117
|
+
# JSON data and raise.
|
|
118
|
+
def es6_number(value)
|
|
119
|
+
raise Error, "JCS input must be finite numbers" if
|
|
120
|
+
value.nan? || value.infinite?
|
|
121
|
+
return "0" if value.zero?
|
|
122
|
+
|
|
123
|
+
sign = value.negative? ? "-" : ""
|
|
124
|
+
s = value.abs.to_s
|
|
125
|
+
if (m = s.match(/\A(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?\z/))
|
|
126
|
+
int_part = m[1]
|
|
127
|
+
frac = m[2] || ""
|
|
128
|
+
exp = m[3] ? m[3].to_i : 0
|
|
129
|
+
digits = (int_part + frac).sub(/\A0+/, "")
|
|
130
|
+
k = int_part.length + exp - (int_part.length + frac.length - digits.length)
|
|
131
|
+
digits = digits.sub(/0+\z/, "")
|
|
132
|
+
digits = "0" if digits.empty?
|
|
133
|
+
n = digits.length
|
|
134
|
+
return sign + format_digits(digits, n, k)
|
|
135
|
+
end
|
|
136
|
+
# Float#to_s always matches above; the fallthrough is defensive
|
|
137
|
+
raise Error, "unserializable number: #{s}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# n and k are ECMA-262 7.1.12.1's own notation (digit count and
|
|
141
|
+
# the decimal-point position); renamed parameters would lose the
|
|
142
|
+
# spec mapping.
|
|
143
|
+
# rubocop:disable Naming/MethodParameterName
|
|
144
|
+
def format_digits(digits, n, k)
|
|
145
|
+
if k <= 0
|
|
146
|
+
# 0.s × 10^k: fixed for -6 < k <= 0 ("0." + zeros + digits),
|
|
147
|
+
# exponential at k <= -6 (values below 1e-6, ECMA boundary)
|
|
148
|
+
return exp_form(digits, k) if k <= -6
|
|
149
|
+
|
|
150
|
+
["0.", "0" * -k, digits].join
|
|
151
|
+
elsif k >= n
|
|
152
|
+
return digits if k == n
|
|
153
|
+
|
|
154
|
+
# integer-shaped: pad zeros while k <= 21 ("1" + twenty
|
|
155
|
+
# zeros for 1e20); exponential above (1e21 -> "1e+21")
|
|
156
|
+
return exp_form(digits, k) if k > 21
|
|
157
|
+
|
|
158
|
+
digits + ("0" * (k - n))
|
|
159
|
+
else
|
|
160
|
+
# 0 < k < n: the point sits inside the digit string
|
|
161
|
+
"#{digits[0, k]}.#{digits[k..]}"
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def exp_form(digits, k)
|
|
166
|
+
mantissa = digits.length == 1 ? digits : "#{digits[0]}.#{digits[1..]}"
|
|
167
|
+
e = k - 1
|
|
168
|
+
"#{mantissa}e#{e.negative? ? '-' : '+'}#{e.abs}"
|
|
169
|
+
end
|
|
170
|
+
# rubocop:enable Naming/MethodParameterName
|
|
171
|
+
|
|
172
|
+
# RFC 8785 §3.2.3: keys sort by their UTF-16 code units (the
|
|
173
|
+
# ECMAScript array sort). Ruby string <=> compares by codepoint,
|
|
174
|
+
# which diverges from UTF-16 order above the BMP (astral planes
|
|
175
|
+
# sort after U+E000..U+FFFF in UTF-16 order).
|
|
176
|
+
def utf16_code_units(string)
|
|
177
|
+
string.encode("UTF-16LE").unpack("v*")
|
|
178
|
+
rescue Encoding::UndefinedConversionError
|
|
179
|
+
raise Error,
|
|
180
|
+
"JCS keys must be valid UTF-16 (no lone surrogates): " \
|
|
181
|
+
"#{string.inspect}"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
data/lib/canon/json.rb
ADDED
data/lib/canon/version.rb
CHANGED
data/lib/canon.rb
CHANGED
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.
|
|
4
|
+
version: 0.3.108
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -347,6 +347,8 @@ files:
|
|
|
347
347
|
- lib/canon/html.rb
|
|
348
348
|
- lib/canon/html/data_model.rb
|
|
349
349
|
- lib/canon/html/nokogiri_support.rb
|
|
350
|
+
- lib/canon/json.rb
|
|
351
|
+
- lib/canon/json/jcs.rb
|
|
350
352
|
- lib/canon/json_parsing.rb
|
|
351
353
|
- lib/canon/nokogiri_loader.rb
|
|
352
354
|
- lib/canon/options.rb
|