111 lines
3.0 KiB
Python
111 lines
3.0 KiB
Python
"""Canonical JSON, RFC 8785 (JSON Canonicalization Scheme).
|
|
|
|
Non-normative.
|
|
The normative rule is spec/01-conventions.md; this module exists so the vectors can be generated and checked.
|
|
"""
|
|
|
|
import math
|
|
|
|
_ESC = {
|
|
0x08: "\\b",
|
|
0x09: "\\t",
|
|
0x0A: "\\n",
|
|
0x0C: "\\f",
|
|
0x0D: "\\r",
|
|
0x22: '\\"',
|
|
0x5C: "\\\\",
|
|
}
|
|
|
|
|
|
def _string(s):
|
|
out = ['"']
|
|
for ch in s:
|
|
cp = ord(ch)
|
|
if 0xD800 <= cp <= 0xDFFF:
|
|
raise ValueError("lone surrogate in string")
|
|
if cp in _ESC:
|
|
out.append(_ESC[cp])
|
|
elif cp < 0x20:
|
|
out.append("\\u%04x" % cp)
|
|
else:
|
|
out.append(ch)
|
|
out.append('"')
|
|
return "".join(out)
|
|
|
|
|
|
def _number(x):
|
|
"""ECMAScript Number.prototype.toString, as RFC 8785 requires."""
|
|
if isinstance(x, bool):
|
|
raise TypeError("bool is not a number")
|
|
if isinstance(x, int):
|
|
if abs(x) > 2**53:
|
|
raise ValueError("integer outside the IEEE 754 exact range")
|
|
return str(x)
|
|
if not math.isfinite(x):
|
|
raise ValueError("NaN and Infinity are not JSON")
|
|
if x == 0:
|
|
return "0"
|
|
if x < 0:
|
|
return "-" + _number(-x)
|
|
# Shortest round-trip digits, as ECMAScript requires.
|
|
r = repr(x)
|
|
if "e" in r or "E" in r:
|
|
mant, exp = r.lower().split("e")
|
|
exp = int(exp)
|
|
else:
|
|
mant, exp = r, 0
|
|
if "." in mant:
|
|
ip, fp = mant.split(".")
|
|
else:
|
|
ip, fp = mant, ""
|
|
digits = (ip + fp).lstrip("0")
|
|
# Position of the decimal point relative to the start of `digits`.
|
|
lead_zeros = len(ip + fp) - len((ip + fp).lstrip("0"))
|
|
n = len(ip) - lead_zeros + exp
|
|
digits = digits.rstrip("0") or "0"
|
|
k = len(digits)
|
|
if k <= n <= 21:
|
|
return digits + "0" * (n - k)
|
|
if 0 < n <= 21:
|
|
return digits[:n] + "." + digits[n:]
|
|
if -6 < n <= 0:
|
|
return "0." + "0" * (-n) + digits
|
|
e = n - 1
|
|
sign = "+" if e >= 0 else "-"
|
|
if k == 1:
|
|
return digits + "e" + sign + str(abs(e))
|
|
return digits[0] + "." + digits[1:] + "e" + sign + str(abs(e))
|
|
|
|
|
|
def _key(s):
|
|
# RFC 8785 sorts member names by their UTF-16 code units.
|
|
return s.encode("utf-16-be")
|
|
|
|
|
|
def dumps(value):
|
|
"""Serialize `value` to canonical JSON text."""
|
|
if value is None:
|
|
return "null"
|
|
if value is True:
|
|
return "true"
|
|
if value is False:
|
|
return "false"
|
|
if isinstance(value, str):
|
|
return _string(value)
|
|
if isinstance(value, (int, float)):
|
|
return _number(value)
|
|
if isinstance(value, (list, tuple)):
|
|
return "[" + ",".join(dumps(v) for v in value) + "]"
|
|
if isinstance(value, dict):
|
|
for k in value:
|
|
if not isinstance(k, str):
|
|
raise TypeError("object keys must be strings")
|
|
items = sorted(value.items(), key=lambda kv: _key(kv[0]))
|
|
return "{" + ",".join(_string(k) + ":" + dumps(v) for k, v in items) + "}"
|
|
raise TypeError("not JSON: %r" % type(value))
|
|
|
|
|
|
def dumpb(value):
|
|
"""Canonical JSON as UTF-8 bytes."""
|
|
return dumps(value).encode("utf-8")
|