1use crate::{File, Graph, MAX_COMMITS, Parent};
2use gix_error::Result;
3use gix_error::{
4 OptionExt, ResourceExhaustionKind, ResultExt, bail, corruption, message, resource_exhaustion, validation,
5};
6use std::{
7 io::{BufRead, BufReader, Read},
8 path::Path,
9};
10
11impl Graph {
13 pub fn at(path: &Path, object_hash: gix_hash::Kind, alloc_limit_bytes: Option<usize>) -> Result<Self> {
24 let metadata = path
25 .metadata()
26 .or_raise(|| message!("Could not access commit-graph path at \"{}\"", path.display()))?;
27 let graph = if metadata.is_file() {
28 Self::from_file(path, object_hash)
29 } else if metadata.is_dir() {
30 if path.join("commit-graph-chain").is_file() {
31 Self::from_commit_graphs_dir(path, object_hash)
32 } else {
33 Self::from_info_dir(path, object_hash)
34 }
35 } else {
36 Err(message!(
37 "Did not find any files that look like commit graphs at \"{}\"",
38 path.display()
39 )
40 .validation_error())
41 }?;
42 if let Some(limit) = alloc_limit_bytes {
43 for file in &graph.files {
44 if let Some(edges) = file.extra_edges_data() {
45 let parent_bytes = (edges.len() / 4 + 1).checked_mul(2 * std::mem::size_of::<Result<Parent>>());
48 if parent_bytes.is_none_or(|bytes| bytes > limit) {
49 bail!(resource_exhaustion(
50 ResourceExhaustionKind::AllocationLimit,
51 "Commit-graph parents require more memory than allowed",
52 ));
53 }
54 }
55 }
56 }
57 Ok(graph)
58 }
59
60 pub fn from_commit_graphs_dir(path: &Path, object_hash: gix_hash::Kind) -> Result<Self> {
64 let commit_graphs_dir = path;
65 let chain_file_path = commit_graphs_dir.join("commit-graph-chain");
66 let chain_file = std::fs::File::open(&chain_file_path).or_raise(|| {
67 message!(
68 "Could not open commit-graph chain file at \"{}\"",
69 chain_file_path.display()
70 )
71 })?;
72
73 let files = read_chain_files(chain_file, &chain_file_path, commit_graphs_dir, object_hash)?;
74 Self::new(files).or_raise(|| {
75 message!(
76 "Could not assemble commit-graph from chain file at \"{}\"",
77 chain_file_path.display()
78 )
79 .corrupted()
80 })
81 }
82
83 pub fn from_file(path: &Path, object_hash: gix_hash::Kind) -> Result<Self> {
88 let file = read_graph_file(path, object_hash)?;
89 Self::new(vec![file])
90 .or_raise(|| message!("Could not assemble commit-graph from file at \"{}\"", path.display()).corrupted())
91 }
92
93 pub fn from_info_dir(info_dir: &Path, object_hash: gix_hash::Kind) -> Result<Self> {
97 Self::from_file(&info_dir.join("commit-graph"), object_hash).or_else(|err| {
98 if err.is_not_found() {
99 Self::from_commit_graphs_dir(&info_dir.join("commit-graphs"), object_hash)
100 } else {
101 Err(err)
102 }
103 })
104 }
105
106 pub fn new(files: Vec<File>) -> Result<Self> {
110 if files.len() > 256 {
111 bail!(validation("Commit-graph must contain at most 256 files"));
112 }
113 let mut files = nonempty::NonEmpty::from_vec(files)
114 .ok_or_raise(|| validation("Commit-graph must contain at least one file"))?;
115 let num_commits: u64 = files.iter().map(|f| u64::from(f.num_commits())).sum();
116 if num_commits > u64::from(MAX_COMMITS) {
117 bail!(
118 "Commit-graph files contain {num_commits} commits altogether, but only {MAX_COMMITS} commits are allowed".validation()
119 );
120 }
121
122 let mut f1 = files.first();
123 for f2 in files.tail() {
124 if f1.object_hash() != f2.object_hash() {
125 bail!(
126 "Commit-graph files mismatch: \"{path1}\" uses hash {hash1:?}, but \"{path2}\" uses hash {hash2:?}"
127 .validation(),
128 path1 = f1.path().display(),
129 hash1 = f1.object_hash(),
130 path2 = f2.path().display(),
131 hash2 = f2.object_hash(),
132 );
133 }
134 f1 = f2;
135 }
136
137 let mut parent_position_limit = 0;
138 for file in files.iter_mut() {
139 parent_position_limit += file.num_commits();
140 file.parent_position_limit = parent_position_limit;
141 }
142 Ok(Self { files })
143 }
144}
145
146fn read_chain_files(
148 chain_file: std::fs::File,
149 chain_file_path: &Path,
150 commit_graphs_dir: &Path,
151 object_hash: gix_hash::Kind,
152) -> Result<Vec<File>> {
153 let mut files = Vec::new();
154 let mut reader = BufReader::new(chain_file);
155 let mut line = Vec::with_capacity(object_hash.len_in_hex() + 2 );
156 loop {
157 line.clear();
158 let bytes = reader
160 .by_ref()
161 .take((object_hash.len_in_hex() + 2 + 1) as u64)
162 .read_until(b'\n', &mut line)
163 .or_raise(|| {
164 message!(
165 "Could not read (enough data) from commit-graph file at \"{}\"",
166 chain_file_path.display()
167 )
168 })?;
169 if bytes == 0 {
170 break;
171 }
172 if line.len() > object_hash.len_in_hex() + 2
173 {
175 bail!(corruption("Commit-graph chain entry exceeds the maximum hash length"));
176 }
177 if line.last() == Some(&b'\n') {
178 line.pop();
179 if line.last() == Some(&b'\r') {
180 line.pop();
181 }
182 }
183 if line.len() != object_hash.len_in_hex() {
184 bail!(
185 "Commit-graph chain entry has {} hex digits, but hash kind {object_hash} requires {}".corrupted(),
186 line.len(),
187 object_hash.len_in_hex()
188 );
189 }
190 gix_hash::ObjectId::from_hex(&line)
191 .or_raise(|| corruption("Commit-graph chain entry is not a valid object ID"))?;
192 let hash = std::str::from_utf8(&line).expect("validated hexadecimal object IDs contain only ASCII");
193 if files.len() == 256 {
194 bail!(corruption("Commit-graph chain contains more than 256 files"));
195 }
196 let graph_file_path = commit_graphs_dir.join(format!("graph-{hash}.graph"));
197 files.push(read_graph_file(&graph_file_path, object_hash)?);
198 }
199 Ok(files)
200}
201
202fn read_graph_file(path: &Path, object_hash: gix_hash::Kind) -> Result<File> {
204 let file = File::at(path)?;
205 if file.object_hash() != object_hash {
206 bail!(
207 "Commit-graph file at \"{}\" uses hash kind {}, but expected {object_hash}".corrupted(),
208 path.display(),
209 file.object_hash()
210 );
211 }
212 Ok(file)
213}