60 lines
1.7 KiB
Python
60 lines
1.7 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
def sha256_file(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as handle:
|
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def sha256_bytes(data: bytes) -> str:
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def atomic_write_json(path: Path, data: Any) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
fd, tmp_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
|
|
try:
|
|
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
json.dump(data, handle, indent=2, sort_keys=True)
|
|
handle.write("\n")
|
|
Path(tmp_name).replace(path)
|
|
except Exception:
|
|
Path(tmp_name).unlink(missing_ok=True)
|
|
raise
|
|
|
|
|
|
def read_json(path: Path, default: Any) -> Any:
|
|
if not path.exists():
|
|
return default
|
|
with path.open("r", encoding="utf-8") as handle:
|
|
return json.load(handle)
|
|
|
|
|
|
def ensure_relative(path: str) -> Path:
|
|
if "\\" in path:
|
|
raise ValueError(f"unsafe relative path: {path}")
|
|
rel = Path(path)
|
|
if rel.is_absolute() or ".." in rel.parts or any(part.endswith(":") for part in rel.parts):
|
|
raise ValueError(f"unsafe relative path: {path}")
|
|
return rel
|
|
|
|
|
|
def ensure_inside(root: Path, target: Path) -> Path:
|
|
root_resolved = root.resolve()
|
|
target_resolved = target.resolve(strict=False)
|
|
try:
|
|
target_resolved.relative_to(root_resolved)
|
|
except ValueError as exc:
|
|
raise ValueError(f"target escapes instance root: {target}") from exc
|
|
return target_resolved
|