gix_commitgraph/file/
init.rs1use gix_error::Result;
2use std::path::{Path, PathBuf};
3
4use gix_error::{ResultExt, bail, corruption, message};
5
6use crate::{
7 File,
8 file::{
9 BASE_GRAPHS_LIST_CHUNK_ID, COMMIT_DATA_CHUNK_ID, COMMIT_DATA_ENTRY_SIZE_SANS_HASH,
10 EXTENDED_EDGES_LIST_CHUNK_ID, FAN_LEN, HEADER_LEN, OID_FAN_CHUNK_ID, OID_LOOKUP_CHUNK_ID, SIGNATURE,
11 },
12};
13
14const MIN_FILE_SIZE: usize = HEADER_LEN
15 + gix_chunk::file::Index::size_for_entries(3 )
16 + FAN_LEN * 4 + gix_hash::Kind::shortest().len_in_bytes();
18
19impl File {
20 pub fn at(path: impl AsRef<Path>) -> Result<File> {
22 Self::try_from(path.as_ref())
23 }
24
25 pub fn new(data: memmap2::Mmap, path: PathBuf) -> Result<File> {
31 let data_size = data.len();
32 if data_size < MIN_FILE_SIZE {
33 bail!(corruption("Commit-graph file too small even for an empty graph"));
34 }
35
36 let mut ofs = 0;
37 if &data[ofs..ofs + SIGNATURE.len()] != SIGNATURE {
38 bail!(corruption("Commit-graph file does not start with expected signature"));
39 }
40 ofs += SIGNATURE.len();
41
42 match data[ofs] {
43 1 => (),
44 x => {
45 bail!("Unsupported commit-graph file version: {x}".unsupported());
46 }
47 }
48 ofs += 1;
49
50 let object_hash = gix_hash::Kind::try_from(data[ofs])
51 .map_err(|v| message!("Commit-graph file uses unsupported hash version: {v}").unsupported_error())?;
52 ofs += 1;
53
54 let chunk_count = data[ofs];
55 ofs += 1;
58
59 let base_graph_count = data[ofs];
60 ofs += 1;
61
62 let chunks = gix_chunk::file::Index::from_bytes(&data, ofs, u32::from(chunk_count)).or_raise(|| {
63 message!("Couldn't read commit-graph file with {chunk_count} chunks at offset {ofs}").corrupted()
64 })?;
65
66 let base_graphs_list_offset = chunks
67 .validated_usize_offset_by_id(BASE_GRAPHS_LIST_CHUNK_ID, |chunk_range| -> Result<_> {
68 let chunk_size = chunk_range.len();
69 if chunk_size % object_hash.len_in_bytes() != 0 {
70 bail!("Commit-graph chunk {BASE_GRAPHS_LIST_CHUNK_ID:?} has invalid size: {msg}".corrupted(),
71 msg = format!(
72 "chunk size {} is not a multiple of {}",
73 chunk_size,
74 object_hash.len_in_bytes()
75 ),
76 );
77 }
78 let chunk_base_graph_count: u32 = (chunk_size / object_hash.len_in_bytes())
79 .try_into()
80 .expect("base graph count to fit in 32-bits");
81 if chunk_base_graph_count != u32::from(base_graph_count) {
82 bail!("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".corrupted())
83 }
84 Ok(chunk_range.start)
85 })
86 .ok()
87 .transpose()?;
88
89 let (commit_data_offset, commit_data_count): (_, u32) = chunks
90 .validated_usize_offset_by_id(COMMIT_DATA_CHUNK_ID, |chunk_range| -> Result<_> {
91 let chunk_size = chunk_range.len();
92
93 let entry_size = object_hash.len_in_bytes() + COMMIT_DATA_ENTRY_SIZE_SANS_HASH;
94 if chunk_size % entry_size != 0 {
95 bail!("Commit-graph chunk {COMMIT_DATA_CHUNK_ID:?} has invalid size: chunk size {chunk_size} is not a multiple of {entry_size}".corrupted())
96 }
97 Ok((
98 chunk_range.start,
99 (chunk_size / entry_size)
100 .try_into()
101 .expect("number of commits in CDAT chunk to fit in 32 bits"),
102 ))
103 })
104 .or_raise(|| corruption("Error getting offset for commit data chunk"))??;
105
106 let fan_offset = chunks
107 .validated_usize_offset_by_id(OID_FAN_CHUNK_ID, |chunk_range| -> Result<_> {
108 let chunk_size = chunk_range.len();
109
110 let expected_size = 4 * FAN_LEN;
111 if chunk_size != expected_size {
112 bail!("Commit-graph chunk {OID_FAN_CHUNK_ID:?} has invalid size: expected chunk length {expected_size}, got {chunk_size}".corrupted())
113 }
114 Ok(chunk_range.start)
115 })
116 .or_raise(|| corruption("Error getting offset for OID fan chunk"))?
117 .or_raise(|| message("Error getting offset for OID fan chunk"))?;
118
119 let (oid_lookup_offset, oid_lookup_count): (_, u32) = chunks
120 .validated_usize_offset_by_id(OID_LOOKUP_CHUNK_ID, |chunk_range| -> Result<_> {
121 let chunk_size = chunk_range.len();
122
123 if chunk_size % object_hash.len_in_bytes() != 0 {
124 bail!("Commit-graph chunk {OID_LOOKUP_CHUNK_ID:?} has invalid size: chunk size {chunk_size} is not a multiple of {hash_len}".corrupted(), hash_len = object_hash.len_in_bytes())
125 }
126 Ok((
127 chunk_range.start,
128 (chunk_size / object_hash.len_in_bytes())
129 .try_into()
130 .expect("number of commits in OIDL chunk to fit in 32 bits"),
131 ))
132 })
133 .or_raise(|| corruption("Error getting offset for OID lookup chunk"))?
134 .or_raise(|| message("Error getting offset for OID lookup chunk"))?;
135
136 let extra_edges_list_range = chunks.usize_offset_by_id(EXTENDED_EDGES_LIST_CHUNK_ID).ok();
137
138 let trailer = &data[chunks.highest_offset() as usize..];
139 if trailer.len() != object_hash.len_in_bytes() {
140 bail!(
141 "Expected commit-graph trailer to contain {} bytes, got {}".corrupted(),
142 object_hash.len_in_bytes(),
143 trailer.len()
144 );
145 }
146
147 if base_graph_count > 0 && base_graphs_list_offset.is_none() {
148 bail!("Chunk named {BASE_GRAPHS_LIST_CHUNK_ID:?} was not found in chunk file index".corrupted());
149 }
150
151 let (fan, _) = read_fan(&data[fan_offset..]);
152 if oid_lookup_count != fan[255] {
153 bail!(
154 "Commit-graph {OID_FAN_CHUNK_ID:?} chunk contains {chunk1_commits} commits, but {OID_LOOKUP_CHUNK_ID:?} chunk contains {chunk2_commits} commits".corrupted(),
155 chunk1_commits = fan[255],
156 chunk2_commits = oid_lookup_count,
157 );
158 }
159 if commit_data_count != fan[255] {
160 bail!(
161 "Commit-graph {OID_FAN_CHUNK_ID:?} chunk contains {chunk1_commits} commits, but {COMMIT_DATA_CHUNK_ID:?} chunk contains {chunk2_commits} commits".corrupted(),
162 chunk1_commits = fan[255],
163 chunk2_commits = commit_data_count,
164 );
165 }
166 Ok(File {
167 base_graph_count,
168 base_graphs_list_offset,
169 commit_data_offset,
170 data,
171 extra_edges_list_range,
172 fan,
173 oid_lookup_offset,
174 path,
175 hash_len: object_hash.len_in_bytes(),
176 object_hash,
177 })
178 }
179}
180
181impl TryFrom<&Path> for File {
182 type Error = gix_error::Error;
183
184 fn try_from(path: &Path) -> Result<Self> {
185 let data = std::fs::File::open(path)
186 .and_then(|file| {
187 #[expect(unsafe_code)]
189 unsafe {
190 memmap2::MmapOptions::new().map_copy_read_only(&file)
191 }
192 })
193 .or_raise(|| message!("Could not open commit-graph file at \"{path}\"", path = path.display()))?;
194 Self::new(data, path.to_owned())
195 }
196}
197
198fn read_fan(d: &[u8]) -> ([u32; FAN_LEN], usize) {
200 assert!(d.len() >= FAN_LEN * 4);
201
202 let mut fan = [0; FAN_LEN];
203 for (c, f) in d.as_chunks::<4>().0.iter().zip(fan.iter_mut()) {
204 *f = u32::from_be_bytes(*c);
205 }
206 (fan, FAN_LEN * 4)
207}