1use std::{cmp::Ordering, sync::atomic::AtomicBool, time::Instant};
2
3use gix_features::progress::{Count, DynNestedProgress, Progress};
4
5use crate::{exact_vec, index, multi_index::File};
6
7pub mod integrity {
9 use crate::multi_index::EntryIndex;
10
11 #[derive(thiserror::Error, Debug)]
13 #[expect(missing_docs)]
14 pub enum Error {
15 #[error("Object {id} should be at pack-offset {expected_pack_offset} but was found at {actual_pack_offset}")]
16 PackOffsetMismatch {
17 id: gix_hash::ObjectId,
18 expected_pack_offset: u64,
19 actual_pack_offset: u64,
20 },
21 #[error(transparent)]
22 MultiIndexChecksum(#[from] crate::multi_index::verify::checksum::Error),
23 #[error(transparent)]
24 IndexIntegrity(#[from] crate::index::verify::integrity::Error),
25 #[error(transparent)]
26 BundleInit(#[from] crate::bundle::init::Error),
27 #[error("Counted {actual} objects, but expected {expected} as per multi-index")]
28 UnexpectedObjectCount { actual: usize, expected: usize },
29 #[error("{id} wasn't found in the index referenced in the multi-pack index")]
30 OidNotFound { id: gix_hash::ObjectId },
31 #[error("The object id at multi-index entry {index} wasn't in order")]
32 OutOfOrder { index: EntryIndex },
33 #[error("The fan at index {index} is out of order as it's larger then the following value.")]
34 Fan { index: usize },
35 #[error("The multi-index claims to have no objects")]
36 Empty,
37 #[error("The multi-index path '{path}' has no parent directory")]
38 InvalidPath { path: std::path::PathBuf },
39 #[error("Interrupted")]
40 Interrupted,
41 }
42
43 pub struct Outcome {
45 pub actual_index_checksum: gix_hash::ObjectId,
47 pub pack_traverse_statistics: Vec<crate::index::traverse::Statistics>,
49 }
50
51 #[derive(Debug, Copy, Clone)]
55 pub enum ProgressId {
56 ChecksumBytes,
58 ObjectOffsets,
60 }
61
62 impl From<ProgressId> for gix_features::progress::Id {
63 fn from(v: ProgressId) -> Self {
64 match v {
65 ProgressId::ChecksumBytes => *b"MVCK",
66 ProgressId::ObjectOffsets => *b"MVOF",
67 }
68 }
69 }
70}
71
72pub mod checksum {
74 pub type Error = crate::verify::checksum::Error;
76}
77
78impl<T> File<T>
79where
80 T: crate::FileData,
81{
82 pub fn verify_checksum(
85 &self,
86 progress: &mut dyn Progress,
87 should_interrupt: &AtomicBool,
88 ) -> Result<gix_hash::ObjectId, checksum::Error> {
89 crate::verify::checksum_on_disk_or_mmap(
90 self.path(),
91 &self.data,
92 self.checksum(),
93 self.object_hash,
94 progress,
95 should_interrupt,
96 )
97 }
98
99 pub fn verify_integrity_fast(
103 &self,
104 progress: &mut dyn DynNestedProgress,
105 should_interrupt: &AtomicBool,
106 ) -> Result<gix_hash::ObjectId, integrity::Error> {
107 self.verify_integrity_inner(
108 progress,
109 should_interrupt,
110 false,
111 index::verify::integrity::Options::default(),
112 )
113 .map_err(|err| match err {
114 index::traverse::Error::Processor(err) => err,
115 _ => unreachable!("BUG: no other error type is possible"),
116 })
117 .map(|o| o.actual_index_checksum)
118 }
119
120 pub fn verify_integrity<C, F>(
124 &self,
125 progress: &mut dyn DynNestedProgress,
126 should_interrupt: &AtomicBool,
127 options: index::verify::integrity::Options<F>,
128 ) -> Result<integrity::Outcome, index::traverse::Error<integrity::Error>>
129 where
130 C: crate::cache::DecodeEntry,
131 F: Fn() -> C + Send + Clone,
132 {
133 self.verify_integrity_inner(progress, should_interrupt, true, options)
134 }
135
136 fn verify_integrity_inner<C, F>(
137 &self,
138 progress: &mut dyn DynNestedProgress,
139 should_interrupt: &AtomicBool,
140 deep_check: bool,
141 options: index::verify::integrity::Options<F>,
142 ) -> Result<integrity::Outcome, index::traverse::Error<integrity::Error>>
143 where
144 C: crate::cache::DecodeEntry,
145 F: Fn() -> C + Send + Clone,
146 {
147 let parent = self.path.parent().ok_or_else(|| {
148 index::traverse::Error::Processor(integrity::Error::InvalidPath {
149 path: self.path.clone(),
150 })
151 })?;
152
153 let actual_index_checksum = self
154 .verify_checksum(
155 &mut progress.add_child_with_id(
156 format!("{}: checksum", self.path.display()),
157 integrity::ProgressId::ChecksumBytes.into(),
158 ),
159 should_interrupt,
160 )
161 .map_err(integrity::Error::from)
162 .map_err(index::traverse::Error::Processor)?;
163
164 if let Some(first_invalid) = crate::verify::fan(&self.fan) {
165 return Err(index::traverse::Error::Processor(integrity::Error::Fan {
166 index: first_invalid,
167 }));
168 }
169
170 if self.num_objects == 0 {
171 return Err(index::traverse::Error::Processor(integrity::Error::Empty));
172 }
173
174 let mut pack_traverse_statistics = Vec::new();
175
176 let operation_start = Instant::now();
177 let mut total_objects_checked = 0;
178 let mut pack_ids_and_offsets = exact_vec(self.num_objects as usize);
179 {
180 let order_start = Instant::now();
181 let mut progress = progress.add_child_with_id("checking oid order".into(), gix_features::progress::UNKNOWN);
182 progress.init(
183 Some(self.num_objects as usize),
184 gix_features::progress::count("objects"),
185 );
186
187 for entry_index in 0..(self.num_objects - 1) {
188 let lhs = self.oid_at_index(entry_index);
189 let rhs = self.oid_at_index(entry_index + 1);
190
191 if rhs.cmp(lhs) != Ordering::Greater {
192 return Err(index::traverse::Error::Processor(integrity::Error::OutOfOrder {
193 index: entry_index,
194 }));
195 }
196 let (pack_id, _) = self.pack_id_and_pack_offset_at_index(entry_index);
197 pack_ids_and_offsets.push((pack_id, entry_index));
198 progress.inc();
199 }
200 {
201 let entry_index = self.num_objects - 1;
202 let (pack_id, _) = self.pack_id_and_pack_offset_at_index(entry_index);
203 pack_ids_and_offsets.push((pack_id, entry_index));
204 }
205 pack_ids_and_offsets.sort_by_key(|l| l.0);
207 progress.show_throughput(order_start);
208 };
209
210 progress.init(
211 Some(self.num_indices as usize),
212 gix_features::progress::count("indices"),
213 );
214
215 let mut pack_ids_slice = pack_ids_and_offsets.as_slice();
216
217 for (pack_id, index_file_name) in self.index_names.iter().enumerate() {
218 progress.set_name(index_file_name.display().to_string());
219 progress.inc();
220
221 let mut bundle = None;
222 let index;
223 let index_path = parent.join(index_file_name);
224 let index = if deep_check {
225 let mut opened_bundle = crate::Bundle::at(index_path, self.object_hash)
226 .map_err(integrity::Error::from)
227 .map_err(index::traverse::Error::Processor)?;
228 opened_bundle.pack.alloc_limit_bytes = self.alloc_limit_bytes;
229 bundle = Some(opened_bundle);
230 bundle.as_ref().map(|b| &b.index).expect("just set")
231 } else {
232 index = Some(
233 index::File::at(index_path, self.object_hash)
234 .map_err(|err| integrity::Error::BundleInit(crate::bundle::init::Error::Index(err)))
235 .map_err(index::traverse::Error::Processor)?,
236 );
237 index.as_ref().expect("just set")
238 };
239
240 let slice_end = pack_ids_slice.partition_point(|e| e.0 == pack_id as crate::data::Id);
241 let multi_index_entries_to_check = &pack_ids_slice[..slice_end];
242 {
243 let offset_start = Instant::now();
244 let mut offsets_progress = progress.add_child_with_id(
245 "verify object offsets".into(),
246 integrity::ProgressId::ObjectOffsets.into(),
247 );
248 offsets_progress.init(
249 Some(pack_ids_and_offsets.len()),
250 gix_features::progress::count("objects"),
251 );
252 pack_ids_slice = &pack_ids_slice[slice_end..];
253
254 for entry_id in multi_index_entries_to_check.iter().map(|e| e.1) {
255 let oid = self.oid_at_index(entry_id);
256 let (_, expected_pack_offset) = self.pack_id_and_pack_offset_at_index(entry_id);
257 let entry_in_bundle_index = index.lookup(oid).ok_or_else(|| {
258 index::traverse::Error::Processor(integrity::Error::OidNotFound { id: oid.to_owned() })
259 })?;
260 let actual_pack_offset = index.pack_offset_at_index(entry_in_bundle_index);
261 if actual_pack_offset != expected_pack_offset {
262 return Err(index::traverse::Error::Processor(
263 integrity::Error::PackOffsetMismatch {
264 id: oid.to_owned(),
265 expected_pack_offset,
266 actual_pack_offset,
267 },
268 ));
269 }
270 offsets_progress.inc();
271 }
272
273 if should_interrupt.load(std::sync::atomic::Ordering::Relaxed) {
274 return Err(index::traverse::Error::Processor(integrity::Error::Interrupted));
275 }
276 offsets_progress.show_throughput(offset_start);
277 }
278
279 total_objects_checked += multi_index_entries_to_check.len();
280
281 if let Some(bundle) = bundle {
282 progress.set_name(format!("Validating {}", index_file_name.display()));
283 let crate::bundle::verify::integrity::Outcome {
284 actual_index_checksum: _,
285 pack_traverse_outcome,
286 } = bundle
287 .verify_integrity(progress, should_interrupt, options.clone())
288 .map_err(|err| {
289 use index::traverse::Error::*;
290 match err {
291 Processor(err) => Processor(integrity::Error::IndexIntegrity(err)),
292 IndexVerify(err) => IndexVerify(err),
293 Tree(err) => Tree(err),
294 TreeTraversal(err) => TreeTraversal(err),
295 PackVerify(err) => PackVerify(err),
296 PackDecode { id, offset, source } => PackDecode { id, offset, source },
297 PackMismatch(err) => PackMismatch(err),
298 EntryType(err) => EntryType(err),
299 PackObjectVerify { offset, source } => PackObjectVerify { offset, source },
300 Crc32Mismatch {
301 expected,
302 actual,
303 offset,
304 kind,
305 } => Crc32Mismatch {
306 expected,
307 actual,
308 offset,
309 kind,
310 },
311 Interrupted => Interrupted,
312 }
313 })?;
314 pack_traverse_statistics.push(pack_traverse_outcome);
315 }
316 }
317
318 assert_eq!(
319 self.num_objects as usize, total_objects_checked,
320 "BUG: our slicing should allow to visit all objects"
321 );
322
323 progress.set_name("Validating multi-pack".into());
324 progress.show_throughput(operation_start);
325
326 Ok(integrity::Outcome {
327 actual_index_checksum,
328 pack_traverse_statistics,
329 })
330 }
331}