Skip to content

Commit ff8c8a8

Browse files
committed
feat: add structural and deep repository verification
1 parent 982952d commit ff8c8a8

1 file changed

Lines changed: 91 additions & 0 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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

Comments
 (0)