Skip to main content

weavatrix_git/
store.rs

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}