mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 00:48:07 +00:00
160 lines
4.6 KiB
Python
160 lines
4.6 KiB
Python
"""
|
|
R77 Integrity Envelopes.
|
|
|
|
Provides canonical serialization and integrity verification for persisted state.
|
|
"""
|
|
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
from dataclasses import asdict, dataclass
|
|
from typing import Any, Dict, Optional, Union
|
|
|
|
from .safe_io import PathTraversalError, resolve_under_root
|
|
|
|
logger = logging.getLogger("ComfyUI-OpenClaw.services.integrity")
|
|
|
|
|
|
@dataclass
|
|
class IntegrityEnvelope:
|
|
"""
|
|
Wrapper for persisted data with integrity metadata.
|
|
"""
|
|
|
|
version: int
|
|
data: Dict[str, Any]
|
|
hash: str # SHA256 of canonical(data)
|
|
algo: str = "sha256"
|
|
meta: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
class IntegrityError(Exception):
|
|
"""Raised when integrity verification fails."""
|
|
|
|
pass
|
|
|
|
|
|
def _normalize_verified_path(path: Union[str, os.PathLike[str]]) -> str:
|
|
raw_path = os.fspath(path)
|
|
if not raw_path:
|
|
raise IntegrityError("verified file path is empty")
|
|
|
|
abs_path = os.path.abspath(raw_path)
|
|
dir_name = os.path.dirname(abs_path)
|
|
file_name = os.path.basename(abs_path)
|
|
if not dir_name or not file_name or file_name in {".", ".."}:
|
|
raise IntegrityError(f"Invalid verified file path: {raw_path}")
|
|
|
|
try:
|
|
return resolve_under_root(dir_name, file_name, follow_symlinks=False)
|
|
except PathTraversalError as exc:
|
|
raise IntegrityError(f"Invalid verified file path: {raw_path}") from exc
|
|
|
|
|
|
def canonical_dumps(data: Any) -> bytes:
|
|
"""
|
|
Serialize data to canonical JSON (sorted keys, no whitespace).
|
|
"""
|
|
return json.dumps(data, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
|
|
|
|
def calculate_hash(data: Any, algo: str = "sha256") -> str:
|
|
"""
|
|
Calculate hash of canonicalized data.
|
|
"""
|
|
if algo != "sha256":
|
|
raise ValueError(f"Unsupported hash algorithm: {algo}")
|
|
|
|
payload = canonical_dumps(data)
|
|
return hashlib.sha256(payload).hexdigest()
|
|
|
|
|
|
def save_verified(path: str, data: Dict[str, Any], version: int = 1) -> None:
|
|
"""
|
|
Save data wrapped in an integrity envelope.
|
|
Atomic write.
|
|
"""
|
|
safe_path = _normalize_verified_path(path)
|
|
data_hash = calculate_hash(data)
|
|
envelope = IntegrityEnvelope(
|
|
version=version, data=data, hash=data_hash, algo="sha256"
|
|
)
|
|
|
|
# Write to temp string first to Ensure serialization works
|
|
try:
|
|
content = json.dumps(asdict(envelope), indent=2)
|
|
except Exception as e:
|
|
logger.error(f"Failed to serialize integrity envelope for {safe_path}: {e}")
|
|
raise
|
|
|
|
# Atomic write
|
|
dir_name = os.path.dirname(safe_path)
|
|
os.makedirs(dir_name, exist_ok=True)
|
|
|
|
fd, tmp_path = tempfile.mkstemp(dir=dir_name, text=True)
|
|
try:
|
|
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
f.write(content)
|
|
f.flush()
|
|
os.fsync(fd)
|
|
|
|
# Renaissance-style atomic rename
|
|
os.replace(tmp_path, safe_path)
|
|
except Exception as e:
|
|
logger.error(f"Failed to save verified file {safe_path}: {e}")
|
|
if os.path.exists(tmp_path):
|
|
os.remove(tmp_path)
|
|
raise
|
|
|
|
|
|
def load_verified(
|
|
path: str, expected_version: int = 1, migrate: bool = True
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Load data from an integrity envelope.
|
|
|
|
If `migrate` is True and the file is valid legacy JSON (no envelope),
|
|
it returns the data as-is (caller should save back to upgrade).
|
|
|
|
Raises IntegrityError if hash mismatch or malformed.
|
|
"""
|
|
safe_path = _normalize_verified_path(path)
|
|
if not os.path.exists(safe_path):
|
|
raise FileNotFoundError(f"File not found: {safe_path}")
|
|
|
|
try:
|
|
with open(safe_path, "r", encoding="utf-8") as f:
|
|
raw = json.load(f)
|
|
except json.JSONDecodeError as e:
|
|
raise IntegrityError(f"Corrupt JSON file {safe_path}: {e}")
|
|
|
|
# Check if it's an envelope
|
|
if isinstance(raw, dict) and "hash" in raw and "data" in raw and "version" in raw:
|
|
# Verify integrity
|
|
stored_hash = raw["hash"]
|
|
stored_data = raw["data"]
|
|
|
|
computed_hash = calculate_hash(stored_data)
|
|
if computed_hash != stored_hash:
|
|
raise IntegrityError(
|
|
f"Integrity check failed for {safe_path} (hash mismatch)"
|
|
)
|
|
|
|
# Verify version if needed
|
|
# We can implement version migration logic here if multiple envelope versions exist
|
|
|
|
return stored_data
|
|
|
|
# Legacy Fallback
|
|
if migrate:
|
|
logger.info(
|
|
f"R77: Loaded legacy file {safe_path}, integrity check skipped (pending migration)."
|
|
)
|
|
# For legacy files, we assume the whole content is the data.
|
|
return raw
|
|
|
|
raise IntegrityError(f"File {safe_path} is not a valid integrity envelope")
|