use std::io::Read;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use super::{OpenError, Repository, RepositoryLocation, RepositoryMetadata, ShallowRoots};
use crate::config::{ConfigInputs, IncludePlacement, ResolveLimits};
use crate::refs::reftable::StackLimits;
#[derive(Clone, Copy, Debug)]
#[non_exhaustive]
pub struct OpenLimits {
pub metadata_bytes: usize,
pub shallow_bytes: usize,
pub configuration: ResolveLimits,
pub references: StackLimits,
}
impl Default for OpenLimits {
fn default() -> Self {
Self {
metadata_bytes: 64 * 1024,
shallow_bytes: 16 * 1024 * 1024,
configuration: ResolveLimits::default(),
references: StackLimits::default(),
}
}
}
impl OpenLimits {
#[must_use]
pub fn with_metadata_bytes(mut self, bytes: usize) -> Self {
self.metadata_bytes = bytes;
self
}
#[must_use]
pub fn with_shallow_bytes(mut self, bytes: usize) -> Self {
self.shallow_bytes = bytes;
self
}
#[must_use]
pub fn with_configuration(mut self, limits: ResolveLimits) -> Self {
self.configuration = limits;
self
}
#[must_use]
pub fn with_references(mut self, limits: StackLimits) -> Self {
self.references = limits;
self
}
}
#[derive(Clone, Debug, Default)]
pub struct OpenOptions {
limits: OpenLimits,
}
impl OpenOptions {
#[must_use]
pub fn with_limits(mut self, limits: OpenLimits) -> Self {
self.limits = limits;
self
}
pub fn open(
&self,
path: impl AsRef<Path>,
cancel: &AtomicBool,
) -> Result<Repository, OpenError> {
let mut control = Control::<true>::new(self.limits.metadata_bytes, cancel);
control.phase()?;
let location = RepositoryLocation::resolve_storage_with(
path.as_ref(),
false,
None,
None,
&mut control,
)?;
let inputs = ConfigInputs::default().with_limits(self.limits.configuration);
let metadata = RepositoryMetadata::read_location_with(
&location,
&inputs,
self.limits.references,
None,
IncludePlacement::InPlace,
&mut control,
)?;
control.phase()?;
let shallow = ShallowRoots::read(
metadata.common_dir.join("shallow"),
metadata.object_format,
self.limits.shallow_bytes,
cancel,
);
control.phase()?;
Ok(metadata.into_storage(shallow?))
}
}
static NEVER_CANCELLED: AtomicBool = AtomicBool::new(false);
pub(super) struct Control<'a, const CONTROLLED: bool> {
remaining: usize,
cancel: &'a AtomicBool,
}
impl Control<'static, false> {
pub(super) fn trusted() -> Self {
Self::new(0, &NEVER_CANCELLED)
}
}
impl<'a, const CONTROLLED: bool> Control<'a, CONTROLLED> {
fn new(remaining: usize, cancel: &'a AtomicBool) -> Self {
Self { remaining, cancel }
}
#[inline]
pub(super) fn phase(&self) -> Result<(), OpenError> {
if CONTROLLED && self.cancel.load(Ordering::Relaxed) {
return Err(OpenError::Cancelled);
}
Ok(())
}
pub(super) fn flag(&self) -> &AtomicBool {
self.cancel
}
#[inline]
pub(super) fn read(&mut self, path: &Path) -> Result<Vec<u8>, OpenError> {
if !CONTROLLED {
return super::read(path);
}
self.phase()?;
let metadata = std::fs::metadata(path).map_err(|error| {
if error.kind() == std::io::ErrorKind::NotFound {
super::malformed(path, "missing required metadata")
} else {
super::io_error(path, error)
}
})?;
if !metadata.is_file() {
return Err(super::unsupported(path, "nonregular repository metadata"));
}
if metadata.len() > self.remaining as u64 {
return Err(OpenError::Limit("metadata bytes"));
}
let file = std::fs::File::open(path).map_err(|error| super::io_error(path, error))?;
let mut reader = file.take(self.remaining.saturating_add(1) as u64);
let mut bytes = Vec::new();
let mut chunk = [0; 8192];
loop {
self.phase()?;
let count = reader
.read(&mut chunk)
.map_err(|error| super::io_error(path, error))?;
if count == 0 {
break;
}
if count > self.remaining.saturating_sub(bytes.len()) {
return Err(OpenError::Limit("metadata bytes"));
}
bytes.extend_from_slice(&chunk[..count]);
}
self.remaining -= bytes.len();
self.phase()?;
Ok(bytes)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn controlled_reader_charges_each_observation_and_checks_before_the_next() {
let root = tempfile::tempdir().unwrap();
let path = root.path().join("metadata");
std::fs::write(&path, b"abcd").unwrap();
let cancel = AtomicBool::new(false);
let mut control = Control::<true>::new(8, &cancel);
assert_eq!(control.read(&path).unwrap(), b"abcd");
assert_eq!(control.read(&path).unwrap(), b"abcd");
assert!(matches!(control.read(&path), Err(OpenError::Limit(_))));
cancel.store(true, Ordering::Relaxed);
assert!(matches!(
control.read(&root.path().join("absent")),
Err(OpenError::Cancelled)
));
}
#[test]
fn trusted_specialization_ignores_budgets_and_cancellation() {
let root = tempfile::tempdir().unwrap();
let path = root.path().join("metadata");
std::fs::write(&path, b"abcd").unwrap();
let cancel = AtomicBool::new(true);
let mut control = Control::<false>::new(0, &cancel);
assert_eq!(control.read(&path).unwrap(), b"abcd");
assert!(control.phase().is_ok());
}
#[test]
fn cancellation_after_location_resolution_precedes_bootstrap() {
let root = tempfile::tempdir().unwrap();
let repo = Repository::init(
crate::ObjectFormat::Sha1,
root.path().join("r.git"),
crate::InitKind::Bare,
)
.unwrap();
let cancel = AtomicBool::new(false);
let mut control = Control::<true>::new(4096, &cancel);
let location = RepositoryLocation::resolve_storage_with(
repo.git_dir(),
false,
None,
None,
&mut control,
)
.unwrap();
std::fs::write(repo.git_dir().join("config"), b"malformed").unwrap();
cancel.store(true, Ordering::Relaxed);
let result = RepositoryMetadata::read_location_with(
&location,
&ConfigInputs::default(),
StackLimits::default(),
None,
IncludePlacement::InPlace,
&mut control,
);
assert!(matches!(result, Err(OpenError::Cancelled)));
}
}