use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use dynamic_config::Error;
pub(crate) const AFTER: u32 = 32;
pub(crate) const MARKER: &str = "dynamic-config-store";
const MARKER_TEXT: &str = "\
This directory is the object cache of a `dynamic-config-git` source.
Everything in it was fetched by that crate, and that crate empties it once it
holds more packs than `GitSource::builder(..).compact_after(..)` allows —
re-fetching what it needs. Do not keep anything here.
";
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
static CLAIMED: Mutex<Option<HashSet<PathBuf>>> = Mutex::new(None);
fn claimed() -> std::sync::MutexGuard<'static, Option<HashSet<PathBuf>>> {
CLAIMED
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[derive(Debug)]
pub(crate) enum Working {
Temporary(Temporary),
Named(Claimed),
}
impl Working {
pub(crate) fn path(&self) -> Result<&Path, Error> {
match self {
Self::Temporary(temporary) => Ok(temporary.path()),
Self::Named(claimed) => {
let path = claimed.path();
if !path.exists() {
create_private(path).map_err(|error| {
Error::remote(format!(
"git: cannot create the working directory {}: {error}",
path.display()
))
})?;
}
Ok(path)
}
}
}
}
pub(crate) fn mark(directory: &Path) -> Result<(), Error> {
std::fs::write(directory.join(MARKER), MARKER_TEXT).map_err(|error| {
Error::remote(format!(
"git: cannot write {} in the working directory {}: {error}",
MARKER,
directory.display()
))
})
}
fn ours(directory: &Path) -> bool {
directory.join(MARKER).is_file()
}
fn packs(directory: &Path) -> usize {
let Ok(entries) = std::fs::read_dir(directory.join("objects").join("pack")) else {
return 0;
};
entries
.flatten()
.filter(|entry| entry.path().extension().is_some_and(|kind| kind == "pack"))
.count()
}
pub(crate) fn compact(directory: &Path, after: u32) -> Result<bool, Error> {
if after == 0 || !ours(directory) || packs(directory) <= after as usize {
return Ok(false);
}
empty(directory).map_err(|error| {
Error::remote(format!(
"git: cannot empty the working directory {}: {error}",
directory.display()
))
})?;
Ok(true)
}
fn empty(directory: &Path) -> std::io::Result<()> {
for entry in std::fs::read_dir(directory)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
std::fs::remove_dir_all(entry.path())?;
} else {
std::fs::remove_file(entry.path())?;
}
}
Ok(())
}
pub(crate) struct Claimed {
path: PathBuf,
identity: PathBuf,
}
impl Claimed {
pub(crate) fn new(path: PathBuf) -> Result<Self, Error> {
let identity = identity(&path);
let mut claimed = claimed();
let taken = claimed.get_or_insert_with(HashSet::new);
if !taken.insert(identity.clone()) {
return Err(Error::remote(format!(
"git: {} is already the working directory of another source in \
this program; two sources fetching into one directory would \
corrupt it, so give each its own",
path.display()
)));
}
Ok(Self { path, identity })
}
fn path(&self) -> &Path {
self.path.as_path()
}
}
fn identity(path: &Path) -> PathBuf {
if let Ok(resolved) = path.canonicalize() {
return resolved;
}
let mut missing = Vec::new();
let mut existing = path;
while let (Some(parent), Some(name)) = (existing.parent(), existing.file_name()) {
missing.push(name.to_owned());
existing = parent;
if let Ok(resolved) = existing.canonicalize() {
let mut identity = resolved;
for name in missing.iter().rev() {
identity.push(name);
}
return identity;
}
}
std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf())
}
impl std::fmt::Debug for Claimed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Claimed").field(&self.path).finish()
}
}
impl Drop for Claimed {
fn drop(&mut self) {
if let Some(taken) = claimed().as_mut() {
taken.remove(&self.identity);
}
}
}
pub(crate) struct Temporary(PathBuf);
impl Temporary {
pub(crate) fn new() -> Result<Self, Error> {
let path = std::env::temp_dir().join(format!(
"dynamic-config-git-{}-{}",
std::process::id(),
SEQUENCE.fetch_add(1, Ordering::Relaxed)
));
create_private(&path).map_err(|error| {
Error::remote(format!(
"git: cannot create a working directory at {}: {error}",
path.display()
))
})?;
Ok(Self(path))
}
pub(crate) fn path(&self) -> &Path {
self.0.as_path()
}
}
impl std::fmt::Debug for Temporary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Temporary").field(&self.0).finish()
}
}
impl Drop for Temporary {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[cfg(unix)]
fn create_private(path: &Path) -> std::io::Result<()> {
use std::os::unix::fs::DirBuilderExt as _;
std::fs::DirBuilder::new().mode(0o700).create(path)
}
#[cfg(not(unix))]
fn create_private(path: &Path) -> std::io::Result<()> {
std::fs::DirBuilder::new().create(path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_temporary_directory_is_private_from_the_moment_it_exists() {
let temporary = Temporary::new().unwrap();
assert!(temporary.path().is_dir());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(temporary.path())
.unwrap()
.permissions()
.mode();
assert_eq!(
mode & 0o777,
0o700,
"objects from a private repository must not be world-readable"
);
}
}
#[test]
fn a_temporary_directory_is_removed_with_its_source() {
let path = {
let temporary = Temporary::new().unwrap();
temporary.path().to_owned()
};
assert!(!path.exists(), "nothing survives the source");
}
#[test]
fn two_sources_never_share_a_temporary_directory() {
let one = Temporary::new().unwrap();
let other = Temporary::new().unwrap();
assert_ne!(one.path(), other.path());
}
#[test]
fn a_named_directory_is_refused_to_a_second_source() {
let directory = Temporary::new().unwrap();
let path = directory.path().join("shared");
let first = Claimed::new(path.clone()).expect("nobody has it yet");
let error = Claimed::new(path.clone())
.expect_err("two sources fetching into one directory would corrupt it");
assert!(
error.to_string().contains("already the working directory"),
"{error}"
);
drop(first);
Claimed::new(path).expect("the claim ends with the source");
}
#[test]
fn one_directory_under_two_spellings_is_one_claim() {
let directory = Temporary::new().unwrap();
let path = directory.path().join("shared");
std::fs::create_dir(&path).unwrap();
let _first = Claimed::new(path.clone()).expect("nobody has it yet");
for spelling in [
directory.path().join(".").join("shared"),
path.join("..").join("shared"),
] {
Claimed::new(spelling.clone())
.expect_err("the same directory, spelled differently, is the same directory");
}
#[cfg(unix)]
{
let link = directory.path().join("by-another-name");
std::os::unix::fs::symlink(&path, &link).unwrap();
Claimed::new(link).expect_err("a symlink to a claimed directory is that directory");
}
}
#[test]
fn a_directory_that_does_not_exist_yet_can_still_be_claimed_once() {
let directory = Temporary::new().unwrap();
let path = directory.path().join("not-yet");
let _first = Claimed::new(path.clone()).expect("nobody has it yet");
Claimed::new(directory.path().join(".").join("not-yet"))
.expect_err("the same directory that does not exist yet is still the same one");
}
fn a_working_directory(marked: bool, count: usize) -> Temporary {
let directory = Temporary::new().unwrap();
let packs = directory.path().join("objects").join("pack");
std::fs::create_dir_all(&packs).unwrap();
for index in 0..count {
std::fs::write(packs.join(format!("pack-{index}.pack")), b"not really").unwrap();
std::fs::write(packs.join(format!("pack-{index}.idx")), b"nor this").unwrap();
}
if marked {
mark(directory.path()).unwrap();
}
directory
}
#[test]
fn a_directory_under_the_bound_is_left_alone_and_one_over_it_is_emptied() {
let directory = a_working_directory(true, 4);
assert!(
!compact(directory.path(), 4).unwrap(),
"four packs is not more than four"
);
assert!(directory.path().join("objects").is_dir());
let directory = a_working_directory(true, 5);
assert!(compact(directory.path(), 4).unwrap(), "five is");
assert!(
!directory.path().join("objects").exists(),
"the object database is what compaction removes"
);
assert_eq!(
std::fs::read_dir(directory.path()).unwrap().count(),
0,
"and it leaves nothing behind but the directory itself"
);
assert!(directory.path().is_dir());
}
#[test]
fn a_directory_this_crate_did_not_create_is_never_emptied() {
let directory = a_working_directory(false, 50);
assert!(
!compact(directory.path(), 4).unwrap(),
"no marker, no delete — at any size"
);
assert_eq!(
std::fs::read_dir(directory.path().join("objects").join("pack"))
.unwrap()
.count(),
100,
"somebody else's packs are still there"
);
}
#[test]
fn compaction_can_be_turned_off_entirely() {
let directory = a_working_directory(true, 50);
assert!(
!compact(directory.path(), 0).unwrap(),
"`compact_after(0)` is a caller who runs their own `git gc`"
);
assert!(directory.path().join("objects").is_dir());
}
}