mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 15:41:41 +02:00
72 lines
2.4 KiB
Python
72 lines
2.4 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
|
|
MAX_SAFE_INTEGER = 9_007_199_254_740_991
|
|
|
|
|
|
def strict_json_loads(text: str) -> Any:
|
|
"""Load the integer-only I-JSON profile used by signed AMPG contracts."""
|
|
|
|
def object_pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
result: dict[str, Any] = {}
|
|
for key, value in pairs:
|
|
if key in result:
|
|
raise ValueError(f"duplicate JSON object key: {key!r}")
|
|
result[key] = value
|
|
return result
|
|
|
|
def reject_float(value: str) -> float:
|
|
raise ValueError(f"floating-point JSON numbers are not permitted: {value}")
|
|
|
|
def reject_constant(value: str) -> None:
|
|
raise ValueError(f"non-I-JSON number is not permitted: {value}")
|
|
|
|
return json.loads(
|
|
text,
|
|
object_pairs_hook=object_pairs,
|
|
parse_float=reject_float,
|
|
parse_constant=reject_constant,
|
|
)
|
|
|
|
|
|
def canonicalize_jcs(value: Any) -> bytes:
|
|
"""Return RFC 8785 bytes for AMPG's integer-only I-JSON profile."""
|
|
|
|
return _canonicalize(value).encode("utf-8")
|
|
|
|
|
|
def _canonicalize(value: Any) -> str:
|
|
if value is None:
|
|
return "null"
|
|
if value is True:
|
|
return "true"
|
|
if value is False:
|
|
return "false"
|
|
if isinstance(value, int):
|
|
if abs(value) > MAX_SAFE_INTEGER:
|
|
raise ValueError(f"integer exceeds I-JSON safe range: {value}")
|
|
return str(value)
|
|
if isinstance(value, float):
|
|
raise TypeError("floating-point values are not supported by signed AMPG contracts")
|
|
if isinstance(value, str):
|
|
_reject_surrogates(value)
|
|
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
|
if isinstance(value, list):
|
|
return "[" + ",".join(_canonicalize(item) for item in value) + "]"
|
|
if isinstance(value, dict):
|
|
if not all(isinstance(key, str) for key in value):
|
|
raise TypeError("JSON object keys must be strings")
|
|
keys = sorted(value, key=lambda key: key.encode("utf-16-be"))
|
|
return "{" + ",".join(
|
|
_canonicalize(key) + ":" + _canonicalize(value[key]) for key in keys
|
|
) + "}"
|
|
raise TypeError(f"unsupported JSON value: {type(value).__name__}")
|
|
|
|
|
|
def _reject_surrogates(value: str) -> None:
|
|
if any(0xD800 <= ord(char) <= 0xDFFF for char in value):
|
|
raise ValueError("lone Unicode surrogate is not valid I-JSON")
|