#[cfg(not(unix))]
use std::fs as stdfs;
use std::path::Path;
#[cfg(not(unix))]
use std::path::PathBuf;
#[cfg(unix)]
mod fd;
#[cfg(not(unix))]
use super::{atomic_write, current_hash};
use super::{PlanEntry, WritePayload};
pub(super) enum Step {
Restored,
Refused(Option<String>),
}
#[cfg(all(test, unix))]
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) enum Stage {
BeforeWalk,
AfterWalk,
}
#[cfg(all(test, unix))]
type RestoreHook = Box<dyn Fn(Stage)>;
#[cfg(all(test, unix))]
thread_local! {
pub(super) static RESTORE_HOOK: std::cell::RefCell<Option<RestoreHook>> =
const { std::cell::RefCell::new(None) };
}
#[cfg(all(test, unix))]
pub(super) fn restore_hook(stage: Stage) {
RESTORE_HOOK.with(|hook| {
if let Some(hook) = hook.borrow().as_ref() {
hook(stage);
}
});
}
#[cfg(unix)]
pub(super) type Anchor = fd::Root;
#[cfg(not(unix))]
pub(super) struct Anchor(PathBuf);
#[cfg(unix)]
pub(super) fn open_anchor(root: &Path) -> Result<Anchor, String> {
fd::Root::open(root)
}
#[cfg(not(unix))]
pub(super) fn open_anchor(root: &Path) -> Result<Anchor, String> {
Ok(Anchor(root.to_path_buf()))
}
#[cfg(unix)]
pub(super) fn restore_one(
anchor: &Anchor,
entry: &PlanEntry,
guard: &Option<String>,
write: Option<&WritePayload>,
) -> Result<Step, String> {
#[cfg(all(test, unix))]
restore_hook(Stage::BeforeWalk);
let parent = anchor.parent_of(&entry.path, write.is_some())?;
#[cfg(all(test, unix))]
restore_hook(Stage::AfterWalk);
let Some(parent) = parent else {
return match guard {
None if write.is_none() => Ok(Step::Restored),
None => Err(format!("{}: parent directory vanished", entry.path)),
Some(_) => Ok(Step::Refused(None)),
};
};
let found = parent.hash()?;
if found != *guard {
return Ok(Step::Refused(found));
}
match write {
Some(payload) => {
parent.write(&payload.bytes, payload.exec, |dir| {
if dir.hash()? == *guard {
Ok(())
} else {
Err(format!("{} changed while restoring", entry.path))
}
})?;
}
None => parent.delete()?,
}
Ok(Step::Restored)
}
#[cfg(not(unix))]
pub(super) fn restore_one(
anchor: &Anchor,
entry: &PlanEntry,
guard: &Option<String>,
write: Option<&WritePayload>,
) -> Result<Step, String> {
let root = &anchor.0;
let full = root.join(&entry.path);
let found = current_hash(&full)?;
if found != *guard {
return Ok(Step::Refused(found));
}
ensure_plain_ancestors(root, &full)?;
match write {
Some(payload) => {
if let Some(parent) = full.parent() {
stdfs::create_dir_all(parent)
.map_err(|e| format!("create {}: {e}", parent.display()))?;
}
atomic_write(&full, &payload.bytes)?;
set_executable(&full, payload.exec);
}
None => {
stdfs::remove_file(&full).map_err(|e| format!("delete {}: {e}", full.display()))?;
let mut dir = full.parent();
while let Some(parent) = dir.filter(|d| *d != root && d.starts_with(root)) {
if stdfs::remove_dir(parent).is_err() {
break;
}
dir = parent.parent();
}
}
}
Ok(Step::Restored)
}
#[cfg(not(unix))]
fn ensure_plain_ancestors(root: &Path, full: &Path) -> Result<(), String> {
let mut dir = full.parent();
let mut chain = Vec::new();
while let Some(d) = dir.filter(|d| *d != root && d.starts_with(root)) {
chain.push(d);
dir = d.parent();
}
for d in chain.into_iter().rev() {
match stdfs::symlink_metadata(d) {
Ok(meta) if meta.is_dir() => {}
Ok(_) => {
return Err(format!(
"{} is a symlink or not a directory; refusing to write through it",
d.display()
))
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => break,
Err(e) => return Err(format!("stat {}: {e}", d.display())),
}
}
Ok(())
}
#[cfg(not(unix))]
fn set_executable(_path: &Path, _exec: bool) {}