use std::sync::atomic::{AtomicBool, Ordering};
use crate::{
IdSet, Object, ObjectId, ObjectKind, ObjectReadError, Objects, ReadLimits, Tag, TagError,
};
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct PeelLimits {
pub max_tags: usize,
pub max_bytes: usize,
pub read: ReadLimits,
}
impl PeelLimits {
#[must_use]
pub fn with_max_tags(mut self, max_tags: usize) -> Self {
self.max_tags = max_tags;
self
}
#[must_use]
pub fn with_max_bytes(mut self, max_bytes: usize) -> Self {
self.max_bytes = max_bytes;
self
}
#[must_use]
pub fn with_read(mut self, read: ReadLimits) -> Self {
self.read = read;
self
}
}
impl Default for PeelLimits {
fn default() -> Self {
Self {
max_tags: 32,
max_bytes: 64 * 1024 * 1024,
read: ReadLimits::default(),
}
}
}
impl PeelLimits {
pub const fn trusted() -> Self {
Self {
max_tags: 10_000,
max_bytes: usize::MAX / 4,
read: ReadLimits::trusted(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct PeeledObject {
pub original: ObjectId,
pub tags: Vec<ObjectId>,
pub target: ObjectId,
pub kind: ObjectKind,
}
#[derive(Debug, thiserror::Error)]
#[error("peeling {original} at {id}")]
#[non_exhaustive]
pub struct PeelError {
pub original: ObjectId,
pub id: ObjectId,
pub from: Option<ObjectId>,
pub depth: usize,
#[source]
pub source: Box<PeelFailure>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PeelFailure {
#[error("missing object")]
Missing,
#[error(transparent)]
Read(#[from] ObjectReadError),
#[error(transparent)]
Tag(#[from] TagError),
#[error(transparent)]
Commit(#[from] crate::CommitError),
#[error("expected {expected:?}, found {actual:?}")]
Kind {
expected: ObjectKind,
actual: ObjectKind,
},
#[error("tag depth limit")]
Depth,
#[error("tag-chain byte limit")]
Bytes,
#[error("tag cycle")]
Cycle,
#[error("tag peeling cancelled")]
Cancelled,
}
impl Objects {
pub fn peel(
&self,
id: ObjectId,
limits: PeelLimits,
cancel: &AtomicBool,
) -> Result<PeeledObject, PeelError> {
#[cfg(feature = "tracing")]
let span = tracing::debug_span!(target: "girt", "objects.peel",
outcome = "incomplete", failure_class = tracing::field::Empty);
let operation = || peel(id, limits, cancel, |id, read| self.read_with(id, read));
#[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::peel);
result
}
}
fn peel(
original: ObjectId,
limits: PeelLimits,
cancel: &AtomicBool,
mut read: impl FnMut(ObjectId, ReadLimits) -> Result<Option<Object>, ObjectReadError>,
) -> Result<PeeledObject, PeelError> {
let mut id = original;
let mut tags = Vec::new();
let mut seen = IdSet::default();
let mut expected = None;
let mut remaining = limits.max_bytes;
loop {
let context = |source| PeelError {
original,
id,
from: tags.last().copied(),
depth: tags.len(),
source: Box::new(source),
};
if cancel.load(Ordering::Relaxed) {
return Err(context(PeelFailure::Cancelled));
}
if !seen.insert(id) {
return Err(context(PeelFailure::Cycle));
}
let mut read_limits = limits.read;
read_limits.max_object_bytes = read_limits.max_object_bytes.min(remaining);
let object = read(id, read_limits);
if cancel.load(Ordering::Relaxed) {
return Err(context(PeelFailure::Cancelled));
}
let object = object
.map_err(|e| context(PeelFailure::Read(e)))?
.ok_or_else(|| context(PeelFailure::Missing))?;
remaining = remaining
.checked_sub(object.data().len())
.ok_or_else(|| context(PeelFailure::Bytes))?;
if let Some(expected) = expected
&& expected != object.kind()
{
return Err(context(PeelFailure::Kind {
expected,
actual: object.kind(),
}));
}
if object.kind() != ObjectKind::Tag {
if object.kind() == ObjectKind::Commit {
crate::Commit::parse_graph(id.format(), object.data())
.map_err(|e| context(PeelFailure::Commit(e)))?;
}
return Ok(PeeledObject {
original,
tags,
target: id,
kind: object.kind(),
});
}
if tags.len() == limits.max_tags {
return Err(context(PeelFailure::Depth));
}
let (target, kind) = Tag::parse_target(id.format(), object.data())
.map_err(|e| context(PeelFailure::Tag(e)))?;
expected = Some(kind);
tags.push(id);
id = target;
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
use crate::{ObjectFormat, TagFields};
fn link(format: ObjectFormat, target: ObjectId) -> Object {
let tag = Tag::new(TagFields {
target,
target_kind: ObjectKind::Tag,
name: b"v".to_vec(),
tagger: None,
extra_headers: vec![],
message: vec![],
})
.unwrap();
Object {
format,
kind: ObjectKind::Tag,
data: tag.encode().into(),
}
}
#[rstest]
fn cycle_retains_failing_link(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
) {
let id = ObjectId::null(format);
let error = peel(
id,
PeelLimits::default(),
&AtomicBool::new(false),
|_, _| Ok(Some(link(format, id))),
)
.unwrap_err();
assert_eq!(
(error.original, error.id, error.from, error.depth),
(id, id, Some(id), 1)
);
assert!(matches!(*error.source, PeelFailure::Cycle));
}
#[test]
fn cancellation_after_read_prevents_target_following() {
let cancel = AtomicBool::new(false);
let id = ObjectId::null(ObjectFormat::Sha1);
let error = peel(id, PeelLimits::default(), &cancel, |_, _| {
cancel.store(true, Ordering::Relaxed);
Ok(Some(link(ObjectFormat::Sha1, id)))
})
.unwrap_err();
assert_eq!(error.depth, 0);
assert!(matches!(*error.source, PeelFailure::Cancelled));
}
#[test]
fn byte_budget_is_checked_even_for_a_misbehaving_reader() {
let id = ObjectId::null(ObjectFormat::Sha1);
let error = peel(
id,
PeelLimits {
max_bytes: 0,
..PeelLimits::default()
},
&AtomicBool::new(false),
|_, _| Ok(Some(link(ObjectFormat::Sha1, id))),
)
.unwrap_err();
assert!(matches!(*error.source, PeelFailure::Bytes));
}
}