Something went wrong. Try again.
A knot backed by enroute.sh forked from anirudh.fi/enroute-knot
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156//! Auxiliary types used in commit graph file verification methods.use std::{ cmp::{max, min}, collections::HashMap, path::Path,};
use gix_error::{ErrorExt, Exn, Message, ResultExt, message};
use crate::{File, GENERATION_NUMBER_INFINITY, GENERATION_NUMBER_MAX, file};
/// The positive result of [`File::traverse()`] providing some statistical information.#[derive(Clone, Debug, Eq, PartialEq)]#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]pub struct Outcome { /// The largest encountered [`file::Commit`] generation number. pub max_generation: u32, /// The smallest encountered [`file::Commit`] generation number. pub min_generation: u32, /// The largest number of parents in a single [`file::Commit`]. pub max_parents: u32, /// The total number of [`commits`][file::Commit]s seen in the iteration. pub num_commits: u32, /// A mapping of `N -> number of commits with N parents`. pub parent_counts: HashMap<u32, u32>,}
/// Verificationimpl File { /// Returns the trailing checksum over the entire content of this file. pub fn checksum(&self) -> &gix_hash::oid { gix_hash::oid::from_bytes_unchecked(&self.data[self.data.len() - self.hash_len..]) }
/// Traverse all [commits][file::Commit] stored in this file and call `processor(commit) -> Result<(), Error>` on it. /// /// If the `processor` fails, the iteration will be stopped and the entire call results in the respective error. pub fn traverse<'a, Processor>( &'a self, mut processor: Processor, ) -> Result<Outcome, Exn<Message>> where Processor: FnMut(&file::Commit<'a>) -> Result<(), Exn>, { self.verify_checksum()?; verify_split_chain_filename_hash(&self.path, self.checksum())?;
let null_id = self.object_hash().null_ref();
let mut stats = Outcome { max_generation: 0, max_parents: 0, min_generation: GENERATION_NUMBER_INFINITY, num_commits: self.num_commits(), parent_counts: HashMap::new(), };
// TODO: Verify self.fan values as we go. let mut prev_id: &gix_hash::oid = null_id; for commit in self.iter_commits() { if commit.id() <= prev_id { if commit.id() == null_id { return Err(message!( "commit at file position {} has invalid ID {}", commit.position(), commit.id() ) .raise()); } return Err(message!( "commit at file position {} with ID {} is out of order relative to its predecessor with ID {prev_id}", commit.position(), commit.id() ) .raise()); } if commit.root_tree_id() == null_id { return Err(message!( "commit {} has invalid root tree ID {}", commit.id(), commit.root_tree_id() ) .raise()); } if commit.generation() > GENERATION_NUMBER_MAX { return Err(message!( "commit {} has invalid generation {}", commit.id(), commit.generation() ) .raise()); }
processor(&commit) .or_raise(|| message!("processor failed on commit {}", commit.id()))?;
stats.max_generation = max(stats.max_generation, commit.generation()); stats.min_generation = min(stats.min_generation, commit.generation()); let parent_count = commit .iter_parents() .try_fold(0u32, |acc, pos| pos.map(|_| acc + 1))?; *stats.parent_counts.entry(parent_count).or_insert(0) += 1; prev_id = commit.id(); }
if stats.min_generation == GENERATION_NUMBER_INFINITY { stats.min_generation = 0; }
Ok(stats) }
/// Assure the [`checksum`][File::checksum()] matches the actual checksum over all content of this file, excluding the trailing /// checksum itself. /// /// Return the actual checksum on success or [`Exn<Message>`] if there is a mismatch. pub fn verify_checksum(&self) -> Result<gix_hash::ObjectId, Exn<Message>> { // Even though we could use gix_hash::bytes_of_file(…), this would require extending our // Error type to support io::Error. As we only gain progress, there probably isn't much value // as these files are usually small enough to process them in less than a second, even for the large ones. // But it's possible, once a progress instance is passed. let data_len_without_trailer = self.data.len() - self.hash_len; let mut hasher = gix_hash::hasher(self.object_hash()); hasher.update(&self.data[..data_len_without_trailer]); let actual = hasher .try_finalize() .map_err(|e| message!("failed to hash commit graph file: {e}").raise())?; actual .verify(self.checksum()) .map_err(|e| message!("{e}").raise())?; Ok(actual) }}
/// If the given path's filename matches "graph-{hash}.graph", check that `hash` matches the/// expected hash.fn verify_split_chain_filename_hash( path: &Path, expected: &gix_hash::oid,) -> Result<(), Exn<Message>> { path.file_name() .and_then(std::ffi::OsStr::to_str) .and_then(|filename| filename.strip_suffix(".graph")) .and_then(|stem| stem.strip_prefix("graph-")) .map_or(Ok(()), |hex| { match gix_hash::ObjectId::from_hex(hex.as_bytes()) { Ok(actual) if actual == expected => Ok(()), _ => Err(message!( "commit-graph filename should be graph-{}.graph", expected.to_hex() ) .raise()), } })}