Skip to main content

git_policy_cli/
repo_read_odb.rs

1//! What:
2//!  The object database of one repository read in process: loose objects, packs and alternates.
3//! Why:
4//!  A lookup that finds the object answers exactly what `git cat-file` would print for it;
5//!  a lookup that does not (missing, corrupt, too large, replaced by a `refs/replace` ref, in a shallow or grafted
6//!  history) returns `None`, and the caller asks Git, which words the failure or fetches a promised object itself.
7//!
8//! In TS you'd write (pseudocode):
9//! ```ts
10//! const odb = openObjectDatabase(commonDir, format); const object = odb.read(name); // undefined: ask Git
11//! ```
12
13#![cfg(unix)]
14
15use super::private_storage::{ReadRefusal, read_regular_file};
16use super::repo_read_layout::ObjectFormat;
17use super::repo_read_object::{OBJECT_SIZE_LIMIT, RawObject, name_length, parse_loose, to_hex};
18use super::repo_read_pack::Pack;
19use std::path::{Path, PathBuf};
20
21/// How many alternate object directories deep the reader follows `info/alternates`.
22const ALTERNATE_DEPTH_LIMIT: usize = 5;
23
24/// The largest `info/alternates` file the reader looks at.
25const ALTERNATES_LIMIT: u64 = 1 << 20;
26
27/// The largest `packed-refs` file the reader scans for replace refs.
28const PACKED_REFS_LIMIT: u64 = 256 << 20;
29
30/// The loose directories and packs of a repository and its alternates.
31#[derive(Debug)]
32pub struct ObjectDatabase {
33    /// The object directories, the repository's own first.
34    roots: Vec<PathBuf>,
35    /// Every pack of every directory.
36    packs: Vec<Pack>,
37    /// The object format, which fixes the name length.
38    format: ObjectFormat,
39}
40
41/// The extra object directories `objects/info/alternates` names; `None` when the file cannot be read.
42fn alternates_of(root: &Path, depth: usize, found: &mut Vec<PathBuf>) -> Option<()> {
43    if depth > ALTERNATE_DEPTH_LIMIT {
44        return None;
45    }
46    let file: PathBuf = root.join("info/alternates");
47    // A bounded read of a regular file: a link, a pipe or a huge file is left to Git.
48    let text: String = match read_regular_file(&file, ALTERNATES_LIMIT) {
49        Ok(bytes) => String::from_utf8(bytes).ok()?,
50        Err(ReadRefusal::Missing) => return Some(()),
51        Err(_) => return None,
52    };
53    for line in text.lines() {
54        if line.is_empty() || line.starts_with('#') {
55            continue;
56        }
57        if line.starts_with('"') || line.contains('\\') {
58            return None;
59        }
60        let target: PathBuf = std::fs::canonicalize(root.join(line)).ok()?;
61        if found.contains(&target) {
62            continue;
63        }
64        found.push(target.clone());
65        alternates_of(target.as_path(), depth + 1, found)?;
66    }
67    return Some(());
68}
69
70/// Whether the repository changes what an object name means: replace refs, a shallow file, grafts.
71fn objects_are_replaced(common_dir: &Path) -> bool {
72    let replace_dir: PathBuf = common_dir.join("refs/replace");
73    if std::fs::read_dir(&replace_dir).is_ok_and(|mut entries| { return entries.next().is_some(); }) {
74        return true;
75    }
76    if common_dir.join("shallow").exists() || common_dir.join("info/grafts").exists() {
77        return true;
78    }
79    return match read_regular_file(common_dir.join("packed-refs").as_path(), PACKED_REFS_LIMIT) {
80        Ok(bytes) => bytes.windows(b" refs/replace/".len()).any(|window| { return window == b" refs/replace/"; }),
81        Err(ReadRefusal::Missing) => false,
82        Err(_) => true,
83    };
84}
85
86impl ObjectDatabase {
87    /// What:
88    ///  Open the database under `common_dir/objects` (or `objects_dir` when given) with its alternates;
89    ///  `None` when anything makes object names mean more than the files say.
90    /// Why:
91    ///   Opening lists the pack indexes once; every later lookup reads only the few bytes it needs.
92    ///
93    /// In TS you'd write (pseudocode):
94    /// ```ts
95    /// openObjectDatabase({ commonDir, objectsDir, format }): ObjectDatabase | undefined;
96    /// ```
97    pub fn open(common_dir: &Path, objects_dir: Option<&Path>, format: ObjectFormat) -> Option<ObjectDatabase> {
98        if objects_are_replaced(common_dir) {
99            return None;
100        }
101        let own: PathBuf = std::fs::canonicalize(objects_dir.map_or_else(|| { return common_dir.join("objects"); }, Path::to_path_buf)).ok()?;
102        let mut roots: Vec<PathBuf> = vec![own.clone()];
103        alternates_of(own.as_path(), 0, &mut roots)?;
104        let mut packs: Vec<Pack> = Vec::new();
105        for root in &roots {
106            let listing = match std::fs::read_dir(root.join("pack")) {
107                Ok(listing) => listing,
108                Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
109                Err(_) => return None,
110            };
111            for entry in listing {
112                let path: PathBuf = entry.ok()?.path();
113                if path.extension().is_some_and(|extension| { return extension == "idx"; }) {
114                    packs.push(Pack::open(path.as_path(), name_length(format))?);
115                }
116            }
117        }
118        return Some(ObjectDatabase { roots, packs, format });
119    }
120
121    /// The object named `name`, or `None` when it is absent, unreadable or beyond the size limit.
122    pub fn read(&self, name: &[u8]) -> Option<RawObject> {
123        return self.read_at_depth(name, 0);
124    }
125
126    /// Whether the object named `name` is stored loose or in a pack, without reading it.
127    pub fn contains(&self, name: &[u8]) -> bool {
128        if name.len() != name_length(self.format) {
129            return false;
130        }
131        let hex: String = to_hex(name);
132        let loose: bool = self.roots.iter().any(|root| { return root.join(&hex[..2]).join(&hex[2..]).is_file(); });
133        return loose || self.packs.iter().any(|pack| { return pack.find(name).is_some(); });
134    }
135
136    /// The same read, counting the delta bases already followed through names so a cycle ends.
137    fn read_at_depth(&self, name: &[u8], depth: usize) -> Option<RawObject> {
138        if name.len() != name_length(self.format) || depth > 8 {
139            return None;
140        }
141        let hex: String = to_hex(name);
142        for root in &self.roots {
143            let path: PathBuf = root.join(&hex[..2]).join(&hex[2..]);
144            let Ok(metadata) = std::fs::symlink_metadata(&path) else {
145                continue;
146            };
147            if !metadata.is_file() || metadata.len() > OBJECT_SIZE_LIMIT as u64 {
148                return None;
149            }
150            return parse_loose(std::fs::read(&path).ok()?.as_slice());
151        }
152        let by_name = |base: &[u8]| -> Option<RawObject> { return self.read_at_depth(base, depth + 1); };
153        for pack in &self.packs {
154            if let Some(offset) = pack.find(name) {
155                return pack.read(offset, &by_name, 0);
156            }
157        }
158        return None;
159    }
160}
161
162#[cfg(test)]
163#[path = "repo_read_odb_tests.rs"]
164mod tests;