1use std::path::PathBuf;
2
3use iroh_blobs::Hash;
4
5use crate::{
6 Result, SyncwebError,
7 folder::SyncwebFolder,
8 node::{blob_store::BlobStore, docs_engine::DocsEngine},
9};
10
11#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
13#[non_exhaustive]
14pub struct CorruptionInfo {
15 pub path: PathBuf,
16 pub expected_hash: Hash,
17 pub actual_hash: Hash,
18}
19
20#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize)]
22#[non_exhaustive]
23pub struct VerifyResult {
24 pub total: u64,
25 pub verified: u64,
26 pub corrupted: Vec<CorruptionInfo>,
27 pub missing: Vec<PathBuf>,
28}
29
30impl VerifyResult {
31 #[must_use]
32 pub const fn is_valid(&self) -> bool {
33 self.corrupted.is_empty() && self.missing.is_empty()
34 }
35}
36
37#[derive(Clone, Debug, Default)]
39#[non_exhaustive]
40pub struct VerifyFilter {
41 pub hashes: Option<Vec<Hash>>,
42 pub path: Option<PathBuf>,
43 pub glob: Option<String>,
44}
45
46impl VerifyFilter {
47 #[must_use]
48 pub const fn new() -> Self {
49 Self {
50 hashes: None,
51 path: None,
52 glob: None,
53 }
54 }
55
56 #[must_use]
57 pub fn with_hashes(mut self, hashes: Vec<Hash>) -> Self {
58 self.hashes = Some(hashes);
59 self
60 }
61
62 #[must_use]
63 pub fn with_path(mut self, path: PathBuf) -> Self {
64 self.path = Some(path);
65 self
66 }
67
68 #[must_use]
70 pub fn matches(&self, entry_key: &[u8], entry_hash: &Hash) -> bool {
71 if let Some(ref hashes) = self.hashes
72 && !hashes.contains(entry_hash)
73 {
74 return false;
75 }
76 if let Some(ref path) = self.path {
77 let entry_path = String::from_utf8_lossy(entry_key);
78 if entry_path != path.to_string_lossy() {
79 return false;
80 }
81 }
82 if let Some(ref glob) = self.glob {
83 let entry_path = String::from_utf8_lossy(entry_key);
84 if !glob_match(glob, &entry_path) {
85 return false;
86 }
87 }
88 true
89 }
90}
91
92fn glob_match(pattern: &str, path: &str) -> bool {
93 globset::Glob::new(pattern).is_ok_and(|glob| glob.compile_matcher().is_match(path))
94}
95
96#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
98#[non_exhaustive]
99pub struct RepairOutcome {
100 pub path: PathBuf,
101 pub hash: Hash,
102 pub success: bool,
103 pub error: Option<String>,
104}
105
106impl RepairOutcome {
107 #[must_use]
108 pub const fn new(path: PathBuf, hash: Hash, success: bool, error: Option<String>) -> Self {
109 Self {
110 path,
111 hash,
112 success,
113 error,
114 }
115 }
116}
117
118#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize)]
120#[non_exhaustive]
121pub struct RepairResult {
122 pub attempted: u64,
123 pub repaired: u64,
124 pub failed: Vec<RepairOutcome>,
125}
126
127#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
129#[non_exhaustive]
130pub struct FixedVerifyResult {
131 pub verify: VerifyResult,
132 pub repair: Option<RepairResult>,
133}
134
135impl FixedVerifyResult {
136 #[must_use]
137 pub const fn new(verify: VerifyResult, repair: Option<RepairResult>) -> Self {
138 Self { verify, repair }
139 }
140}
141
142#[derive(Clone)]
144pub struct IntegrityChecker {
145 blob_store: BlobStore,
146 docs_engine: DocsEngine,
147}
148
149impl IntegrityChecker {
150 #[must_use]
151 pub const fn new(blob_store: BlobStore, docs_engine: DocsEngine) -> Self {
152 Self {
153 blob_store,
154 docs_engine,
155 }
156 }
157
158 #[must_use]
159 pub const fn blob_store(&self) -> &BlobStore {
160 &self.blob_store
161 }
162
163 #[must_use]
164 pub const fn docs_engine(&self) -> &DocsEngine {
165 &self.docs_engine
166 }
167
168 pub async fn verify_folder(&self, folder: &SyncwebFolder) -> Result<VerifyResult> {
174 let entries = self.docs_engine.list_latest(folder.doc()).await?;
175 let mut result = VerifyResult::default();
176 for entry in entries {
177 if entry.key().starts_with(b"sys/") {
178 continue;
179 }
180 result.total = result.total.saturating_add(1);
181 let path = String::from_utf8(entry.key().to_vec())
182 .map(PathBuf::from)
183 .map_err(|error| SyncwebError::operation("folder entry path is not UTF-8", error))?;
184 let expected = entry.content_hash();
185 if !self.blob_store.has(expected).await? {
186 result.missing.push(path);
187 continue;
188 }
189 let bytes = self.blob_store.get(expected).await?;
190 let actual = Hash::from_bytes(*blake3::hash(&bytes).as_bytes());
191 if actual == expected {
192 result.verified = result.verified.saturating_add(1);
193 } else {
194 result.corrupted.push(CorruptionInfo {
195 path,
196 expected_hash: expected,
197 actual_hash: actual,
198 });
199 }
200 }
201 Ok(result)
202 }
203
204 pub async fn verify_folder_filtered(
212 &self,
213 folder: &SyncwebFolder,
214 filter: Option<&VerifyFilter>,
215 ) -> Result<VerifyResult> {
216 let entries = self.docs_engine.list_latest(folder.doc()).await?;
217 let mut result = VerifyResult::default();
218 for entry in entries {
219 if entry.key().starts_with(b"sys/") {
220 continue;
221 }
222 let expected = entry.content_hash();
223 if filter.is_some_and(|f| !f.matches(entry.key(), &expected)) {
224 continue;
225 }
226 result.total = result.total.saturating_add(1);
227 let path = String::from_utf8(entry.key().to_vec())
228 .map(PathBuf::from)
229 .map_err(|error| SyncwebError::operation("folder entry path is not UTF-8", error))?;
230 if !self.blob_store.has(expected).await? {
231 result.missing.push(path);
232 continue;
233 }
234 let bytes = self.blob_store.get(expected).await?;
235 let actual = Hash::from_bytes(*blake3::hash(&bytes).as_bytes());
236 if actual == expected {
237 result.verified = result.verified.saturating_add(1);
238 } else {
239 result.corrupted.push(CorruptionInfo {
240 path,
241 expected_hash: expected,
242 actual_hash: actual,
243 });
244 }
245 }
246 Ok(result)
247 }
248}