use super::dir::{self, Dir, Kind, OpenError};
use std::ffi::{OsStr, OsString};
use std::io::Write;
use std::path::{Path, PathBuf};
pub(crate) struct Pending {
pub display: String,
pub target: PathBuf,
pub dir: Dir,
pub name: OsString,
pub original: Option<Vec<u8>>,
pub identity: Option<(u64, u64)>,
pub mode: Option<u32>,
pub new: Vec<u8>,
}
pub(crate) struct Staged {
name: OsString,
identity: Option<(u64, u64)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Primitive {
RenameNoReplace,
Link,
UnlinkTemp,
}
pub(crate) trait Hooks {
fn after_stage(&mut self) {}
fn finalize(&mut self, _index: usize, staged: &Staged, pending: &Pending) -> Published {
#[cfg(test)]
let mut unsupported = |p: Primitive| self.unsupported(p);
#[cfg(not(test))]
let mut unsupported = |_: Primitive| false;
finalize(staged, pending, &mut unsupported)
}
#[cfg(test)]
fn temp_written(&mut self, _file: &std::fs::File) -> std::io::Result<()> {
Ok(())
}
#[cfg(test)]
fn unsupported(&mut self, _primitive: Primitive) -> bool {
false
}
#[cfg(test)]
fn before_read(&mut self) {}
}
pub(crate) struct RealHooks;
impl Hooks for RealHooks {}
pub(crate) type Published = std::io::Result<Option<String>>;
pub(crate) fn finalize(
staged: &Staged,
pending: &Pending,
unsupported: &mut dyn FnMut(Primitive) -> bool,
) -> Published {
if pending.original.is_some() {
return pending.dir.rename(&staged.name, &pending.name).map(|()| None);
}
create(staged, pending, unsupported)
}
fn create(
staged: &Staged,
pending: &Pending,
unsupported: &mut dyn FnMut(Primitive) -> bool,
) -> Published {
let dir = &pending.dir;
let not_here = || std::io::Error::from(std::io::ErrorKind::Unsupported);
let renamed = if unsupported(Primitive::RenameNoReplace) {
Err(not_here())
} else {
dir.rename_noreplace(&staged.name, &pending.name)
};
match renamed {
Ok(()) => return Ok(None),
Err(e) if !dir::rename_unsupported(&e) => return Err(e),
Err(_) => {}
}
let linked = if unsupported(Primitive::Link) {
Err(not_here())
} else {
dir.link(&staged.name, &pending.name)
};
match linked {
Ok(()) => {
let removed = if unsupported(Primitive::UnlinkTemp) {
Err(not_here().to_string())
} else {
remove_if_ours(dir, &staged.name, staged.identity)
};
Ok(removed.err().map(|why| {
format!(
"{} published; could not remove temp {}: {why}",
pending.display,
pending.target.with_file_name(&staged.name).display()
)
}))
}
Err(e) if dir::link_unsupported(&e) => Err(std::io::Error::other(format!(
"cannot create {} without risking an overwrite on this filesystem",
pending.display
))),
Err(e) => Err(e),
}
}
fn temp_name(p: &Pending) -> OsString {
super::super::util::unique_tmp_path(Path::new(&p.name)).into_os_string()
}
fn stage(p: &Pending, bytes: &[u8], hooks: &mut dyn Hooks) -> std::io::Result<Staged> {
write_new(&p.dir, &temp_name(p), bytes, p.mode, hooks)
}
#[cfg_attr(not(test), allow(unused_variables))]
pub(crate) fn write_new(
dir: &Dir,
name: &OsStr,
bytes: &[u8],
mode: Option<u32>,
hooks: &mut dyn Hooks,
) -> std::io::Result<Staged> {
let mut file = dir.create_new(name, if mode.is_some() { 0o600 } else { 0o666 })?;
let created = dir::meta_of_file(&file).map(|meta| meta.identity);
let written: std::io::Result<Option<(u64, u64)>> = (|| {
file.write_all(bytes)?;
#[cfg(test)]
hooks.temp_written(&file)?;
#[cfg(unix)]
if let Some(mode) = mode {
use std::os::unix::fs::PermissionsExt;
file.set_permissions(std::fs::Permissions::from_mode(mode))?;
}
Ok(dir::meta_of_file(&file)?.identity)
})();
match written {
Ok(identity) => Ok(Staged {
name: name.to_os_string(),
identity,
}),
Err(e) => {
if let Ok(identity) = created {
let _ = remove_if_ours(dir, name, identity);
}
Err(e)
}
}
}
type ReadBack = (Vec<u8>, Option<(u64, u64)>);
fn read_target(p: &Pending, cap: usize) -> Result<ReadBack, OpenError> {
let (file, meta) = p.dir.open_regular(&p.name)?;
let bytes = dir::read_capped(file, cap).map_err(OpenError::Io)?;
Ok((bytes, meta.identity))
}
fn unchanged(p: &Pending, staged: &Staged) -> Result<(), String> {
if !p.dir.still_there() {
return Err(format!("the parent directory of {} was replaced", p.display));
}
match p.dir.lstat(&staged.name) {
Ok(meta) if meta.kind == Kind::File && meta.identity == staged.identity => {}
_ => return Err(format!("the staged temp of {} was replaced", p.display)),
}
match &p.original {
None => match p.dir.lstat(&p.name) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
_ => Err(format!("{} was created by something else during the call", p.display)),
},
Some(original) => {
let (now, identity) = read_target(p, original.len() + 1).map_err(|e| match e {
OpenError::NotRegular | OpenError::Symlink => {
format!("{} was replaced since it was read", p.display)
}
OpenError::Io(e) => format!("{} can no longer be read: {e}", p.display),
})?;
if identity != p.identity {
return Err(format!("{} was replaced since it was read", p.display));
}
if &now != original {
return Err(format!("{} changed since it was read", p.display));
}
Ok(())
}
}
}
fn restore(p: &Pending, hooks: &mut dyn Hooks) -> Result<(), String> {
let (current, _) =
read_target(p, p.new.len() + 1).map_err(|e| format!("cannot read it back: {e}"))?;
if current != p.new {
return Err("it changed again after this call wrote it; left as is".into());
}
match &p.original {
None => p.dir.unlink(&p.name).map_err(|e| format!("cannot remove it: {e}")),
Some(original) => {
let tmp = stage(p, original, hooks).map_err(|e| format!("staging: {e}"))?;
p.dir.rename(&tmp.name, &p.name).map_err(|e| {
remove_temp(p, &tmp);
format!("rename: {e}")
})
}
}
}
fn remove_temp(p: &Pending, staged: &Staged) {
let _ = remove_if_ours(&p.dir, &staged.name, staged.identity);
}
fn remove_if_ours(dir: &Dir, name: &OsStr, identity: Option<(u64, u64)>) -> Result<(), String> {
match dir.lstat(name) {
Ok(meta) if meta.kind == Kind::File && meta.identity == identity => {
dir.unlink(name).map_err(|e| e.to_string())
}
Ok(_) => Err("another file has taken its name; left as is".into()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e.to_string()),
}
}
fn remove_all(files: &[Pending], temps: &[Staged]) {
for (p, staged) in files.iter().zip(temps) {
remove_temp(p, staged);
}
}
#[cfg(all(test, unix))]
pub(crate) fn create_temp_for_test(parent: &Path, name: &str, bytes: &[u8]) -> std::io::Result<()> {
let dir = Dir::open(&std::fs::canonicalize(parent)?)?;
write_new(&dir, OsStr::new(name), bytes, None, &mut RealHooks).map(drop)
}
pub(crate) fn commit(files: &[Pending], hooks: &mut dyn Hooks) -> Result<Vec<String>, String> {
let mut temps: Vec<Staged> = Vec::with_capacity(files.len());
for p in files {
match stage(p, &p.new, hooks) {
Ok(staged) => temps.push(staged),
Err(e) => {
remove_all(files, &temps);
return Err(format!("staging {}: {e}; no file was changed", p.display));
}
}
}
hooks.after_stage();
for (p, staged) in files.iter().zip(&temps) {
if let Err(e) = unchanged(p, staged) {
remove_all(files, &temps);
return Err(format!("{e}; no file was changed"));
}
}
let mut notes: Vec<(usize, String)> = Vec::new();
for (k, (p, staged)) in files.iter().zip(&temps).enumerate() {
let e = match hooks.finalize(k, staged, p) {
Ok(note) => {
notes.extend(note.map(|note| (k, note)));
continue;
}
Err(e) => e,
};
remove_all(&files[k..], &temps[k..]);
let mut restored = Vec::new();
let mut failed = Vec::new();
for done in &files[..k] {
match restore(done, hooks) {
Ok(()) => restored.push(done.display.clone()),
Err(why) => failed.push(format!("{} ({why})", done.display)),
}
}
let leftovers: String = notes
.iter()
.map(|(j, note)| {
let temp = &temps[*j];
let outcome = match remove_if_ours(&files[*j].dir, &temp.name, temp.identity) {
Ok(()) => "removed on retry".to_string(),
Err(why) => format!("still there: {why}"),
};
format!(" Left over earlier: {note}; {outcome}.")
})
.collect();
let never: Vec<String> = files[k..].iter().map(|f| f.display.clone()).collect();
let list = |v: &[String]| {
if v.is_empty() {
"none".to_string()
} else {
v.join(", ")
}
};
return Err(format!(
"writing {} failed: {e}. Committed then restored: {}. Restore FAILED: {}. Never written: {}.{leftovers}",
p.display,
list(&restored),
list(&failed),
list(&never)
));
}
Ok(notes.into_iter().map(|(_, note)| note).collect())
}