use std::fs::{self, ReadDir};
use std::io;
use std::iter::FusedIterator;
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use super::{Objects, Store};
use crate::pack::FilePack;
use crate::pack::reader::check_cancelled;
use crate::{ObjectFormat, ObjectId, ObjectReadError};
#[derive(Debug, Clone, Copy, Default)]
pub(super) struct PackMarks {
pub(super) keep: bool,
pub(super) promisor: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct ListLimits {
pub max_entries: usize,
}
impl ListLimits {
#[must_use]
pub fn with_max_entries(mut self, max_entries: usize) -> Self {
self.max_entries = max_entries;
self
}
}
impl Default for ListLimits {
fn default() -> Self {
Self {
max_entries: 1 << 27,
}
}
}
impl Objects {
pub fn stores(&self) -> impl ExactSizeIterator<Item = ObjectStore<'_>> {
let format = self.format;
self.stores
.iter()
.enumerate()
.map(move |(position, store)| ObjectStore {
store,
format,
alternate: position != 0,
})
}
pub fn packs(&self) -> impl Iterator<Item = IndexedPack<'_>> {
self.stores().flat_map(|store| store.packs())
}
pub fn object_ids(&self, limits: ListLimits) -> Result<Vec<ObjectId>, ObjectReadError> {
self.object_ids_controlled(limits, &AtomicBool::new(false))
}
pub fn object_ids_controlled(
&self,
limits: ListLimits,
cancelled: &AtomicBool,
) -> Result<Vec<ObjectId>, ObjectReadError> {
let mut remaining = limits.max_entries;
let mut ids = Vec::new();
for store in self.stores() {
for pack in store.packs() {
check_cancelled(cancelled)?;
remaining = remaining
.checked_sub(pack.object_count())
.ok_or(ObjectReadError::Limit("listed object entries"))?;
ids.extend(pack.ids());
}
let mut loose = store.loose_objects_controlled(
ListLimits::default().with_max_entries(remaining),
cancelled,
);
for object in &mut loose {
ids.push(object?.id());
}
remaining = loose.remaining;
}
ids.sort_unstable();
ids.dedup();
Ok(ids)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ObjectStore<'a> {
store: &'a Store,
format: ObjectFormat,
alternate: bool,
}
impl<'a> ObjectStore<'a> {
pub fn directory(&self) -> &'a Path {
&self.store.directory
}
pub fn is_alternate(&self) -> bool {
self.alternate
}
pub fn packs(&self) -> impl ExactSizeIterator<Item = IndexedPack<'a>> + use<'a> {
let store = self.store;
let format = self.format;
store
.packs
.iter()
.zip(&store.pack_paths)
.zip(&store.pack_marks)
.map(move |((pack, path), marks)| IndexedPack {
pack,
path,
marks: *marks,
format,
})
}
pub fn loose_objects(&self) -> LooseIds<'a> {
self.loose_objects_controlled(ListLimits::default(), &NEVER_CANCELLED)
}
pub fn loose_objects_controlled(
&self,
limits: ListLimits,
cancelled: &'a AtomicBool,
) -> LooseIds<'a> {
LooseIds {
directory: &self.store.directory,
format: self.format,
cancelled,
remaining: limits.max_entries,
state: State::Start,
}
}
}
static NEVER_CANCELLED: AtomicBool = AtomicBool::new(false);
#[derive(Debug, Clone, Copy)]
pub struct IndexedPack<'a> {
pack: &'a FilePack,
path: &'a PathBuf,
marks: PackMarks,
format: ObjectFormat,
}
impl<'a> IndexedPack<'a> {
pub fn path(&self) -> &'a Path {
self.path
}
pub fn index_path(&self) -> PathBuf {
self.path.with_extension("idx")
}
pub fn object_count(&self) -> usize {
self.pack.object_count()
}
pub fn pack_bytes(&self) -> u64 {
self.pack.artifact_bytes().0
}
pub fn index_bytes(&self) -> u64 {
self.pack.artifact_bytes().1
}
pub fn checksum(&self) -> ObjectId {
self.pack.checksum()
}
pub fn is_kept(&self) -> bool {
self.marks.keep
}
pub fn is_promisor(&self) -> bool {
self.marks.promisor
}
pub fn object_format(&self) -> ObjectFormat {
self.format
}
pub fn contains(&self, id: ObjectId) -> Result<bool, ObjectReadError> {
Ok(self.pack.find(id)?.is_some())
}
pub fn ids(&self) -> PackIds<'a> {
PackIds {
pack: self.pack,
positions: 0..self.pack.object_count(),
}
}
}
#[derive(Debug, Clone)]
pub struct PackIds<'a> {
pack: &'a FilePack,
positions: Range<usize>,
}
impl Iterator for PackIds<'_> {
type Item = ObjectId;
fn next(&mut self) -> Option<ObjectId> {
self.positions
.next()
.map(|position| self.pack.identity_at(position))
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.positions.size_hint()
}
fn nth(&mut self, n: usize) -> Option<ObjectId> {
self.positions
.nth(n)
.map(|position| self.pack.identity_at(position))
}
}
impl DoubleEndedIterator for PackIds<'_> {
fn next_back(&mut self) -> Option<ObjectId> {
self.positions
.next_back()
.map(|position| self.pack.identity_at(position))
}
}
impl ExactSizeIterator for PackIds<'_> {}
impl FusedIterator for PackIds<'_> {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LooseObject {
id: ObjectId,
path: PathBuf,
}
impl LooseObject {
pub fn id(&self) -> ObjectId {
self.id
}
pub fn path(&self) -> &Path {
&self.path
}
}
#[derive(Debug)]
pub struct LooseIds<'a> {
directory: &'a Path,
format: ObjectFormat,
cancelled: &'a AtomicBool,
remaining: usize,
state: State,
}
#[derive(Debug)]
enum State {
Start,
Fanouts(ReadDir),
Objects {
fanouts: ReadDir,
prefix: String,
directory: PathBuf,
entries: Box<ReadDir>,
},
Done,
}
impl LooseIds<'_> {
fn charge(&mut self) -> Result<(), ObjectReadError> {
check_cancelled(self.cancelled)?;
self.remaining = self
.remaining
.checked_sub(1)
.ok_or(ObjectReadError::Limit("loose directory entries"))?;
Ok(())
}
fn advance(&mut self) -> Result<Option<LooseObject>, ObjectReadError> {
loop {
match std::mem::replace(&mut self.state, State::Done) {
State::Done => return Ok(None),
State::Start => match fs::read_dir(self.directory) {
Ok(fanouts) => self.state = State::Fanouts(fanouts),
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(source) => return Err(at(self.directory, source)),
},
State::Fanouts(mut fanouts) => {
let Some(entry) = fanouts.next() else {
return Ok(None);
};
self.charge()?;
let entry = entry.map_err(|source| at(self.directory, source))?;
let name = entry.file_name();
let prefix = name
.to_str()
.filter(|name| name.len() == 2 && name.bytes().all(is_lower_hex));
let directory = entry.path();
let entries = match prefix.map(|_| fs::read_dir(&directory)) {
Some(Ok(entries)) => Some(entries),
Some(Err(error))
if matches!(
error.kind(),
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
) =>
{
None
}
Some(Err(source)) => return Err(at(&directory, source)),
None => None,
};
self.state = match (prefix, entries) {
(Some(prefix), Some(entries)) => State::Objects {
fanouts,
prefix: prefix.to_owned(),
directory,
entries: Box::new(entries),
},
_ => State::Fanouts(fanouts),
};
}
State::Objects {
fanouts,
mut prefix,
directory,
mut entries,
} => {
let Some(entry) = entries.next() else {
self.state = State::Fanouts(fanouts);
continue;
};
self.charge()?;
let entry = entry.map_err(|source| at(&directory, source))?;
let name = entry.file_name();
let found = name
.to_str()
.filter(|suffix| {
suffix.len() == self.format.digest_len() * 2 - 2
&& suffix.bytes().all(is_lower_hex)
})
.and_then(|suffix| {
prefix.push_str(suffix);
let id = ObjectId::from_hex(self.format, &prefix).ok();
prefix.truncate(2);
id
});
let found = match found {
Some(id) => entry
.file_type()
.map_err(|source| at(&directory, source))?
.is_file()
.then_some(id),
None => None,
};
let path = found.is_some().then(|| entry.path());
self.state = State::Objects {
fanouts,
prefix,
directory,
entries,
};
if let (Some(id), Some(path)) = (found, path) {
return Ok(Some(LooseObject { id, path }));
}
}
}
}
}
}
impl Iterator for LooseIds<'_> {
type Item = Result<LooseObject, ObjectReadError>;
fn next(&mut self) -> Option<Self::Item> {
self.advance().transpose()
}
}
impl FusedIterator for LooseIds<'_> {}
fn is_lower_hex(byte: u8) -> bool {
matches!(byte, b'0'..=b'9' | b'a'..=b'f')
}
fn at(path: &Path, source: io::Error) -> ObjectReadError {
ObjectReadError::Path {
path: path.to_owned(),
source,
}
}