|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +from dataclasses import dataclass |
| 4 | + |
| 5 | +from endstone_endkeep.logical.merge import hash_state |
| 6 | + |
| 7 | +from .gc import referenced_object_hashes |
| 8 | +from .manifest import ManifestStore, RepositoryManifest |
| 9 | +from .objects import ObjectMetadata, ObjectStore |
| 10 | +from .reader import RepositoryReader |
| 11 | + |
| 12 | + |
| 13 | +class VerificationError(RuntimeError): |
| 14 | + pass |
| 15 | + |
| 16 | + |
| 17 | +@dataclass(frozen=True) |
| 18 | +class VerifyReport: |
| 19 | + generation: int | None |
| 20 | + snapshots: int |
| 21 | + referenced_objects: int |
| 22 | + orphan_objects: int |
| 23 | + deep: bool |
| 24 | + |
| 25 | + |
| 26 | +class RepositoryVerifier: |
| 27 | + def __init__(self, manifests: ManifestStore, objects: ObjectStore) -> None: |
| 28 | + self.manifests = manifests |
| 29 | + self.objects = objects |
| 30 | + self.reader = RepositoryReader(objects) |
| 31 | + |
| 32 | + def verify(self, *, deep: bool = False) -> VerifyReport: |
| 33 | + manifest = self.manifests.load_current() |
| 34 | + if manifest is None: |
| 35 | + return VerifyReport(None, 0, 0, self._count_object_files(), deep) |
| 36 | + |
| 37 | + self._verify_structural(manifest) |
| 38 | + referenced = referenced_object_hashes(manifest) |
| 39 | + existing = {path.stem for path in self.objects.objects_root.glob("*/*.zst")} |
| 40 | + orphans = existing - referenced |
| 41 | + |
| 42 | + if deep: |
| 43 | + self._verify_deep(manifest) |
| 44 | + |
| 45 | + return VerifyReport( |
| 46 | + generation=manifest.generation, |
| 47 | + snapshots=len(manifest.chain), |
| 48 | + referenced_objects=len(referenced), |
| 49 | + orphan_objects=len(orphans), |
| 50 | + deep=deep, |
| 51 | + ) |
| 52 | + |
| 53 | + def _verify_structural(self, manifest: RepositoryManifest) -> None: |
| 54 | + manifest.validate_chain() |
| 55 | + for node in manifest.chain: |
| 56 | + self._verify_metadata(node.object) |
| 57 | + self._verify_metadata(node.sidecar) |
| 58 | + |
| 59 | + def _verify_metadata(self, metadata: ObjectMetadata) -> None: |
| 60 | + if metadata.logical_bytes < 0 or metadata.compressed_bytes <= 0: |
| 61 | + raise VerificationError("object metadata contains invalid sizes") |
| 62 | + path = self.objects.path_for(metadata.logical_sha256) |
| 63 | + if not path.is_file(): |
| 64 | + raise VerificationError(f"referenced object is missing: {metadata.logical_sha256}") |
| 65 | + if path.stat().st_size != metadata.compressed_bytes: |
| 66 | + raise VerificationError( |
| 67 | + f"compressed size mismatch for {metadata.logical_sha256}: " |
| 68 | + f"{path.stat().st_size} != {metadata.compressed_bytes}" |
| 69 | + ) |
| 70 | + |
| 71 | + def _verify_deep(self, manifest: RepositoryManifest) -> None: |
| 72 | + checked: set[str] = set() |
| 73 | + for node in manifest.chain: |
| 74 | + for metadata in (node.object, node.sidecar): |
| 75 | + if metadata.logical_sha256 not in checked: |
| 76 | + self.objects.verify(metadata) |
| 77 | + checked.add(metadata.logical_sha256) |
| 78 | + |
| 79 | + for node in manifest.chain: |
| 80 | + stats = hash_state(self.reader.iter_state(manifest, snapshot=node.snapshot)) |
| 81 | + if ( |
| 82 | + stats.sha256 != node.state_sha256 |
| 83 | + or stats.records != node.records |
| 84 | + or stats.value_bytes != node.value_bytes |
| 85 | + ): |
| 86 | + raise VerificationError(f"state digest mismatch at snapshot {node.snapshot}") |
| 87 | + |
| 88 | + def _count_object_files(self) -> int: |
| 89 | + if not self.objects.objects_root.exists(): |
| 90 | + return 0 |
| 91 | + return sum(1 for _ in self.objects.objects_root.glob("*/*.zst")) |
0 commit comments