use std::fs::File;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use tempfile::NamedTempFile;
#[derive(Debug, thiserror::Error)]
pub(crate) enum Error {
#[error("pack publication cancelled")]
Cancelled,
#[error("conflicting existing pack artifact: {0}")]
Existing(PathBuf),
#[error(transparent)]
Io(#[from] io::Error),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum Step {
Marker,
SyncMarker,
Pack,
SyncPack,
Index,
SyncIndex,
}
pub(crate) struct Pair<'a> {
pub(crate) pack: NamedTempFile,
pub(crate) index: NamedTempFile,
pub(crate) basename: &'a Path,
pub(crate) marker: Option<&'a [u8]>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
#[cfg_attr(
not(unix),
allow(dead_code, reason = "only Unix maintenance reads publication progress")
)]
pub(crate) struct Progress {
pub(crate) visible: Vec<PathBuf>,
pub(crate) uncertain: Option<PathBuf>,
}
impl Progress {
#[cfg(any(unix, test))]
pub(crate) fn pair_visible(&self) -> bool {
self.visible
.last()
.is_some_and(|path| path.extension().is_some_and(|ext| ext == "idx"))
}
}
pub(crate) fn pair_checked<E>(
pair: Pair<'_>,
cancel: &AtomicBool,
progress: &mut Progress,
mut before: impl FnMut(Step) -> Result<(), E>,
) -> Result<(), E>
where
E: From<Error>,
{
let Pair {
pack,
index,
basename,
marker,
} = pair;
let directory = basename.parent().expect("pack basename has a directory");
let sync = |before: &mut dyn FnMut(Step) -> Result<(), E>,
step: Step,
path: PathBuf,
progress: &mut Progress|
-> Result<(), E> {
progress.uncertain = Some(path);
before(step)?;
sync_directory(directory).map_err(Error::Io)?;
progress.uncertain = None;
Ok(())
};
if let Some(contents) = marker {
let path = basename.with_extension("promisor");
before(Step::Marker)?;
check(cancel)?;
publish_marker(directory, &path, contents)?;
progress.visible.push(path.clone());
sync(&mut before, Step::SyncMarker, path, progress)?;
}
let path = basename.with_extension("pack");
before(Step::Pack)?;
check(cancel)?;
file(pack, &path, cancel)?;
progress.visible.push(path.clone());
sync(&mut before, Step::SyncPack, path, progress)?;
let path = basename.with_extension("idx");
before(Step::Index)?;
check(cancel)?;
file(index, &path, cancel)?;
progress.visible.push(path.clone());
sync(&mut before, Step::SyncIndex, path, progress)
}
fn publish_marker(directory: &Path, path: &Path, contents: &[u8]) -> Result<(), Error> {
let mut temporary = NamedTempFile::new_in(directory)?;
temporary.write_all(contents)?;
temporary.as_file().sync_all()?;
match temporary.persist_noclobber(path) {
Ok(_) => Ok(()),
Err(error) if error.error.kind() == io::ErrorKind::AlreadyExists => {
if std::fs::symlink_metadata(path)?.is_file() {
Ok(())
} else {
Err(Error::Existing(path.into()))
}
}
Err(error) => Err(error.error.into()),
}
}
pub(crate) fn sync_directory(directory: &Path) -> io::Result<()> {
#[cfg(unix)]
File::open(directory)?.sync_all()?;
#[cfg(not(unix))]
let _ = directory;
Ok(())
}
pub(crate) fn file(
temporary: NamedTempFile,
path: &Path,
cancel: &AtomicBool,
) -> Result<(), Error> {
match temporary.persist_noclobber(path) {
Ok(_) => Ok(()),
Err(error) if error.error.kind() == io::ErrorKind::AlreadyExists => {
let mut existing = File::open(path)?;
let mut candidate = error.file.reopen()?;
if existing.metadata()?.len() != candidate.metadata()?.len() {
return Err(Error::Existing(path.into()));
}
let mut a = [0; 8192];
let mut b = [0; 8192];
loop {
check(cancel)?;
let n = existing.read(&mut a)?;
candidate.read_exact(&mut b[..n])?;
if a[..n] != b[..n] {
return Err(Error::Existing(path.into()));
}
if n == 0 {
return Ok(());
}
}
}
Err(error) => Err(error.error.into()),
}
}
fn check(cancel: &AtomicBool) -> Result<(), Error> {
if cancel.load(Ordering::Relaxed) {
Err(Error::Cancelled)
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use tempfile::NamedTempFile;
use super::*;
fn temporary(directory: &Path, bytes: &[u8]) -> NamedTempFile {
let mut file = NamedTempFile::new_in(directory).unwrap();
file.write_all(bytes).unwrap();
file.as_file().sync_all().unwrap();
file
}
#[rstest::rstest]
#[case(false)]
#[case(true)]
fn refuses_conflicting_existing_bytes_without_clobber(#[case] different_length: bool) {
let root = tempfile::tempdir().unwrap();
let path = root.path().join("pack-existing.pack");
let original = vec![b'a'; 17_000];
fs::write(&path, &original).unwrap();
let mut candidate = original.clone();
if different_length {
candidate.push(b'b');
} else {
candidate[16_999] = b'b';
}
let result = file(
temporary(root.path(), &candidate),
&path,
&AtomicBool::new(false),
);
assert!(matches!(result, Err(Error::Existing(ref conflict)) if *conflict == path));
assert_eq!(fs::read(path).unwrap(), original);
}
#[test]
fn identical_existing_artifacts_are_verified_and_reused() {
let root = tempfile::tempdir().unwrap();
let path = root.path().join("pack-existing.pack");
let original = vec![b'a'; 17_000];
fs::write(&path, &original).unwrap();
file(
temporary(root.path(), &original),
&path,
&AtomicBool::new(false),
)
.unwrap();
assert_eq!(fs::read(path).unwrap(), original);
assert_eq!(fs::read_dir(root.path()).unwrap().count(), 1);
}
fn pair<'a>(root: &Path, basename: &'a Path, marker: Option<&'a [u8]>) -> Pair<'a> {
Pair {
pack: temporary(root, b"pack"),
index: temporary(root, b"index"),
basename,
marker,
}
}
#[test]
fn pack_name_is_synced_before_index_visibility() {
let root = tempfile::tempdir().unwrap();
let basename = root.path().join("pack-pair");
let events = RefCell::new(Vec::new());
let mut progress = Progress::default();
pair_checked::<Error>(
pair(root.path(), &basename, None),
&AtomicBool::new(false),
&mut progress,
|step| {
if step == Step::Index {
assert_eq!(fs::read(basename.with_extension("pack")).unwrap(), b"pack");
assert!(!basename.with_extension("idx").exists());
}
events.borrow_mut().push(step);
Ok(())
},
)
.unwrap();
assert_eq!(
*events.borrow(),
[Step::Pack, Step::SyncPack, Step::Index, Step::SyncIndex]
);
assert_eq!(fs::read(basename.with_extension("idx")).unwrap(), b"index");
assert!(!basename.with_extension("promisor").exists());
assert!(progress.pair_visible() && progress.uncertain.is_none());
}
#[test]
fn marker_is_published_before_the_pack() {
let root = tempfile::tempdir().unwrap();
let basename = root.path().join("pack-pair");
let marker = basename.with_extension("promisor");
let events = RefCell::new(Vec::new());
let mut progress = Progress::default();
pair_checked::<Error>(
pair(root.path(), &basename, Some(b"")),
&AtomicBool::new(false),
&mut progress,
|step| {
match step {
Step::Marker => assert!(!marker.exists()),
Step::Pack => {
assert_eq!(fs::read(&marker).unwrap(), b"");
assert!(!basename.with_extension("pack").exists());
}
Step::Index => assert!(!basename.with_extension("idx").exists()),
_ => {}
}
events.borrow_mut().push(step);
Ok(())
},
)
.unwrap();
assert_eq!(
*events.borrow(),
[
Step::Marker,
Step::SyncMarker,
Step::Pack,
Step::SyncPack,
Step::Index,
Step::SyncIndex
]
);
assert_eq!(fs::read(&marker).unwrap(), b"");
assert_eq!(fs::read(basename.with_extension("pack")).unwrap(), b"pack");
assert_eq!(fs::read(basename.with_extension("idx")).unwrap(), b"index");
assert_eq!(fs::read_dir(root.path()).unwrap().count(), 3);
assert_eq!(
progress.visible,
["promisor", "pack", "idx"].map(|ext| basename.with_extension(ext))
);
}
#[rstest::rstest]
#[case::marker(Step::Marker, &[], None)]
#[case::marker_sync(Step::SyncMarker, &["promisor"], Some("promisor"))]
#[case::pack(Step::Pack, &["promisor"], None)]
#[case::pack_sync(Step::SyncPack, &["promisor", "pack"], Some("pack"))]
#[case::index(Step::Index, &["promisor", "pack"], None)]
#[case::final_sync(Step::SyncIndex, &["promisor", "pack", "idx"], Some("idx"))]
fn stopping_at_a_step_reports_exactly_the_visible_names(
#[case] stop: Step,
#[case] visible: &[&str],
#[case] uncertain: Option<&str>,
) {
let root = tempfile::tempdir().unwrap();
let basename = root.path().join("pack-pair");
let mut progress = Progress::default();
let result = pair_checked::<Error>(
pair(root.path(), &basename, Some(b"")),
&AtomicBool::new(false),
&mut progress,
|step| {
if step == stop {
Err(Error::Io(io::Error::other("injected")))
} else {
Ok(())
}
},
);
assert!(matches!(result, Err(Error::Io(_))));
let mut names: Vec<_> = fs::read_dir(root.path())
.unwrap()
.map(|entry| entry.unwrap().path())
.filter(|path| path.file_stem() == basename.file_stem())
.map(|path| path.extension().unwrap().to_str().unwrap().to_owned())
.collect();
names.sort();
let mut expected = visible.to_vec();
expected.sort();
assert_eq!(names, expected);
let reported: Vec<_> = visible
.iter()
.map(|ext| basename.with_extension(ext))
.collect();
assert_eq!(progress.visible, reported);
assert_eq!(
progress.uncertain,
uncertain.map(|ext| basename.with_extension(ext))
);
assert_eq!(progress.pair_visible(), stop == Step::SyncIndex);
}
#[test]
fn existing_marker_file_is_kept() {
let root = tempfile::tempdir().unwrap();
let basename = root.path().join("pack-pair");
let marker = basename.with_extension("promisor");
fs::write(&marker, b"advertised tips\n").unwrap();
pair_checked::<Error>(
pair(root.path(), &basename, Some(b"")),
&AtomicBool::new(false),
&mut Progress::default(),
|_| Ok(()),
)
.unwrap();
assert_eq!(fs::read(marker).unwrap(), b"advertised tips\n");
assert_eq!(fs::read_dir(root.path()).unwrap().count(), 3);
}
#[test]
fn non_file_marker_refuses_before_pack_visibility() {
let root = tempfile::tempdir().unwrap();
let basename = root.path().join("pack-pair");
let marker = basename.with_extension("promisor");
fs::create_dir(&marker).unwrap();
let result = pair_checked::<Error>(
pair(root.path(), &basename, Some(b"")),
&AtomicBool::new(false),
&mut Progress::default(),
|_| Ok(()),
);
assert!(matches!(result, Err(Error::Existing(ref path)) if *path == marker));
assert!(!basename.with_extension("pack").exists());
}
#[test]
fn cancellation_between_names_preserves_only_unindexed_pack() {
let root = tempfile::tempdir().unwrap();
let basename = root.path().join("pack-pair");
let cancel = AtomicBool::new(false);
let result = pair_checked::<Error>(
pair(root.path(), &basename, None),
&cancel,
&mut Progress::default(),
|step| {
if step == Step::Index {
cancel.store(true, Ordering::Relaxed);
}
Ok(())
},
);
assert!(matches!(result, Err(Error::Cancelled)));
assert_eq!(fs::read(basename.with_extension("pack")).unwrap(), b"pack");
assert!(!basename.with_extension("idx").exists());
}
}