1use std::collections::HashMap;
4use std::io::Read;
5use std::path::{Path, PathBuf};
6
7use crate::apply::MetadataOptions;
8use crate::control::RunControl;
9use crate::index::RIPSYNC_DIR;
10use crate::meta::{FileTypeKind, meta_min};
11use crate::plan::{Action, SyncPlan};
12use crate::report::{Event, Reporter, RunPhase};
13use crate::walk::{Entry, EntryKind, walk_controlled};
14use crate::{Error, Result};
15
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18pub enum VerifyMode {
19 #[default]
21 None,
22 Changed,
24 All,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct VerificationMismatch {
31 pub rel: PathBuf,
33 pub detail: String,
35}
36
37#[derive(Debug, Clone, Default, PartialEq, Eq)]
39pub struct VerificationSummary {
40 pub checked: usize,
42 pub mismatches: Vec<VerificationMismatch>,
44}
45
46const MMAP_HASH_THRESHOLD: u64 = 16 * 1024 * 1024;
50
51fn hash_file(path: &Path) -> Result<[u8; 32]> {
52 let mut hasher = blake3::Hasher::new();
53 let len = std::fs::metadata(path)
54 .map_err(|error| Error::io(path, error))?
55 .len();
56
57 if len >= MMAP_HASH_THRESHOLD {
58 hasher
60 .update_mmap_rayon(path)
61 .map_err(|error| Error::io(path, error))?;
62 return Ok(*hasher.finalize().as_bytes());
63 }
64
65 let mut file = std::fs::File::open(path).map_err(|error| Error::io(path, error))?;
66 let mut buffer = vec![0; 1024 * 1024];
67 loop {
68 let read = file
69 .read(&mut buffer)
70 .map_err(|error| Error::io(path, error))?;
71 if read == 0 {
72 return Ok(*hasher.finalize().as_bytes());
73 }
74 hasher.update(&buffer[..read]);
75 }
76}
77
78fn mismatch(rel: &Path, detail: impl Into<String>) -> VerificationMismatch {
79 VerificationMismatch {
80 rel: rel.to_path_buf(),
81 detail: detail.into(),
82 }
83}
84
85fn live_entry(root: &Path, rel: &Path) -> Result<Entry> {
86 let path = root.join(rel);
87 let metadata = meta_min(&path)?;
88 let kind = match metadata.kind {
89 FileTypeKind::File => EntryKind::File,
90 FileTypeKind::Dir => EntryKind::Dir,
91 FileTypeKind::Symlink => {
92 EntryKind::Symlink(std::fs::read_link(&path).map_err(|error| Error::io(&path, error))?)
93 }
94 FileTypeKind::Other => {
95 return Err(Error::io(
96 &path,
97 std::io::Error::other("unsupported file kind"),
98 ));
99 }
100 };
101 Ok(Entry {
102 rel: rel.to_path_buf(),
103 len: if matches!(kind, EntryKind::File) {
104 metadata.len
105 } else {
106 0
107 },
108 kind,
109 mtime: metadata.mtime,
110 mode: metadata.mode,
111 ino: metadata.ino,
112 dev: metadata.dev,
113 uid: metadata.uid,
114 gid: metadata.gid,
115 })
116}
117
118fn compare_entry(
119 source: &Entry,
120 destination: Option<&Entry>,
121 src: &Path,
122 dst: &Path,
123 metadata: MetadataOptions,
124) -> Result<Option<VerificationMismatch>> {
125 let Some(destination) = destination else {
126 return Ok(Some(mismatch(&source.rel, "missing from destination")));
127 };
128 if std::mem::discriminant(&source.kind) != std::mem::discriminant(&destination.kind) {
129 return Ok(Some(mismatch(&source.rel, "file kind differs")));
130 }
131 if source.mode & 0o7777 != destination.mode & 0o7777 {
132 return Ok(Some(mismatch(&source.rel, "mode differs")));
133 }
134 if source.mtime != destination.mtime {
135 return Ok(Some(mismatch(&source.rel, "mtime differs")));
136 }
137 if metadata.owner && source.uid != destination.uid {
138 return Ok(Some(mismatch(&source.rel, "uid differs")));
139 }
140 if metadata.group && source.gid != destination.gid {
141 return Ok(Some(mismatch(&source.rel, "gid differs")));
142 }
143 match (&source.kind, &destination.kind) {
144 (EntryKind::File, EntryKind::File) => {
145 if source.len != destination.len
146 || hash_file(&src.join(&source.rel))? != hash_file(&dst.join(&source.rel))?
147 {
148 return Ok(Some(mismatch(&source.rel, "content differs")));
149 }
150 if metadata.sparse
151 && sparse_blocks(&src.join(&source.rel)) != sparse_blocks(&dst.join(&source.rel))
152 {
153 return Ok(Some(mismatch(&source.rel, "sparse allocation differs")));
154 }
155 }
156 (EntryKind::Symlink(a), EntryKind::Symlink(b)) if a != b => {
157 return Ok(Some(mismatch(&source.rel, "symlink target differs")));
158 }
159 _ => {}
160 }
161 if (metadata.xattrs || metadata.acls)
162 && extended_attributes(&src.join(&source.rel), metadata)
163 != extended_attributes(&dst.join(&source.rel), metadata)
164 {
165 return Ok(Some(mismatch(&source.rel, "extended attributes differ")));
166 }
167 Ok(None)
168}
169
170#[cfg(unix)]
171fn sparse_blocks(path: &Path) -> Option<u64> {
172 use std::os::unix::fs::MetadataExt;
173 std::fs::symlink_metadata(path)
174 .ok()
175 .map(|metadata| metadata.blocks())
176}
177
178#[cfg(not(unix))]
179fn sparse_blocks(_path: &Path) -> Option<u64> {
180 None
181}
182
183#[cfg(unix)]
184fn extended_attributes(path: &Path, metadata: MetadataOptions) -> Option<Vec<(String, Vec<u8>)>> {
185 let mut attributes = Vec::new();
186 for name in xattr::list(path).ok()? {
187 let text = name.to_string_lossy();
188 let is_acl = text.starts_with("system.posix_acl_");
189 if (is_acl && !metadata.acls) || (!is_acl && !metadata.xattrs) {
190 continue;
191 }
192 attributes.push((
193 text.into_owned(),
194 xattr::get(path, &name).ok().flatten().unwrap_or_default(),
195 ));
196 }
197 attributes.sort_unstable();
198 Some(attributes)
199}
200
201#[cfg(not(unix))]
202fn extended_attributes(_path: &Path, _metadata: MetadataOptions) -> Option<Vec<(String, Vec<u8>)>> {
203 None
204}
205
206#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
212pub fn verify<R: Reporter>(
213 plan: &SyncPlan,
214 src: &Path,
215 dst: &Path,
216 mode: VerifyMode,
217 metadata: MetadataOptions,
218 threads: usize,
219 control: &RunControl,
220 reporter: &R,
221) -> Result<VerificationSummary> {
222 if mode == VerifyMode::None {
223 return Ok(VerificationSummary::default());
224 }
225 reporter.event(Event::Phase(RunPhase::Verifying));
226 control.checkpoint()?;
227 let filter = crate::filter::Filter::none();
228 let source_entries = if mode == VerifyMode::All {
229 walk_controlled(src, threads, &filter, control)?
230 } else {
231 Vec::new()
232 };
233 let mut destination_entries = if mode == VerifyMode::All {
234 walk_controlled(dst, threads, &filter, control)?
235 } else {
236 plan.actions
237 .iter()
238 .filter(|planned| metadata.hard_links || planned.action != Action::Skip)
239 .filter_map(|planned| live_entry(dst, &planned.entry.rel).ok())
240 .collect()
241 };
242 destination_entries.retain(|entry| !entry.rel.starts_with(RIPSYNC_DIR));
243 let destination: HashMap<&Path, &Entry> = destination_entries
244 .iter()
245 .map(|entry| (entry.rel.as_path(), entry))
246 .collect();
247 let selected: Vec<&Entry> = match mode {
248 VerifyMode::None => Vec::new(),
249 VerifyMode::Changed => plan
250 .actions
251 .iter()
252 .filter(|planned| planned.action != Action::Skip)
253 .map(|planned| &planned.entry)
254 .collect(),
255 VerifyMode::All => source_entries.iter().collect(),
256 };
257 let total = selected.len();
258 let mut summary = VerificationSummary::default();
259 for entry in &selected {
260 control.checkpoint()?;
261 summary.checked += 1;
262 if let Some(problem) = compare_entry(
263 entry,
264 destination.get(entry.rel.as_path()).copied(),
265 src,
266 dst,
267 metadata,
268 )? {
269 reporter.event(Event::VerificationFailed {
270 rel: problem.rel.clone(),
271 detail: problem.detail.clone(),
272 });
273 summary.mismatches.push(problem);
274 }
275 reporter.event(Event::VerificationProgress {
276 checked: summary.checked,
277 total,
278 mismatches: summary.mismatches.len(),
279 });
280 }
281 if metadata.hard_links {
282 let mut groups: HashMap<(u64, u64), (u64, u64)> = HashMap::new();
283 let hardlink_entries: Vec<&Entry> = if mode == VerifyMode::Changed {
284 plan.actions.iter().map(|planned| &planned.entry).collect()
285 } else {
286 selected.clone()
287 };
288 for entry in hardlink_entries {
289 if !entry.is_file() {
290 continue;
291 }
292 let Some(destination) = destination.get(entry.rel.as_path()) else {
293 continue;
294 };
295 let source_id = (entry.dev, entry.ino);
296 let destination_id = (destination.dev, destination.ino);
297 let expected = groups.entry(source_id).or_insert(destination_id);
298 if *expected != destination_id {
299 let problem = mismatch(&entry.rel, "hardlink identity differs");
300 reporter.event(Event::VerificationFailed {
301 rel: problem.rel.clone(),
302 detail: problem.detail.clone(),
303 });
304 summary.mismatches.push(problem);
305 }
306 }
307 }
308 if mode == VerifyMode::All {
309 let source: std::collections::HashSet<&Path> = source_entries
310 .iter()
311 .map(|entry| entry.rel.as_path())
312 .collect();
313 for entry in destination_entries {
314 control.checkpoint()?;
315 if !source.contains(entry.rel.as_path()) {
316 let problem = mismatch(&entry.rel, "extra destination entry");
317 reporter.event(Event::VerificationFailed {
318 rel: problem.rel.clone(),
319 detail: problem.detail.clone(),
320 });
321 summary.mismatches.push(problem);
322 }
323 }
324 }
325 Ok(summary)
326}