#![warn(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::indexing_slicing,
reason = "parses untrusted repository or network input (#103)"
)]
mod alternates;
mod prefix;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::{fs, io};
pub use alternates::AlternateLimits;
pub use prefix::ResolvePrefixError;
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: Arc<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> {
Arc::unwrap_or_clone(self.data)
}
pub fn id(&self) -> ObjectId {
self.format.hash_object(self.kind, &self.data)
}
}
pub(crate) type Located = (ObjectId, Option<(ObjectKind, u64)>);
#[derive(Debug)]
pub(crate) struct UnverifiedObject {
pub(crate) kind: ObjectKind,
pub(crate) data: Arc<Vec<u8>>,
}
impl UnverifiedObject {
pub(crate) fn kind(&self) -> ObjectKind {
self.kind
}
pub(crate) fn data(&self) -> &[u8] {
&self.data
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
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 PackLimits {
#[must_use]
pub fn with_max_bytes(mut self, max_bytes: usize) -> Self {
self.max_bytes = max_bytes;
self
}
#[must_use]
pub fn with_max_index_bytes(mut self, max_index_bytes: usize) -> Self {
self.max_index_bytes = max_index_bytes;
self
}
#[must_use]
pub fn with_max_open_files(mut self, max_open_files: usize) -> Self {
self.max_open_files = max_open_files;
self
}
#[must_use]
pub fn with_max_directory_entries(mut self, max_directory_entries: usize) -> Self {
self.max_directory_entries = max_directory_entries;
self
}
#[must_use]
pub fn with_max_packs(mut self, max_packs: usize) -> Self {
self.max_packs = max_packs;
self
}
}
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)]
#[non_exhaustive]
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 ReadLimits {
#[must_use]
pub fn with_max_object_bytes(mut self, max_object_bytes: usize) -> Self {
self.max_object_bytes = max_object_bytes;
self
}
#[must_use]
pub fn with_max_delta_bytes(mut self, max_delta_bytes: usize) -> Self {
self.max_delta_bytes = max_delta_bytes;
self
}
#[must_use]
pub fn with_max_decode_bytes(mut self, max_decode_bytes: usize) -> Self {
self.max_decode_bytes = max_decode_bytes;
self
}
#[must_use]
pub fn with_max_input_bytes(mut self, max_input_bytes: usize) -> Self {
self.max_input_bytes = max_input_bytes;
self
}
#[must_use]
pub fn with_max_delta_depth(mut self, max_delta_depth: usize) -> Self {
self.max_delta_depth = max_delta_depth;
self
}
}
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,
damage_history: Arc<crate::pack::DamageHistory>,
abbreviations: Arc<prefix::LooseAbbreviations>,
}
#[derive(Debug, Clone)]
struct Store {
directory: PathBuf,
loose: LooseObjects,
packs: Vec<Arc<FilePack>>,
pack_paths: Vec<PathBuf>,
}
fn share_pack_damage(
stores: &mut [Store],
history: &crate::pack::DamageHistory,
) -> Result<(), ObjectReadError> {
for store in stores {
for pack in &mut store.packs {
let pack = Arc::get_mut(pack).ok_or(ObjectReadError::Corrupt(
"pack shared before opening completed",
))?;
history.share(pack);
}
}
Ok(())
}
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
.first()
.cloned()
.ok_or(ObjectReadError::Corrupt(
"object store has no primary directory",
))?;
let mut budget = limits;
let mut stores = Vec::new();
for directory in directories {
stores.push(Store::open(format, &directory, &mut budget, cancelled)?);
}
let damage_history = Arc::new(crate::pack::DamageHistory::default());
share_pack_damage(&mut stores, &damage_history)?;
Ok(Self {
format,
stores,
damage_history,
abbreviations: Arc::default(),
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 mut candidate =
Self::open_controlled(self.format, &self.directory, packs, alternates, cancelled)?;
check_cancelled(cancelled)?;
share_pack_damage(&mut candidate.stores, &self.damage_history)?;
self.stores = candidate.stores;
self.abbreviations = candidate.abbreviations;
Ok(())
}
pub(crate) fn retention_directories(directory: &Path) -> Result<Vec<PathBuf>, ObjectReadError> {
alternates::discover(directory, AlternateLimits::default())
}
pub(crate) fn locate(
&self,
id: ObjectId,
limits: ReadLimits,
cancel: &AtomicBool,
) -> Result<Option<(ObjectKind, u64)>, ObjectReadError> {
id.require_format(self.format)
.map_err(crate::loose::Error::from)?;
for store in &self.stores {
check_cancelled(cancel)?;
for pack in &store.packs {
if let Some(position) = pack.find(id)? {
return pack.locate(position, limits, cancel).map(Some);
}
}
match store.loose.locate(id) {
Ok(located) => {
if located.1 > limits.max_object_bytes as u64 {
return Err(ObjectReadError::Limit("object bytes"));
}
return Ok(Some(located));
}
Err(crate::loose::Error::Io(error)) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error.into()),
}
}
Ok((id == self.format.hash_object(ObjectKind::Tree, b"")).then_some((ObjectKind::Tree, 0)))
}
pub(crate) fn locate_all(
&self,
ids: impl IntoIterator<Item = ObjectId>,
limits: ReadLimits,
cancel: &AtomicBool,
) -> Result<Vec<Located>, ObjectReadError> {
let mut located = Vec::new();
let mut packed = Vec::new();
'ids: for id in ids {
id.require_format(self.format)
.map_err(crate::loose::Error::from)?;
let mut pack_index = 0;
for store in &self.stores {
check_cancelled(cancel)?;
for pack in &store.packs {
if let Some(position) = pack.find(id)? {
packed.push((pack_index, pack.offset(position), position, id));
continue 'ids;
}
pack_index += 1;
}
match store.loose.locate(id) {
Ok(loose) => {
if loose.1 > limits.max_object_bytes as u64 {
return Err(ObjectReadError::Limit("object bytes"));
}
located.push((id, Some(loose)));
continue 'ids;
}
Err(crate::loose::Error::Io(error))
if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error.into()),
}
}
let empty = id == self.format.hash_object(ObjectKind::Tree, b"");
located.push((id, empty.then_some((ObjectKind::Tree, 0))));
}
packed.sort_unstable();
let packs: Vec<_> = self.stores.iter().flat_map(|store| &store.packs).collect();
let _ordered = crate::pack::HeaderScope::ordered();
let mut known = (usize::MAX, crate::pack::BaseKinds::new());
for (pack, _, position, id) in packed {
check_cancelled(cancel)?;
if known.0 != pack {
known = (pack, crate::pack::BaseKinds::new());
}
#[expect(
clippy::indexing_slicing,
reason = "pack positions were enumerated from these same stores above"
)]
let found = packs[pack].locate_memoized(position, limits, cancel, &mut known.1)?;
located.push((id, Some(found)));
}
Ok(located)
}
pub(crate) fn check_retention_blob_kinds(
&self,
limits: ReadLimits,
max_entries: usize,
cancel: &AtomicBool,
is_blob: impl Fn(ObjectId) -> bool,
) -> Result<(), ObjectReadError> {
if is_blob(self.format.hash_object(ObjectKind::Tree, b"")) {
return Err(ObjectReadError::Corrupt("blob claims canonical empty tree"));
}
let check_packs = self.stores.len() > 1
|| self
.stores
.first()
.is_some_and(|store| store.packs.len() > 1);
let mut entries = 0;
for store in &self.stores {
if check_packs {
store.check_packed_blob_kinds(
limits,
max_entries,
cancel,
&is_blob,
&mut entries,
)?;
}
store.check_loose_blob_kinds(max_entries, cancel, &is_blob, &mut entries)?;
}
Ok(())
}
pub(crate) fn check_packs_unchanged(&self, cancel: &AtomicBool) -> Result<(), ObjectReadError> {
for store in &self.stores {
for pack in &store.packs {
check_cancelled(cancel)?;
pack.check_unchanged()?;
}
}
check_cancelled(cancel)
}
pub fn verify_packs(&self, cancelled: &AtomicBool) -> Result<(), ObjectReadError> {
check_cancelled(cancelled)?;
for store in &self.stores {
for pack in &store.packs {
pack.verify(cancelled)?;
}
}
check_cancelled(cancelled)
}
pub(crate) fn verify_indexes(&self, cancelled: &AtomicBool) -> Result<(), ObjectReadError> {
for store in &self.stores {
for pack in &store.packs {
check_cancelled(cancelled)?;
pack.verify_index()?;
}
}
Ok(())
}
pub fn read(
&self,
id: ObjectId,
limits: ReadLimits,
) -> Result<Option<Object>, ObjectReadError> {
self.read_controlled(id, limits, &AtomicBool::new(false))
}
pub fn read_commit(
&self,
id: ObjectId,
limits: ReadLimits,
) -> Result<crate::Commit, ReadCommitError> {
let object = self.read(id, limits)?.ok_or(ReadCommitError::Missing(id))?;
let kind = object.kind();
if kind != ObjectKind::Commit {
return Err(ReadCommitError::Kind { id, kind });
}
Ok(crate::Commit::parse(self.format, object.data())?)
}
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);
let mut damage = None;
for store in &self.stores {
check_cancelled(cancelled)?;
for pack in &store.packs {
check_cancelled(cancelled)?;
if let Some(position) = pack.find(id)? {
match pack.read_candidate(position, limits, cancelled) {
Ok(object) => {
check_cancelled(cancelled)?;
return Ok(Some(object));
}
Err(crate::pack::ReadFailure::Damaged(error)) => {
damage.get_or_insert(error);
}
Err(crate::pack::ReadFailure::Other(error)) => return Err(error),
}
}
}
match store.loose.read_raw(id, loose_limit) {
Ok(object) => {
check_cancelled(cancelled)?;
return Ok(Some(object));
}
Err(crate::loose::Error::Io(error))
if error.kind() == io::ErrorKind::NotFound => {}
Err(crate::loose::Error::Corrupt(_)) if damage.is_some() => {}
Err(error) => return Err(error.into()),
}
}
check_cancelled(cancelled)?;
if let Some(error) = damage {
return Err(error);
}
if id == self.format.hash_object(ObjectKind::Tree, b"") {
return Ok(Some(Object {
kind: ObjectKind::Tree,
format: self.format,
data: Arc::default(),
}));
}
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 Objects {
pub(crate) fn read_unverified(
&self,
id: ObjectId,
limits: ReadLimits,
cancelled: &AtomicBool,
) -> Result<Option<UnverifiedObject>, ObjectReadError> {
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)? {
match pack.read_candidate_unverified(position, limits, cancelled) {
Ok(object) => {
check_cancelled(cancelled)?;
return Ok(Some(object));
}
Err(crate::pack::ReadFailure::Damaged(_)) => {
return self.read_controlled(id, limits, cancelled).map(|object| {
object.map(|object| UnverifiedObject {
kind: object.kind,
data: object.data,
})
});
}
Err(crate::pack::ReadFailure::Other(error)) => return Err(error),
}
}
}
match store.loose.read_raw(id, loose_limit) {
Ok(object) => {
check_cancelled(cancelled)?;
return Ok(Some(UnverifiedObject {
kind: object.kind,
data: object.data,
}));
}
Err(crate::loose::Error::Io(error)) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => return Err(error.into()),
}
}
check_cancelled(cancelled)?;
Ok(
(id == self.format.hash_object(ObjectKind::Tree, b"")).then(|| UnverifiedObject {
kind: ObjectKind::Tree,
data: Arc::default(),
}),
)
}
pub(crate) fn has_stored_empty_tree(
&self,
cancel: &AtomicBool,
) -> Result<bool, ObjectReadError> {
let id = self.format.hash_object(ObjectKind::Tree, b"");
let hex = id.to_string();
for store in &self.stores {
check_cancelled(cancel)?;
for pack in &store.packs {
check_cancelled(cancel)?;
if pack.find(id)?.is_some() {
return Ok(true);
}
}
let path = store.directory.join(&hex[..2]).join(&hex[2..]);
if path
.try_exists()
.map_err(|source| ObjectReadError::Path { path, source })?
{
return Ok(true);
}
}
check_cancelled(cancel)?;
Ok(false)
}
pub(crate) fn selects_received(
&self,
id: ObjectId,
received: &FilePack,
cancel: &AtomicBool,
) -> Result<bool, ObjectReadError> {
for store in &self.stores {
for pack in &store.packs {
check_cancelled(cancel)?;
if std::ptr::eq(pack.as_ref(), received) {
return Ok(true);
}
if pack.find(id)?.is_some() {
return Ok(false);
}
}
let hex = id.to_string();
let path = store.directory.join(&hex[..2]).join(&hex[2..]);
if path
.try_exists()
.map_err(|source| ObjectReadError::Path { path, source })?
{
return Ok(false);
}
}
Ok(false)
}
pub(crate) fn transfer_packs(&self) -> Vec<&FilePack> {
self.stores
.iter()
.flat_map(|s| s.packs.iter().map(AsRef::as_ref))
.collect()
}
pub(crate) fn transfer_object(
&self,
id: ObjectId,
limits: ReadLimits,
cancel: &AtomicBool,
) -> Result<Option<(ObjectKind, usize, Option<crate::pack::Packed>)>, ObjectReadError> {
Ok(match self.locate_transfer(id, limits, cancel)? {
None => None,
Some(Transfer::Packed(pack, packed)) => {
let (kind, length) = pack.object_header(packed.position, limits)?;
Some((kind, length, Some(packed)))
}
Some(Transfer::Stored(kind, length)) => Some((kind, length, None)),
})
}
pub(crate) fn transfer_location(
&self,
id: ObjectId,
limits: ReadLimits,
cancel: &AtomicBool,
) -> Result<Option<Option<crate::pack::Packed>>, ObjectReadError> {
Ok(self
.locate_transfer(id, limits, cancel)?
.map(|transfer| match transfer {
Transfer::Packed(_, packed) => Some(packed),
Transfer::Stored(..) => None,
}))
}
fn locate_transfer(
&self,
id: ObjectId,
limits: ReadLimits,
cancel: &AtomicBool,
) -> Result<Option<Transfer<'_>>, ObjectReadError> {
id.require_format(self.format)
.map_err(crate::loose::Error::from)?;
let mut pack_index = 0;
for store in &self.stores {
check_cancelled(cancel)?;
for pack in &store.packs {
check_cancelled(cancel)?;
if let Some(position) = pack.find(id)? {
return Ok(Some(Transfer::Packed(
pack,
crate::pack::Packed {
id,
pack: pack_index,
position,
},
)));
}
pack_index += 1;
}
match store
.loose
.read_raw(id, limits.max_object_bytes.min(limits.max_decode_bytes))
{
Ok(object) => {
return Ok(Some(Transfer::Stored(object.kind(), object.data().len())));
}
Err(crate::loose::Error::Io(e)) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => return Err(e.into()),
}
}
if id == self.format.hash_object(ObjectKind::Tree, b"") {
return Ok(Some(Transfer::Stored(ObjectKind::Tree, 0)));
}
Ok(None)
}
pub(crate) fn primary_packs_with_paths(&self) -> Vec<(&Path, &FilePack)> {
self.stores
.iter()
.take(1)
.flat_map(|store| {
store
.pack_paths
.iter()
.map(Path::new)
.zip(store.packs.iter().map(AsRef::as_ref))
})
.collect()
}
}
enum Transfer<'a> {
Packed(&'a FilePack, crate::pack::Packed),
Stored(ObjectKind, usize),
}
impl Store {
fn check_packed_blob_kinds(
&self,
limits: ReadLimits,
max_entries: usize,
cancel: &AtomicBool,
is_blob: &impl Fn(ObjectId) -> bool,
entries: &mut usize,
) -> Result<(), ObjectReadError> {
for pack in &self.packs {
for id in pack.identities() {
check_cancelled(cancel)?;
charge_copy_entry(entries, max_entries)?;
let id = id?;
if is_blob(id) {
let position = pack
.find(id)?
.ok_or(ObjectReadError::Corrupt("copy vanished"))?;
if pack.locate(position, limits, cancel)?.0 != ObjectKind::Blob {
return Err(ObjectReadError::Corrupt(
"reachable blob has a graph-bearing copy",
));
}
}
}
}
Ok(())
}
fn check_loose_blob_kinds(
&self,
max_entries: usize,
cancel: &AtomicBool,
is_blob: &impl Fn(ObjectId) -> bool,
entries: &mut usize,
) -> Result<(), ObjectReadError> {
let at = |source| ObjectReadError::Path {
path: self.directory.clone(),
source,
};
for entry in fs::read_dir(&self.directory).map_err(at)? {
check_cancelled(cancel)?;
charge_copy_entry(entries, max_entries)?;
let entry = entry.map_err(at)?;
let name = entry.file_name();
let Some(prefix) = name.to_str() else {
continue;
};
if prefix.len() != 2 || !prefix.bytes().all(|byte| byte.is_ascii_hexdigit()) {
continue;
}
if !fs::symlink_metadata(entry.path()).map_err(at)?.is_dir() {
return Err(ObjectReadError::Corrupt("non-directory loose fanout"));
}
for file in fs::read_dir(entry.path()).map_err(at)? {
check_cancelled(cancel)?;
charge_copy_entry(entries, max_entries)?;
let file = file.map_err(at)?;
let name = file.file_name();
let Some(suffix) = name.to_str() else {
continue;
};
let Ok(id) =
ObjectId::from_hex(self.loose.object_format(), &format!("{prefix}{suffix}"))
else {
continue;
};
if is_blob(id) && self.loose.locate(id)?.0 != ObjectKind::Blob {
return Err(ObjectReadError::Corrupt(
"reachable blob has a graph-bearing copy",
));
}
}
}
Ok(())
}
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![],
pack_paths: 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 pack_paths: Vec<PathBuf> = paths.iter().map(|p| p.with_extension("pack")).collect();
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,
pack_paths,
})
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ReadCommitError {
#[error("commit {0} is missing")]
Missing(ObjectId),
#[error("object {id} is a {kind:?}, not a commit")]
Kind {
id: ObjectId,
kind: ObjectKind,
},
#[error(transparent)]
Parse(#[from] crate::CommitError),
#[error(transparent)]
Read(#[from] ObjectReadError),
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ObjectReadError {
#[error("pinned pack artifact changed: {path}")]
PackChanged {
path: PathBuf,
},
#[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::loose::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,
}
fn charge_copy_entry(entries: &mut usize, max: usize) -> Result<(), ObjectReadError> {
*entries = entries
.checked_add(1)
.ok_or(ObjectReadError::Limit("retention copy entries"))?;
if *entries > max {
return Err(ObjectReadError::Limit("retention copy entries"));
}
Ok(())
}
#[cfg(test)]
#[path = "objects/tests.rs"]
mod tests;