Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269use std::path::{Path, PathBuf};
use gix_error::{ErrorExt, Exn, Message, ResultExt, message};
use crate::{ BloomFilterSettings, File, file::{ BASE_GRAPHS_LIST_CHUNK_ID, BLOOM_FILTER_DATA_CHUNK_ID, BLOOM_FILTER_HEADER_SIZE, BLOOM_FILTER_INDEX_CHUNK_ID, COMMIT_DATA_CHUNK_ID, COMMIT_DATA_ENTRY_SIZE_SANS_HASH, EXTENDED_EDGES_LIST_CHUNK_ID, FAN_LEN, GENERATION_DATA_CHUNK_ID, GENERATION_DATA_OVERFLOW_CHUNK_ID, HEADER_LEN, OID_FAN_CHUNK_ID, OID_LOOKUP_CHUNK_ID, SIGNATURE, }, from_be_u32,};
const MIN_FILE_SIZE: usize = HEADER_LEN + gix_chunk::file::Index::size_for_entries(3 /*OIDF, OIDL, CDAT*/) + FAN_LEN * 4 /* FANOUT TABLE CHUNK OIDF */ + gix_hash::Kind::shortest().len_in_bytes();
impl File { /// Try to parse the commit graph file at `path`. pub fn at(path: impl AsRef<Path>) -> Result<File, Exn<Message>> { Self::try_from(path.as_ref()) }
/// A lower-level constructor which constructs a new instance directly from the mapping in `data`, /// assuming that it originated from `path`. /// /// Note that `path` is only used for verification of the hash its basename contains, but otherwise /// is not of importance. pub fn new(data: memmap2::Mmap, path: PathBuf) -> Result<File, Exn<Message>> { let data_size = data.len(); if data_size < MIN_FILE_SIZE { return Err(message("Commit-graph file too small even for an empty graph").raise()); }
let mut ofs = 0; if &data[ofs..ofs + SIGNATURE.len()] != SIGNATURE { return Err( message("Commit-graph file does not start with expected signature").raise(), ); } ofs += SIGNATURE.len();
match data[ofs] { 1 => (), x => { return Err(message!("Unsupported commit-graph file version: {x}").raise()); } } ofs += 1;
let object_hash = gix_hash::Kind::try_from(data[ofs]).map_err(|v| { message!("Commit-graph file uses unsupported hash version: {v}").raise() })?; ofs += 1;
let chunk_count = data[ofs]; // Can assert chunk_count >= MIN_CHUNKS here, but later OIDF+OIDL+CDAT presence checks make // it redundant. ofs += 1;
let base_graph_count = data[ofs]; ofs += 1;
let chunks = gix_chunk::file::Index::from_bytes(&data, ofs, u32::from(chunk_count)) .or_raise(|| { message!( "Couldn't read commit-graph file with {chunk_count} chunks at offset {ofs}" ) })?;
let base_graphs_list_offset = chunks .validated_usize_offset_by_id(BASE_GRAPHS_LIST_CHUNK_ID, |chunk_range| { let chunk_size = chunk_range.len(); if chunk_size % object_hash.len_in_bytes() != 0 { return Err(message!("Commit-graph chunk {BASE_GRAPHS_LIST_CHUNK_ID:?} has invalid size: {msg}", msg = format!( "chunk size {} is not a multiple of {}", chunk_size, object_hash.len_in_bytes() ), ).raise()); } let chunk_base_graph_count: u32 = (chunk_size / object_hash.len_in_bytes()) .try_into() .expect("base graph count to fit in 32-bits"); if chunk_base_graph_count != u32::from(base_graph_count) { return Err(message!("Commit-graph {BASE_GRAPHS_LIST_CHUNK_ID:?} chunk contains {chunk_base_graph_count} base graphs, but commit-graph file header claims {base_graph_count} base graphs").raise()) } Ok(chunk_range.start) }) .ok() .transpose()?;
let (commit_data_offset, commit_data_count): (_, u32) = chunks .validated_usize_offset_by_id(COMMIT_DATA_CHUNK_ID, |chunk_range| { let chunk_size = chunk_range.len();
let entry_size = object_hash.len_in_bytes() + COMMIT_DATA_ENTRY_SIZE_SANS_HASH; if chunk_size % entry_size != 0 { return Err(message!("Commit-graph chunk {COMMIT_DATA_CHUNK_ID:?} has invalid size: chunk size {chunk_size} is not a multiple of {entry_size}").raise()) } Ok(( chunk_range.start, (chunk_size / entry_size) .try_into() .expect("number of commits in CDAT chunk to fit in 32 bits"), )) })??;
let fan_offset = chunks .validated_usize_offset_by_id(OID_FAN_CHUNK_ID, |chunk_range| { let chunk_size = chunk_range.len();
let expected_size = 4 * FAN_LEN; if chunk_size != expected_size { return Err(message!("Commit-graph chunk {OID_FAN_CHUNK_ID:?} has invalid size: expected chunk length {expected_size}, got {chunk_size}").raise()) } Ok(chunk_range.start) })? .or_raise(|| message("Error getting offset for OID fan chunk"))?;
let (oid_lookup_offset, oid_lookup_count): (_, u32) = chunks .validated_usize_offset_by_id(OID_LOOKUP_CHUNK_ID, |chunk_range| { let chunk_size = chunk_range.len();
if chunk_size % object_hash.len_in_bytes() != 0 { return Err(message!("Commit-graph chunk {OID_LOOKUP_CHUNK_ID:?} has invalid size: chunk size {chunk_size} is not a multiple of {hash_len}", hash_len = object_hash.len_in_bytes()).raise()) } Ok(( chunk_range.start, (chunk_size / object_hash.len_in_bytes()) .try_into() .expect("number of commits in OIDL chunk to fit in 32 bits"), )) })? .or_raise(|| message("Error getting offset for OID lookup chunk"))?;
let extra_edges_list_range = chunks.usize_offset_by_id(EXTENDED_EDGES_LIST_CHUNK_ID).ok(); // A wrongly sized generation data chunk is ignored instead of rejecting the file. let generation_data_offset = chunks .usize_offset_by_id(GENERATION_DATA_CHUNK_ID) .ok() .filter(|range| range.len() / 4 == commit_data_count as usize) .map(|range| range.start); // The overflow chunk is only checked when reading from it. let generation_data_overflow_range = chunks .usize_offset_by_id(GENERATION_DATA_OVERFLOW_CHUNK_ID) .ok(); let bloom_filter_index_range = chunks.usize_offset_by_id(BLOOM_FILTER_INDEX_CHUNK_ID).ok(); let bloom_filter_data_range = chunks.usize_offset_by_id(BLOOM_FILTER_DATA_CHUNK_ID).ok();
let mut bloom_filter_data_len = 0usize; let mut bloom_filter_data_offset = None; let mut bloom_filter_index_offset = None; let mut bloom_filter_settings = None; if let (Some(index_range), Some(data_range)) = ( bloom_filter_index_range.as_ref(), bloom_filter_data_range.as_ref(), ) { let expected_index_chunk_size = commit_data_count as usize * std::mem::size_of::<u32>(); if index_range.len() == expected_index_chunk_size && data_range.len() >= BLOOM_FILTER_HEADER_SIZE { let settings = BloomFilterSettings { hash_version: from_be_u32(&data[data_range.start..][..4]), num_hashes: from_be_u32(&data[data_range.start + 4..][..4]), bits_per_entry: from_be_u32(&data[data_range.start + 8..][..4]), }; let bloom_data_payload_len = data_range.len() - BLOOM_FILTER_HEADER_SIZE; if settings.is_supported() { bloom_filter_settings = Some(settings); bloom_filter_data_len = bloom_data_payload_len; bloom_filter_data_offset = Some(data_range.start + BLOOM_FILTER_HEADER_SIZE); bloom_filter_index_offset = Some(index_range.start); } } }
let trailer = &data[chunks.highest_offset() as usize..]; if trailer.len() != object_hash.len_in_bytes() { return Err(message!( "Expected commit-graph trailer to contain {} bytes, got {}", object_hash.len_in_bytes(), trailer.len() ) .raise()); }
if base_graph_count > 0 && base_graphs_list_offset.is_none() { return Err(message!( "Chunk named {BASE_GRAPHS_LIST_CHUNK_ID:?} was not found in chunk file index" ) .into()); }
let (fan, _) = read_fan(&data[fan_offset..]); if oid_lookup_count != fan[255] { return Err(message!("Commit-graph {OID_FAN_CHUNK_ID:?} chunk contains {chunk1_commits} commits, but {OID_LOOKUP_CHUNK_ID:?} chunk contains {chunk2_commits} commits", chunk1_commits = fan[255], chunk2_commits = oid_lookup_count, ).raise()); } if commit_data_count != fan[255] { return Err( message!("Commit-graph {OID_FAN_CHUNK_ID:?} chunk contains {chunk1_commits} commits, but {COMMIT_DATA_CHUNK_ID:?} chunk contains {chunk2_commits} commits", chunk1_commits = fan[255], chunk2_commits = commit_data_count, ).raise(), ); } Ok(File { base_graph_count, base_graphs_list_offset, bloom_filter_data_len, bloom_filter_data_offset, bloom_filter_index_offset, bloom_filter_settings, commit_data_offset, data, extra_edges_list_range, fan, generation_data_offset, generation_data_overflow_range, read_generation_data: generation_data_offset.is_some(), oid_lookup_offset, path, hash_len: object_hash.len_in_bytes(), object_hash, }) }}
impl TryFrom<&Path> for File { type Error = Exn<Message>;
fn try_from(path: &Path) -> Result<Self, Self::Error> { let data = std::fs::File::open(path) .and_then(|file| { // SAFETY: we have to take the risk of somebody changing the file underneath. Git never writes into the same file. #[allow(unsafe_code)] unsafe { memmap2::MmapOptions::new().map_copy_read_only(&file) } }) .or_raise(|| { message!( "Could not open commit-graph file at '{path}'", path = path.display() ) })?; Self::new(data, path.to_owned()) }}
// Copied from gix-odb/pack/index/init.rsfn read_fan(d: &[u8]) -> ([u32; FAN_LEN], usize) { assert!(d.len() >= FAN_LEN * 4);
let mut fan = [0; FAN_LEN]; for (c, f) in d.chunks_exact(4).zip(fan.iter_mut()) { *f = u32::from_be_bytes(c.try_into().unwrap()); } (fan, FAN_LEN * 4)}