git_policy_cli/
repo_read_odb.rs1#![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
21const ALTERNATE_DEPTH_LIMIT: usize = 5;
23
24const ALTERNATES_LIMIT: u64 = 1 << 20;
26
27const PACKED_REFS_LIMIT: u64 = 256 << 20;
29
30#[derive(Debug)]
32pub struct ObjectDatabase {
33 roots: Vec<PathBuf>,
35 packs: Vec<Pack>,
37 format: ObjectFormat,
39}
40
41fn 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 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
70fn 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 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 pub fn read(&self, name: &[u8]) -> Option<RawObject> {
123 return self.read_at_depth(name, 0);
124 }
125
126 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 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;