use std::sync::atomic::{AtomicBool, Ordering};
use crate::remote::{Direction, Refspec, Refspecs, RefspecsError};
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct PrefetchLimits {
pub max_specs: usize,
pub max_input_bytes: usize,
pub max_output_bytes: usize,
}
impl Default for PrefetchLimits {
fn default() -> Self {
Self {
max_specs: 1024,
max_input_bytes: 1024 * 1024,
max_output_bytes: 2 * 1024 * 1024,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum PrefetchSkipReason {
NoDestination,
TagSource,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct SkippedPrefetchSpec {
pub index: usize,
pub reason: PrefetchSkipReason,
}
#[derive(Clone, Debug)]
pub struct PrefetchRefspecs {
refspecs: Refspecs,
skipped: Vec<SkippedPrefetchSpec>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum PrefetchRefspecsError {
#[error("invalid prefetch input refspec")]
Input(#[source] RefspecsError),
#[error("invalid rewritten prefetch refspec")]
Output(#[source] RefspecsError),
#[error("prefetch refspec count limit exceeded")]
SpecLimit,
#[error("prefetch refspec input byte limit exceeded")]
InputByteLimit,
#[error("prefetch refspec output byte limit exceeded")]
OutputByteLimit,
#[error("prefetch refspec preparation cancelled")]
Cancelled,
}
impl PrefetchRefspecs {
pub fn parse<I, B>(
values: I,
limits: PrefetchLimits,
cancel: &AtomicBool,
) -> Result<Self, PrefetchRefspecsError>
where
I: IntoIterator<Item = B>,
B: AsRef<[u8]>,
{
check(cancel)?;
let mut input = values.into_iter();
let mut count: usize = 0;
let mut input_bytes: usize = 0;
let mut output_bytes: usize = 0;
let mut output = Vec::new();
let mut skipped = Vec::new();
loop {
if count != 0 && count.is_multiple_of(64) {
check(cancel)?;
}
let Some(value) = input.next() else {
break;
};
if count >= limits.max_specs {
return Err(PrefetchRefspecsError::SpecLimit);
}
let index = count;
count += 1;
let raw = value.as_ref();
input_bytes = input_bytes
.checked_add(raw.len())
.filter(|total| *total <= limits.max_input_bytes)
.ok_or(PrefetchRefspecsError::InputByteLimit)?;
let spec = Refspec::parse(Direction::Fetch, raw)
.map_err(|source| PrefetchRefspecsError::Input(RefspecsError { index, source }))?;
let rewritten = if spec.is_negative() {
charge_output(&mut output_bytes, raw.len(), limits)?;
raw.to_vec()
} else {
let body = raw.strip_prefix(b"+").unwrap_or(raw);
let Some(separator) = body.iter().position(|byte| *byte == b':') else {
skipped.push(SkippedPrefetchSpec {
index,
reason: PrefetchSkipReason::NoDestination,
});
continue;
};
let source = &body[..separator];
let destination = &body[separator + 1..];
let reason = if destination.is_empty() {
Some(PrefetchSkipReason::NoDestination)
} else if source.starts_with(b"refs/tags/") {
Some(PrefetchSkipReason::TagSource)
} else {
None
};
if let Some(reason) = reason {
skipped.push(SkippedPrefetchSpec { index, reason });
continue;
}
let size = raw
.len()
.checked_add(b"prefetch/".len())
.ok_or(PrefetchRefspecsError::OutputByteLimit)?;
charge_output(&mut output_bytes, size, limits)?;
let mut bytes = Vec::with_capacity(size);
if spec.force() {
bytes.push(b'+');
}
bytes.extend_from_slice(source);
bytes.extend_from_slice(b":refs/prefetch/");
bytes.extend_from_slice(&destination[b"refs/".len()..]);
bytes
};
output.push(rewritten);
}
check(cancel)?;
let refspecs =
Refspecs::parse(Direction::Fetch, output).map_err(PrefetchRefspecsError::Output)?;
check(cancel)?;
Ok(Self { refspecs, skipped })
}
pub fn refspecs(&self) -> &Refspecs {
&self.refspecs
}
pub fn skipped(&self) -> &[SkippedPrefetchSpec] {
&self.skipped
}
}
fn charge_output(
used: &mut usize,
size: usize,
limits: PrefetchLimits,
) -> Result<(), PrefetchRefspecsError> {
*used = used
.checked_add(size)
.filter(|total| *total <= limits.max_output_bytes)
.ok_or(PrefetchRefspecsError::OutputByteLimit)?;
Ok(())
}
fn check(cancel: &AtomicBool) -> Result<(), PrefetchRefspecsError> {
if cancel.load(Ordering::Relaxed) {
Err(PrefetchRefspecsError::Cancelled)
} else {
Ok(())
}
}