#![cfg(unix)]
use super::private_storage::{ReadRefusal, read_regular_file};
use super::repo_read_layout::ObjectFormat;
use super::repo_read_object::{OBJECT_SIZE_LIMIT, RawObject, name_length, parse_loose, to_hex};
use super::repo_read_pack::Pack;
use std::path::{Path, PathBuf};
const ALTERNATE_DEPTH_LIMIT: usize = 5;
const ALTERNATES_LIMIT: u64 = 1 << 20;
const PACKED_REFS_LIMIT: u64 = 256 << 20;
#[derive(Debug)]
pub struct ObjectDatabase {
roots: Vec<PathBuf>,
packs: Vec<Pack>,
format: ObjectFormat,
}
pub fn alternates_of(root: &Path, depth: usize, found: &mut Vec<PathBuf>) -> Option<()> {
if depth > ALTERNATE_DEPTH_LIMIT {
return None;
}
let file: PathBuf = root.join("info/alternates");
let text: String = match read_regular_file(&file, ALTERNATES_LIMIT) {
Ok(bytes) => String::from_utf8(bytes).ok()?,
Err(ReadRefusal::Missing) => return Some(()),
Err(_) => return None,
};
for line in text.lines() {
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with('"') || line.contains('\\') {
return None;
}
let target: PathBuf = std::fs::canonicalize(root.join(line)).ok()?;
if found.contains(&target) {
continue;
}
found.push(target.clone());
alternates_of(target.as_path(), depth + 1, found)?;
}
return Some(());
}
fn objects_are_replaced(common_dir: &Path) -> bool {
let replace_dir: PathBuf = common_dir.join("refs/replace");
if std::fs::read_dir(&replace_dir).is_ok_and(|mut entries| { return entries.next().is_some(); }) {
return true;
}
if common_dir.join("shallow").exists() || common_dir.join("info/grafts").exists() {
return true;
}
return match read_regular_file(common_dir.join("packed-refs").as_path(), PACKED_REFS_LIMIT) {
Ok(bytes) => bytes.windows(b" refs/replace/".len()).any(|window| { return window == b" refs/replace/"; }),
Err(ReadRefusal::Missing) => false,
Err(_) => true,
};
}
impl ObjectDatabase {
pub fn open(common_dir: &Path, objects_dir: Option<&Path>, format: ObjectFormat) -> Option<ObjectDatabase> {
if objects_are_replaced(common_dir) {
return None;
}
let own: PathBuf = std::fs::canonicalize(objects_dir.map_or_else(|| { return common_dir.join("objects"); }, Path::to_path_buf)).ok()?;
let mut roots: Vec<PathBuf> = vec![own.clone()];
alternates_of(own.as_path(), 0, &mut roots)?;
let mut packs: Vec<Pack> = Vec::new();
for root in &roots {
let listing = match std::fs::read_dir(root.join("pack")) {
Ok(listing) => listing,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(_) => return None,
};
for entry in listing {
let path: PathBuf = entry.ok()?.path();
if path.extension().is_some_and(|extension| { return extension == "idx"; }) {
packs.push(Pack::open(path.as_path(), name_length(format))?);
}
}
}
return Some(ObjectDatabase { roots, packs, format });
}
pub fn read(&self, name: &[u8]) -> Option<RawObject> {
return self.read_at_depth(name, 0);
}
pub fn contains(&self, name: &[u8]) -> bool {
if name.len() != name_length(self.format) {
return false;
}
let hex: String = to_hex(name);
let loose: bool = self.roots.iter().any(|root| { return root.join(&hex[..2]).join(&hex[2..]).is_file(); });
return loose || self.packs.iter().any(|pack| { return pack.find(name).is_some(); });
}
fn read_at_depth(&self, name: &[u8], depth: usize) -> Option<RawObject> {
if name.len() != name_length(self.format) || depth > 8 {
return None;
}
let hex: String = to_hex(name);
for root in &self.roots {
let path: PathBuf = root.join(&hex[..2]).join(&hex[2..]);
let Ok(metadata) = std::fs::symlink_metadata(&path) else {
continue;
};
if !metadata.is_file() || metadata.len() > OBJECT_SIZE_LIMIT as u64 {
return None;
}
return parse_loose(std::fs::read(&path).ok()?.as_slice());
}
let by_name = |base: &[u8]| -> Option<RawObject> { return self.read_at_depth(base, depth + 1); };
for pack in &self.packs {
if let Some(offset) = pack.find(name) {
return pack.read(offset, &by_name, 0);
}
}
return None;
}
}
#[cfg(test)]
#[path = "repo_read_odb_tests.rs"]
mod tests;