gix_commitgraph/file/
verify.rs1use gix_error::Result;
3use std::{
4 cmp::{max, min},
5 collections::HashMap,
6 path::Path,
7};
8
9use gix_error::{ErrorExt, ResultExt, bail, message};
10
11use crate::{File, GENERATION_NUMBER_INFINITY, GENERATION_NUMBER_MAX, file};
12
13#[derive(Clone, Debug, Eq, PartialEq)]
15#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
16pub struct Outcome {
17 pub max_generation: u32,
19 pub min_generation: u32,
21 pub max_parents: u32,
23 pub num_commits: u32,
25 pub parent_counts: HashMap<u32, u32>,
27}
28
29impl File {
31 pub fn checksum(&self) -> &gix_hash::oid {
33 gix_hash::oid::from_bytes_unchecked(&self.data[self.data.len() - self.hash_len..])
34 }
35
36 pub fn traverse<'a, Processor>(&'a self, mut processor: Processor) -> Result<Outcome>
40 where
41 Processor: FnMut(&file::Commit<'a>) -> Result,
42 {
43 self.verify_checksum()?;
44 verify_split_chain_filename_hash(&self.path, self.checksum())?;
45
46 let null_id = self.object_hash().null_ref();
47
48 let mut stats = Outcome {
49 max_generation: 0,
50 max_parents: 0,
51 min_generation: GENERATION_NUMBER_INFINITY,
52 num_commits: self.num_commits(),
53 parent_counts: HashMap::new(),
54 };
55
56 let mut prev_id: &gix_hash::oid = null_id;
58 for commit in self.iter_commits() {
59 if commit.id() <= prev_id {
60 if commit.id() == null_id {
61 bail!(
62 "commit at file position {} has invalid ID {}".corrupted(),
63 commit.position(),
64 commit.id()
65 );
66 }
67 bail!(
68 "commit at file position {} with ID {} is out of order relative to its predecessor with ID {prev_id}".corrupted(),
69 commit.position(),
70 commit.id()
71 );
72 }
73 if commit.root_tree_id() == null_id {
74 bail!(
75 "commit {} has invalid root tree ID {}".corrupted(),
76 commit.id(),
77 commit.root_tree_id()
78 );
79 }
80 if commit.generation() > GENERATION_NUMBER_MAX {
81 bail!(
82 "commit {} has invalid generation {}".corrupted(),
83 commit.id(),
84 commit.generation()
85 );
86 }
87
88 processor(&commit).or_raise(|| message!("processor failed on commit {}", commit.id()))?;
89
90 stats.max_generation = max(stats.max_generation, commit.generation());
91 stats.min_generation = min(stats.min_generation, commit.generation());
92 let parent_count = commit.iter_parents().try_fold(0u32, |acc, pos| pos.map(|_| acc + 1))?;
93 *stats.parent_counts.entry(parent_count).or_insert(0) += 1;
94 prev_id = commit.id();
95 }
96
97 if stats.min_generation == GENERATION_NUMBER_INFINITY {
98 stats.min_generation = 0;
99 }
100
101 Ok(stats)
102 }
103
104 pub fn verify_checksum(&self) -> Result<gix_hash::ObjectId> {
109 let data_len_without_trailer = self.data.len() - self.hash_len;
110 let mut hasher = gix_hash::hasher(self.object_hash());
111 hasher.update(&self.data[..data_len_without_trailer]);
112 let actual = hasher.try_finalize()?;
113 actual
114 .verify(self.checksum())
115 .or_raise(|| message("commit-graph checksum does not match"))?;
116 Ok(actual)
117 }
118}
119
120fn verify_split_chain_filename_hash(path: &Path, expected: &gix_hash::oid) -> Result {
123 path.file_name()
124 .and_then(std::ffi::OsStr::to_str)
125 .and_then(|filename| filename.strip_suffix(".graph"))
126 .and_then(|stem| stem.strip_prefix("graph-"))
127 .map_or(Ok(()), |hex| match gix_hash::ObjectId::from_hex(hex.as_bytes()) {
128 Ok(actual) if actual == expected => Ok(()),
129 Ok(_) => {
130 Err(message!("commit-graph filename should be graph-{}.graph", expected.to_hex()).corrupted_error())
131 }
132 Err(err) => Err(err
133 .and_raise(message!("commit-graph filename should be graph-{}.graph", expected.to_hex()).corrupted())),
134 })
135}