"""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")