1use std::{
2 collections::{HashMap, HashSet, VecDeque},
3 fs,
4 path::{Path, PathBuf},
5 sync::{Arc, Mutex},
6};
7
8use crate::{
9 GitError, HashKind, Object, ObjectId, Repository, Result, cache::ByteCache, loose,
10 midx::MultiPackIndex, pack::PackIndex,
11};
12
13struct MidxRoute {
14 index: MultiPackIndex,
15 packs: HashMap<String, usize>,
16}
17
18pub(crate) struct ObjectStore {
19 directories: Vec<PathBuf>,
20 packs: Vec<PackIndex>,
21 midx: Vec<MidxRoute>,
22 object_cache: Mutex<ByteCache<ObjectId, Arc<Object>>>,
23}
24
25impl ObjectStore {
26 pub(crate) fn open(
27 primary: PathBuf,
28 hash: HashKind,
29 object_cache_bytes: usize,
30 delta_cache_bytes: usize,
31 ) -> Result<Self> {
32 let directories = object_directories(primary)?;
33 let mut packs = Vec::new();
34 let mut midx = Vec::new();
35 for directory in &directories {
36 let pack_directory = directory.join("pack");
37 let entries = match fs::read_dir(&pack_directory) {
38 Ok(entries) => entries,
39 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
40 Err(error) => return Err(error.into()),
41 };
42 let mut paths = entries
43 .filter_map(std::result::Result::ok)
44 .map(|entry| entry.path())
45 .filter(|path| path.extension().is_some_and(|value| value == "idx"))
46 .collect::<Vec<_>>();
47 paths.sort_unstable();
48 let mut route = HashMap::new();
49 for path in paths {
50 let name = path
51 .file_name()
52 .and_then(|value| value.to_str())
53 .ok_or_else(|| crate::error::invalid("pack index name is not UTF-8"))?
54 .to_owned();
55 let slot = packs.len();
56 packs.push(PackIndex::open(&path, hash, delta_cache_bytes)?);
57 route.insert(name, slot);
58 }
59 if let Some(index) = MultiPackIndex::open(&pack_directory, hash)? {
60 midx.push(MidxRoute {
61 index,
62 packs: route,
63 });
64 }
65 }
66 Ok(Self {
67 directories,
68 packs,
69 midx,
70 object_cache: Mutex::new(ByteCache::new(object_cache_bytes)),
71 })
72 }
73
74 pub(crate) fn read_shared(
75 &self,
76 id: ObjectId,
77 max_object_bytes: usize,
78 max_delta_depth: usize,
79 ) -> Result<Arc<Object>> {
80 if let Some(object) = self
81 .object_cache
82 .lock()
83 .unwrap_or_else(std::sync::PoisonError::into_inner)
84 .get(id)
85 {
86 return Ok(object);
87 }
88 let object = self.read_inner(id, max_object_bytes, max_delta_depth, 0)?;
89 let bytes = object.data.len();
90 let object = Arc::new(object);
91 self.object_cache
92 .lock()
93 .unwrap_or_else(std::sync::PoisonError::into_inner)
94 .insert(id, Arc::clone(&object), bytes);
95 Ok(object)
96 }
97
98 pub(crate) fn pack_count(&self) -> usize {
99 self.packs.len()
100 }
101
102 pub(crate) fn multi_pack_index_count(&self) -> usize {
103 self.midx.len()
104 }
105
106 pub(crate) fn bitmap_reachable(
107 &self,
108 id: ObjectId,
109 max_objects: usize,
110 ) -> Result<Option<Vec<ObjectId>>> {
111 for route in &self.midx {
112 if let Some(objects) = route.index.bitmap_reachable(id, max_objects)? {
113 return Ok(Some(objects));
114 }
115 }
116 for pack in &self.packs {
117 if let Some(objects) = pack.bitmap_reachable(id, max_objects)? {
118 return Ok(Some(objects));
119 }
120 }
121 Ok(None)
122 }
123
124 pub(crate) fn contains(&self, id: ObjectId) -> bool {
125 let hex = id.to_hex();
126 self.directories
127 .iter()
128 .any(|directory| directory.join(&hex[..2]).join(&hex[2..]).is_file())
129 || self
130 .midx
131 .iter()
132 .any(|route| route.index.find(id).is_ok_and(|value| value.is_some()))
133 || self.packs.iter().any(|pack| pack.offset(id).is_some())
134 }
135
136 fn read_inner(
137 &self,
138 id: ObjectId,
139 max_object_bytes: usize,
140 max_delta_depth: usize,
141 depth: usize,
142 ) -> Result<Object> {
143 if depth > max_delta_depth {
144 return Err(GitError::LimitExceeded {
145 resource: "pack delta depth",
146 limit: max_delta_depth,
147 });
148 }
149 for directory in &self.directories {
150 if let Some(object) = loose::read(directory, id, max_object_bytes)? {
151 return Ok(object);
152 }
153 }
154 for route in &self.midx {
155 let Some(location) = route.index.find(id)? else {
156 continue;
157 };
158 let pack = route
159 .packs
160 .get(location.pack)
161 .and_then(|slot| self.packs.get(*slot))
162 .ok_or_else(|| crate::error::invalid("MIDX references a missing pack index"))?;
163 let external = |base, next_depth| {
164 self.read_inner(base, max_object_bytes, max_delta_depth, next_depth)
165 };
166 return pack.read_at(
167 id,
168 location.offset,
169 max_object_bytes,
170 max_delta_depth,
171 &external,
172 depth,
173 );
174 }
175 for pack in &self.packs {
176 let external = |base, next_depth| {
177 self.read_inner(base, max_object_bytes, max_delta_depth, next_depth)
178 };
179 if let Some(object) =
180 pack.read(id, max_object_bytes, max_delta_depth, &external, depth)?
181 {
182 return Ok(object);
183 }
184 }
185 Err(GitError::NotFound(id.to_string()))
186 }
187}
188
189impl Repository {
190 pub fn object_shared(&self, id: ObjectId) -> Result<Arc<Object>> {
191 if id.kind() != self.hash_kind() {
192 return Err(crate::error::invalid(
193 "object id hash kind differs from repository",
194 ));
195 }
196 if let Some(object) =
197 crate::backend::read(&self.backends, id, self.limits.max_object_bytes)?
198 {
199 return Ok(Arc::new(object));
200 }
201 self.store.read_shared(
202 id,
203 self.limits.max_object_bytes,
204 self.limits.max_delta_depth,
205 )
206 }
207}
208
209fn object_directories(primary: PathBuf) -> Result<Vec<PathBuf>> {
210 let mut queue = VecDeque::from([primary]);
211 let mut seen = HashSet::new();
212 let mut result = Vec::new();
213 while let Some(directory) = queue.pop_front() {
214 let identity = directory
215 .canonicalize()
216 .unwrap_or_else(|_| directory.clone());
217 if !seen.insert(identity) {
218 continue;
219 }
220 if result.len() >= 32 {
221 return Err(GitError::LimitExceeded {
222 resource: "object alternates",
223 limit: 32,
224 });
225 }
226 let alternates = directory.join("info").join("alternates");
227 match fs::read_to_string(alternates) {
228 Ok(value) => {
229 for line in value.lines().filter(|line| !line.trim().is_empty()) {
230 let path = Path::new(line.trim());
231 queue.push_back(if path.is_absolute() {
232 path.to_owned()
233 } else {
234 directory.join(path)
235 });
236 }
237 }
238 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
239 Err(error) => return Err(error.into()),
240 }
241 result.push(directory);
242 }
243 Ok(result)
244}