use anyhow::{bail, Result};
use std::io::Write;
use std::path::{Path, PathBuf};
pub(crate) fn sync_directory(path: &Path) -> Result<()> {
#[cfg(unix)]
std::fs::File::open(path)?.sync_all()?;
#[cfg(not(unix))]
let _ = path;
Ok(())
}
pub(crate) fn sync_mapped_file(file: &std::fs::File) -> std::io::Result<()> {
#[cfg(target_os = "macos")]
{
use std::os::fd::AsRawFd;
use std::os::raw::c_int;
extern "C" {
fn fcntl(fd: c_int, cmd: c_int, ...) -> c_int;
fn fsync(fd: c_int) -> c_int;
}
const F_FULLFSYNC: c_int = 51;
let fd = file.as_raw_fd();
loop {
if unsafe { fcntl(fd, F_FULLFSYNC) } == 0 {
return Ok(());
}
let error = std::io::Error::last_os_error();
if error.kind() != std::io::ErrorKind::Interrupted {
tracing::debug!(%error, "F_FULLFSYNC refused; falling back to fsync");
break;
}
}
loop {
if unsafe { fsync(fd) } == 0 {
return Ok(());
}
let error = std::io::Error::last_os_error();
if error.kind() != std::io::ErrorKind::Interrupted {
return Err(error);
}
}
}
#[cfg(not(target_os = "macos"))]
{
let _ = file;
Ok(())
}
}
pub(crate) fn scrub_remove_file(path: &Path) -> Result<()> {
use std::io::{Seek, SeekFrom};
if path.symlink_metadata()?.file_type().is_symlink() {
return Ok(std::fs::remove_file(path)?);
}
let mut file = std::fs::OpenOptions::new().write(true).open(path)?;
let mut remaining = file.metadata()?.len();
let zeros = vec![0u8; 1 << 20];
file.seek(SeekFrom::Start(0))?;
while remaining > 0 {
let n = remaining.min(zeros.len() as u64) as usize;
file.write_all(&zeros[..n])?;
remaining -= n as u64;
}
file.sync_all()?;
drop(file);
std::fs::remove_file(path)?;
Ok(())
}
pub(crate) fn scrub_remove_dir(dir: &Path) -> Result<()> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let kind = entry.file_type()?;
if kind.is_dir() {
scrub_remove_dir(&entry.path())?;
} else {
scrub_remove_file(&entry.path())?;
}
}
std::fs::remove_dir(dir)?;
sync_directory(dir.parent().unwrap_or_else(|| Path::new(".")))
}
pub(crate) const CHECKSUMMED_MANIFESTS: [&str; 2] = ["store.json", "format.json"];
fn sidecar(path: &Path) -> Option<PathBuf> {
let name = path.file_name()?.to_str()?;
CHECKSUMMED_MANIFESTS
.contains(&name)
.then(|| path.with_file_name(format!("{name}.crc32")))
}
pub(crate) fn atomic_write(path: &Path, bytes: &[u8]) -> Result<()> {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
if let Some(side) = sidecar(path) {
write_file(
&side,
format!("{:08x}\n", crc32fast::hash(bytes)).as_bytes(),
)?;
}
write_file(path, bytes)?;
sync_directory(parent)
}
fn write_file(path: &Path, bytes: &[u8]) -> Result<()> {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let mut temp = tempfile::NamedTempFile::new_in(parent)?;
temp.write_all(bytes)?;
temp.as_file().sync_all()?;
sync_mapped_file(temp.as_file())?;
temp.persist(path)?;
Ok(())
}
pub(crate) fn read_manifest(path: &Path) -> Result<(Vec<u8>, bool)> {
let bytes = std::fs::read(path)?;
let Some(side) = sidecar(path) else {
return Ok((bytes, false));
};
if !side.exists() {
return Ok((bytes, false));
}
let recorded = std::fs::read_to_string(&side)?;
let recorded = u32::from_str_radix(recorded.trim(), 16)
.map_err(|e| anyhow::anyhow!("invalid checksum sidecar {}: {e}", side.display()))?;
let actual = crc32fast::hash(&bytes);
if recorded != actual {
bail!(
"manifest {} fails its checksum (recorded {recorded:08x}, actual {actual:08x}): the store is corrupt",
path.display()
);
}
Ok((bytes, true))
}
pub(crate) fn verify_manifests(store: &Path, arena: &Path) -> Result<Vec<String>> {
let mut verified = Vec::new();
for (dir, name) in [(store, "store.json"), (arena, "format.json")] {
let path = dir.join(name);
if path.exists() && read_manifest(&path)?.1 {
verified.push(name.to_string());
}
}
Ok(verified)
}
pub(crate) fn arena_generation(base: &Path) -> Result<PathBuf> {
let pointer = base.join("CURRENT");
if !pointer.exists() {
return Ok(base.to_path_buf());
}
let name = std::fs::read_to_string(pointer)?;
if !name.starts_with("gen-") || !name.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-') {
bail!("invalid arena generation pointer");
}
let path = base.join(name);
if !path.is_dir() || path.symlink_metadata()?.file_type().is_symlink() {
bail!("arena generation is missing or is a symbolic link");
}
Ok(path)
}