use std::{
collections::{HashMap, HashSet, VecDeque},
fs,
path::{Path, PathBuf},
sync::Mutex,
};
use crate::{
GitError, HashKind, Object, ObjectId, Result, cache::ByteCache, loose, midx::MultiPackIndex,
pack::PackIndex,
};
struct MidxRoute {
index: MultiPackIndex,
packs: HashMap<String, usize>,
}
pub(crate) struct ObjectStore {
directories: Vec<PathBuf>,
packs: Vec<PackIndex>,
midx: Vec<MidxRoute>,
object_cache: Mutex<ByteCache<ObjectId, Object>>,
}
impl ObjectStore {
pub(crate) fn open(
primary: PathBuf,
hash: HashKind,
object_cache_bytes: usize,
delta_cache_bytes: usize,
) -> Result<Self> {
let directories = object_directories(primary)?;
let mut packs = Vec::new();
let mut midx = Vec::new();
for directory in &directories {
let pack_directory = directory.join("pack");
let entries = match fs::read_dir(&pack_directory) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => return Err(error.into()),
};
let mut paths = entries
.filter_map(std::result::Result::ok)
.map(|entry| entry.path())
.filter(|path| path.extension().is_some_and(|value| value == "idx"))
.collect::<Vec<_>>();
paths.sort_unstable();
let mut route = HashMap::new();
for path in paths {
let name = path
.file_name()
.and_then(|value| value.to_str())
.ok_or_else(|| crate::error::invalid("pack index name is not UTF-8"))?
.to_owned();
let slot = packs.len();
packs.push(PackIndex::open(&path, hash, delta_cache_bytes)?);
route.insert(name, slot);
}
if let Some(index) = MultiPackIndex::open(&pack_directory, hash)? {
midx.push(MidxRoute {
index,
packs: route,
});
}
}
Ok(Self {
directories,
packs,
midx,
object_cache: Mutex::new(ByteCache::new(object_cache_bytes)),
})
}
pub(crate) fn read(
&self,
id: ObjectId,
max_object_bytes: usize,
max_delta_depth: usize,
) -> Result<Object> {
if let Some(object) = self
.object_cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(id)
{
return Ok(object);
}
let object = self.read_inner(id, max_object_bytes, max_delta_depth, 0)?;
let bytes = object.data.len();
self.object_cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(id, object.clone(), bytes);
Ok(object)
}
pub(crate) fn pack_count(&self) -> usize {
self.packs.len()
}
pub(crate) fn multi_pack_index_count(&self) -> usize {
self.midx.len()
}
pub(crate) fn bitmap_reachable(
&self,
id: ObjectId,
max_objects: usize,
) -> Result<Option<Vec<ObjectId>>> {
for route in &self.midx {
if let Some(objects) = route.index.bitmap_reachable(id, max_objects)? {
return Ok(Some(objects));
}
}
for pack in &self.packs {
if let Some(objects) = pack.bitmap_reachable(id, max_objects)? {
return Ok(Some(objects));
}
}
Ok(None)
}
pub(crate) fn contains(&self, id: ObjectId) -> bool {
let hex = id.to_hex();
self.directories
.iter()
.any(|directory| directory.join(&hex[..2]).join(&hex[2..]).is_file())
|| self
.midx
.iter()
.any(|route| route.index.find(id).is_ok_and(|value| value.is_some()))
|| self.packs.iter().any(|pack| pack.offset(id).is_some())
}
fn read_inner(
&self,
id: ObjectId,
max_object_bytes: usize,
max_delta_depth: usize,
depth: usize,
) -> Result<Object> {
if depth > max_delta_depth {
return Err(GitError::LimitExceeded {
resource: "pack delta depth",
limit: max_delta_depth,
});
}
for directory in &self.directories {
if let Some(object) = loose::read(directory, id, max_object_bytes)? {
return Ok(object);
}
}
for route in &self.midx {
let Some(location) = route.index.find(id)? else {
continue;
};
let pack = route
.packs
.get(location.pack)
.and_then(|slot| self.packs.get(*slot))
.ok_or_else(|| crate::error::invalid("MIDX references a missing pack index"))?;
let external = |base, next_depth| {
self.read_inner(base, max_object_bytes, max_delta_depth, next_depth)
};
return pack.read_at(
id,
location.offset,
max_object_bytes,
max_delta_depth,
&external,
depth,
);
}
for pack in &self.packs {
let external = |base, next_depth| {
self.read_inner(base, max_object_bytes, max_delta_depth, next_depth)
};
if let Some(object) =
pack.read(id, max_object_bytes, max_delta_depth, &external, depth)?
{
return Ok(object);
}
}
Err(GitError::NotFound(id.to_string()))
}
}
fn object_directories(primary: PathBuf) -> Result<Vec<PathBuf>> {
let mut queue = VecDeque::from([primary]);
let mut seen = HashSet::new();
let mut result = Vec::new();
while let Some(directory) = queue.pop_front() {
let identity = directory
.canonicalize()
.unwrap_or_else(|_| directory.clone());
if !seen.insert(identity) {
continue;
}
if result.len() >= 32 {
return Err(GitError::LimitExceeded {
resource: "object alternates",
limit: 32,
});
}
let alternates = directory.join("info").join("alternates");
match fs::read_to_string(alternates) {
Ok(value) => {
for line in value.lines().filter(|line| !line.trim().is_empty()) {
let path = Path::new(line.trim());
queue.push_back(if path.is_absolute() {
path.to_owned()
} else {
directory.join(path)
});
}
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error.into()),
}
result.push(directory);
}
Ok(result)
}