Skip to main content

gix_commitgraph/file/
verify.rs

1//! Auxiliary types used in commit graph file verification methods.
2use 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/// The positive result of [`File::traverse()`] providing some statistical information.
14#[derive(Clone, Debug, Eq, PartialEq)]
15#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
16pub struct Outcome {
17    /// The largest encountered [`file::Commit`] generation number.
18    pub max_generation: u32,
19    /// The smallest encountered [`file::Commit`] generation number.
20    pub min_generation: u32,
21    /// The largest number of parents in a single [`file::Commit`].
22    pub max_parents: u32,
23    /// The total number of [`commits`][file::Commit]s seen in the iteration.
24    pub num_commits: u32,
25    /// A mapping of `N -> number of commits with N parents`.
26    pub parent_counts: HashMap<u32, u32>,
27}
28
29/// Verification
30impl File {
31    /// Returns the trailing checksum over the entire content of this file.
32    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    /// Traverse all [commits][file::Commit] stored in this file and call `processor(commit) -> Result<(), Error>` on it.
37    ///
38    /// If the `processor` fails, the iteration will be stopped and the entire call results in the respective error.
39    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        // TODO: Verify self.fan values as we go.
57        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    /// Assure the [`checksum`][File::checksum()] matches the actual checksum over all content of this file, excluding the trailing
105    /// checksum itself.
106    ///
107    /// Return the actual checksum on success or [`gix_error::Error`] if there is a mismatch.
108    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
120/// If the given path's filename matches "graph-{hash}.graph", check that `hash` matches the
121/// expected hash.
122fn 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}