#![cfg(unix)]
use crate::private_storage::{ReadRefusal, read_regular_file};
use crate::repo_read_layout::ObjectFormat;
use crate::repo_read_object::{OBJECT_SIZE_LIMIT, RawObject, name_length, parse_loose, to_hex};
use crate::repo_read_pack::{BaseByName, 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;
thread_local! {
static LOOSE_LOOKS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
#[cfg(test)]
pub(crate) fn loose_looks() -> u64 {
return LOOSE_LOOKS.with(std::cell::Cell::get);
}
#[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 let Ok(mut entries) = std::fs::read_dir(&replace_dir)
&& 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) => {
let mut names_replace_refs: bool = false;
for window in bytes.windows(b" refs/replace/".len()) {
if window == b" refs/replace/" {
names_replace_refs = true;
break;
}
}
names_replace_refs
}
Err(ReadRefusal::Missing) => false,
Err(_) => true,
};
}
struct BaseOneDeeper<'a> {
database: &'a ObjectDatabase,
depth: usize,
}
impl BaseByName for BaseOneDeeper<'_> {
fn base_named(&self, name: &[u8]) -> Option<RawObject> {
return self.database.read_at_depth(name, self.depth + 1);
}
}
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 objects: PathBuf = match objects_dir {
Some(directory) => directory.to_path_buf(),
None => common_dir.join("objects"),
};
let own: PathBuf = std::fs::canonicalize(objects).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() == Some(std::ffi::OsStr::new("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 decoded_entries(&self) -> u64 {
return self.packs.iter().map(Pack::decoded_entries).sum();
}
pub fn contains(&self, name: &[u8]) -> bool {
if name.len() != name_length(self.format) {
return false;
}
let hex: String = to_hex(name);
for root in &self.roots {
if root.join(&hex[..2]).join(&hex[2..]).is_file() {
return true;
}
}
for pack in &self.packs {
if pack.find(name).is_some() {
return true;
}
}
return false;
}
fn read_at_depth(&self, name: &[u8], depth: usize) -> Option<RawObject> {
if name.len() != name_length(self.format) || depth > 8 {
return None;
}
let by_name: BaseOneDeeper<'_> = BaseOneDeeper { database: self, depth };
for pack in &self.packs {
if let Some(offset) = pack.find(name) {
if let Some(found) = pack.read(offset, &by_name, 0) {
return Some(found);
}
break;
}
}
let hex: String = to_hex(name);
for root in &self.roots {
let path: PathBuf = root.join(&hex[..2]).join(&hex[2..]);
LOOSE_LOOKS.set(LOOSE_LOOKS.get() + 1);
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());
}
return None;
}
}
#[cfg(test)]
#[path = "repo_read_odb_tests.rs"]
mod tests;