mod alternates;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::{fs, io};
pub use alternates::AlternateLimits;
use crate::pack::FilePack;
use crate::pack::reader::check_cancelled;
use crate::{LooseObjects, ObjectFormat, ObjectId, ObjectKind};
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Object {
pub(crate) kind: ObjectKind,
pub(crate) format: ObjectFormat,
pub(crate) data: Vec<u8>,
}
impl Object {
pub fn object_format(&self) -> ObjectFormat {
self.format
}
pub fn kind(&self) -> ObjectKind {
self.kind
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn into_data(self) -> Vec<u8> {
self.data
}
pub fn id(&self) -> ObjectId {
self.format.hash_object(self.kind, &self.data)
}
}
#[derive(Debug, Clone, Copy)]
pub struct PackLimits {
pub max_bytes: usize,
pub max_index_bytes: usize,
pub max_open_files: usize,
pub max_directory_entries: usize,
pub max_packs: usize,
}
impl Default for PackLimits {
fn default() -> Self {
Self {
max_bytes: usize::try_from(64_u64 * 1024 * 1024 * 1024).unwrap_or(usize::MAX),
max_index_bytes: 64 * 1024 * 1024,
max_open_files: 512,
max_directory_entries: 1_000_000,
max_packs: 256,
}
}
}
impl PackLimits {
pub const fn trusted() -> Self {
Self {
max_bytes: usize::MAX / 4,
max_index_bytes: usize::MAX / 8,
max_open_files: 4096,
max_directory_entries: usize::MAX / 4,
max_packs: 100_000,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ReadLimits {
pub max_object_bytes: usize,
pub max_delta_bytes: usize,
pub max_decode_bytes: usize,
pub max_input_bytes: usize,
pub max_delta_depth: usize,
}
impl Default for ReadLimits {
fn default() -> Self {
Self {
max_object_bytes: 64 * 1024 * 1024,
max_delta_bytes: 64 * 1024 * 1024,
max_decode_bytes: 256 * 1024 * 1024,
max_delta_depth: 64,
max_input_bytes: 256 * 1024 * 1024,
}
}
}
impl ReadLimits {
pub const fn trusted() -> Self {
Self {
max_object_bytes: usize::MAX / 4,
max_delta_bytes: usize::MAX / 4,
max_decode_bytes: usize::MAX / 4,
max_input_bytes: usize::MAX / 4,
max_delta_depth: 10_000,
}
}
}
#[derive(Debug, Clone)]
pub struct Objects {
format: ObjectFormat,
stores: Vec<Store>,
directory: PathBuf,
pub(crate) shallow: crate::ShallowRoots,
}
#[derive(Debug, Clone)]
struct Store {
directory: PathBuf,
loose: LooseObjects,
packs: Vec<Arc<FilePack>>,
}
impl Objects {
pub fn store_directories(&self) -> impl Iterator<Item = &Path> {
self.stores.iter().map(|store| store.directory.as_path())
}
pub fn shallow_roots(&self) -> &crate::ShallowRoots {
&self.shallow
}
pub fn object_format(&self) -> ObjectFormat {
self.format
}
pub(crate) fn open(
format: ObjectFormat,
directory: &Path,
limits: PackLimits,
alternates: AlternateLimits,
) -> Result<Self, ObjectReadError> {
Self::open_controlled(
format,
directory,
limits,
alternates,
&AtomicBool::new(false),
)
}
pub(crate) fn open_controlled(
format: ObjectFormat,
directory: &Path,
limits: PackLimits,
alternates: AlternateLimits,
cancelled: &AtomicBool,
) -> Result<Self, ObjectReadError> {
#[cfg(feature = "tracing")]
let span = tracing::debug_span!(
target: "girt",
"objects.open",
object_format = %format,
outcome = "incomplete",
failure_class = tracing::field::Empty,
effects = tracing::field::Empty,
);
let operation = || {
check_cancelled(cancelled)?;
let directories = alternates::discover(directory, alternates)?;
check_cancelled(cancelled)?;
let directory = directories[0].clone();
let mut budget = limits;
let mut stores = Vec::new();
for directory in directories {
stores.push(Store::open(format, &directory, &mut budget, cancelled)?);
}
Ok(Self {
format,
stores,
directory,
shallow: crate::ShallowRoots::empty(format),
})
};
#[cfg(feature = "tracing")]
let result = span.in_scope(operation);
#[cfg(not(feature = "tracing"))]
let result = { operation }();
#[cfg(feature = "tracing")]
crate::trace::finish(&span, &result, crate::trace::object);
result
}
pub fn refresh(
&mut self,
packs: PackLimits,
alternates: AlternateLimits,
) -> Result<(), ObjectReadError> {
self.refresh_controlled(packs, alternates, &AtomicBool::new(false))
}
pub fn refresh_controlled(
&mut self,
packs: PackLimits,
alternates: AlternateLimits,
cancelled: &AtomicBool,
) -> Result<(), ObjectReadError> {
let candidate =
Self::open_controlled(self.format, &self.directory, packs, alternates, cancelled)?;
check_cancelled(cancelled)?;
self.stores = candidate.stores;
Ok(())
}
pub fn read(
&self,
id: ObjectId,
limits: ReadLimits,
) -> Result<Option<Object>, ObjectReadError> {
self.read_controlled(id, limits, &AtomicBool::new(false))
}
pub fn read_controlled(
&self,
id: ObjectId,
limits: ReadLimits,
cancelled: &AtomicBool,
) -> Result<Option<Object>, ObjectReadError> {
#[cfg(feature = "tracing")]
let span = tracing::trace_span!(
target: "girt",
"objects.read",
outcome = "incomplete",
failure_class = tracing::field::Empty,
effects = tracing::field::Empty,
);
let operation = || {
check_cancelled(cancelled)?;
let loose_limit = limits.max_object_bytes.min(limits.max_decode_bytes);
for store in &self.stores {
check_cancelled(cancelled)?;
for pack in &store.packs {
check_cancelled(cancelled)?;
if let Some(position) = pack.find(id)? {
let object = pack.read(position, limits, cancelled)?;
check_cancelled(cancelled)?;
return Ok(Some(object));
}
}
match store.loose.read_raw(id, loose_limit) {
Ok(object) => {
check_cancelled(cancelled)?;
return Ok(Some(object));
}
Err(crate::Error::Io(error)) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error.into()),
}
}
check_cancelled(cancelled)?;
if id == self.format.hash_object(ObjectKind::Tree, b"") {
return Ok(Some(Object {
kind: ObjectKind::Tree,
format: self.format,
data: Vec::new(),
}));
}
Ok(None)
};
#[cfg(feature = "tracing")]
let result = span.in_scope(operation);
#[cfg(not(feature = "tracing"))]
let result = { operation }();
#[cfg(feature = "tracing")]
crate::trace::finish(&span, &result, crate::trace::object);
result
}
}
impl Store {
fn open(
format: ObjectFormat,
directory: &Path,
budget: &mut PackLimits,
cancelled: &AtomicBool,
) -> Result<Self, ObjectReadError> {
let loose = LooseObjects::new(directory, format);
let pack_directory = directory.join("pack");
let entries = match fs::read_dir(&pack_directory) {
Ok(entries) => entries,
Err(error) if error.kind() == io::ErrorKind::NotFound => {
return Ok(Self {
directory: directory.to_owned(),
loose,
packs: vec![],
});
}
Err(source) => {
return Err(ObjectReadError::Path {
path: pack_directory,
source,
});
}
};
let mut paths = Vec::new();
for entry in entries {
check_cancelled(cancelled)?;
budget.max_directory_entries = budget
.max_directory_entries
.checked_sub(1)
.ok_or(ObjectReadError::Limit("pack directory entries"))?;
let path = entry
.map_err(|source| ObjectReadError::Path {
path: pack_directory.clone(),
source,
})?
.path();
if path.extension().is_some_and(|extension| extension == "idx") {
budget.max_packs = budget
.max_packs
.checked_sub(1)
.ok_or(ObjectReadError::Limit("pack count"))?;
paths.push(path);
}
}
paths.sort();
let mut packs = Vec::new();
for path in paths {
budget.max_open_files = budget
.max_open_files
.checked_sub(2)
.ok_or(ObjectReadError::Limit("pack file handles"))?;
packs.push(Arc::new(
FilePack::open(format, &path, budget, cancelled).map_err(
|source| match source {
ObjectReadError::Limit(_)
| ObjectReadError::Cancelled
| ObjectReadError::Path { .. } => source,
source => ObjectReadError::PackArtifacts {
pack: path.with_extension("pack"),
index: path,
source: Box::new(source),
},
},
)?,
));
}
Ok(Self {
directory: directory.to_owned(),
loose,
packs,
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum ObjectReadError {
#[error("object storage cancelled")]
Cancelled,
#[error("invalid alternate path in {path}: {reason}")]
Alternate {
path: PathBuf,
reason: &'static str,
},
#[error("unsupported object storage: {0}")]
Unsupported(&'static str),
#[error("object storage at {path}")]
Path {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("object storage pair {index} / {pack}")]
PackArtifacts {
index: PathBuf,
pack: PathBuf,
#[source]
source: Box<ObjectReadError>,
},
#[error("loose object")]
Loose(#[from] crate::Error),
#[error("unsupported pack index version {0}")]
IndexVersion(u32),
#[error("unsupported pack version {0}")]
PackVersion(u32),
#[error("unsupported packed object type {0}")]
ObjectType(u8),
#[error("corrupt packed storage: {0}")]
Corrupt(&'static str),
#[error("object storage limit exceeded: {0}")]
Limit(&'static str),
#[error("missing or external delta base {0}")]
MissingBase(ObjectId),
#[error("cyclic delta chain")]
DeltaCycle,
}