exoskeleton/code/experiments/hashing.py

63 lines
1.9 KiB
Python
Raw Normal View History

"""Canonical serialization and stable SHA-256 helpers."""
from __future__ import annotations
import dataclasses
import hashlib
import json
from pathlib import Path
from typing import Any, Mapping
import numpy as np
def to_jsonable(value: Any) -> Any:
"""Convert supported scientific-Python values to strict JSON values."""
if dataclasses.is_dataclass(value):
return to_jsonable(dataclasses.asdict(value))
if isinstance(value, Path):
return str(value)
if isinstance(value, np.ndarray):
return to_jsonable(value.tolist())
if isinstance(value, np.generic):
return to_jsonable(value.item())
if isinstance(value, Mapping):
return {str(key): to_jsonable(item) for key, item in value.items()}
if isinstance(value, (tuple, list)):
return [to_jsonable(item) for item in value]
if isinstance(value, (str, int, float, bool)) or value is None:
return value
raise TypeError(f"Unsupported value for canonical JSON: {type(value).__name__}")
def canonical_json_bytes(value: Any) -> bytes:
"""Return deterministic UTF-8 JSON bytes, rejecting NaN and infinity."""
return json.dumps(
to_jsonable(value),
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
allow_nan=False,
).encode("utf-8")
def stable_hash(value: Any, *, prefix: str = "") -> str:
"""Hash a value using canonical JSON and an optional domain prefix."""
digest = hashlib.sha256()
if prefix:
digest.update(prefix.encode("utf-8"))
digest.update(b"\0")
digest.update(canonical_json_bytes(value))
return digest.hexdigest()
def file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
while True:
block = stream.read(1024 * 1024)
if not block:
break
digest.update(block)
return digest.hexdigest()