use std::path::Path;
use crate::fs::vfs_join;
#[cfg_attr(not(feature = "failpoints"), allow(dead_code))]
pub(crate) struct AtomicFailpoints {
pub(crate) allocate: &'static str,
pub(crate) chmod: &'static str,
pub(crate) rename: &'static str,
}
pub(crate) async fn atomic_replace(
fs: &dyn crate::fs::FileSystem,
target: &Path,
content: &[u8],
tool: &str,
failpoints: AtomicFailpoints,
) -> std::result::Result<(), String> {
let parent = target.parent().unwrap_or_else(|| Path::new("/"));
#[cfg(feature = "failpoints")]
if injected_failure(failpoints.allocate, "exhausted") {
return Err("cannot allocate temporary file".to_string());
}
let mut temp = None;
for _ in 0..16 {
let mut suffix = [0u8; 8];
getrandom::fill(&mut suffix)
.map_err(|_| "cannot generate temporary filename".to_string())?;
let suffix = suffix
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
let candidate = vfs_join(parent, format!(".bashkit-{tool}-{suffix}.tmp"));
match fs.exists(&candidate).await {
Ok(false) => {
temp = Some(candidate);
break;
}
Ok(true) => {}
Err(_) => return direct_replace(fs, target, content).await,
}
}
let temp = temp.ok_or_else(|| "cannot allocate temporary file".to_string())?;
if fs.write_file(&temp, content).await.is_err() {
let _ = fs.remove(&temp, false).await;
return direct_replace(fs, target, content).await;
}
#[cfg(feature = "failpoints")]
if injected_failure(failpoints.chmod, "error") {
let _ = fs.remove(&temp, false).await;
return Err("cannot preserve file mode: injected failure".to_string());
}
if let Ok(metadata) = fs.stat(target).await
&& let Err(error) = fs.chmod(&temp, metadata.mode).await
{
let _ = fs.remove(&temp, false).await;
return Err(format!("cannot preserve file mode: {error}"));
}
#[cfg(feature = "failpoints")]
if injected_failure(failpoints.rename, "error") {
let _ = fs.remove(&temp, false).await;
return Err(format!(
"cannot replace '{}': injected failure",
target.display()
));
}
if let Err(error) = fs.rename(&temp, target).await {
let _ = fs.remove(&temp, false).await;
return Err(format!("cannot replace '{}': {error}", target.display()));
}
#[cfg(not(feature = "failpoints"))]
let _ = failpoints;
Ok(())
}
async fn direct_replace(
fs: &dyn crate::fs::FileSystem,
target: &Path,
content: &[u8],
) -> std::result::Result<(), String> {
let mode = fs.stat(target).await.ok().map(|metadata| metadata.mode);
fs.write_file(target, content)
.await
.map_err(|error| format!("cannot write '{}': {error}", target.display()))?;
if let Some(mode) = mode
&& fs.stat(target).await.map(|m| m.mode).ok() != Some(mode)
{
fs.chmod(target, mode)
.await
.map_err(|error| format!("cannot preserve file mode: {error}"))?;
}
Ok(())
}
#[cfg(feature = "failpoints")]
fn injected_failure(name: &str, expected: &str) -> bool {
fail::eval(name, |action| action.as_deref() == Some(expected)).unwrap_or(false)
}