use std::collections::HashSet;
use std::ffi::OsString;
use std::fs;
use std::io::{self, Write};
use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt};
use std::path::Path;
use log::error;
use rand::{rngs::OsRng, Rng};
use super::error::Error;
pub fn spit(
tmp: impl AsRef<Path>,
path: impl AsRef<Path>,
overwrite: bool,
mode: u32,
data: &[u8],
) -> io::Result<()> {
let mut tf = tempfile::NamedTempFile::new_in(tmp)?;
tf.as_file_mut().write_all(data)?;
chmod(tf.path(), mode)?;
tf.as_file_mut().sync_all()?;
if overwrite {
tf.persist(path)?;
} else {
tf.persist_noclobber(path)?;
}
Ok(())
}
pub fn delete_atomically(
target: impl AsRef<Path>,
garbage: impl AsRef<Path>,
) -> io::Result<()> {
let target = target.as_ref();
let garbage = garbage.as_ref();
loop {
let name = format!("garbage.{}", OsRng.gen::<u64>());
let dst = garbage.join(name);
match fs::rename(target, &dst) {
Ok(()) => {
if let Err(e) = fs::remove_dir_all(&dst) {
error!("Failed to remove {}: {}", dst.display(), e);
}
break;
},
Err(e) if io::ErrorKind::AlreadyExists == e.kind() => continue,
Err(e) => return Err(e),
}
}
Ok(())
}
pub fn replicate_directory_for_chroot(
src: &Path,
dst: &Path,
) -> io::Result<()> {
if fs::symlink_metadata(dst)
.ok()
.is_some_and(|md| !md.is_dir())
{
fs::remove_file(dst).ignore_not_found()?;
}
fs::DirBuilder::new()
.recursive(true)
.mode(0o755)
.create(dst)
.ignore_already_exists()?;
let mut known_files = HashSet::<OsString>::new();
for entry in fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let file_name = entry.file_name();
let dst_path = dst.join(&file_name);
let src_md = match fs::metadata(&src_path) {
Ok(md) => md,
Err(e) if io::ErrorKind::NotFound == e.kind() => continue,
Err(e) => {
error!("stat {}: {}", src_path.display(), e);
continue;
},
};
if 0 == src_md.mode() & 0o004 {
continue;
}
known_files.insert(file_name);
if src_md.is_dir() {
replicate_directory_for_chroot(&src_path, &dst_path)?;
} else if src_md.is_file() {
if let Ok(dst_md) = fs::symlink_metadata(&dst_path) {
if !dst_md.is_file() {
if let Err(e) = remove_any(&dst_path) {
error!("remove {}: {}", dst_path.display(), e);
continue;
}
} else if (src_md.dev(), src_md.ino())
== (dst_md.dev(), dst_md.ino())
|| (src_md.size() == dst_md.size()
&& src_md.mtime() <= dst_md.mtime())
{
continue;
} else {
if let Err(e) =
fs::remove_file(&dst_path).ignore_not_found()
{
error!("remove {}: {}", dst_path.display(), e);
continue;
}
}
}
match nix::unistd::linkat(
None,
&src_path,
None,
&dst_path,
nix::unistd::LinkatFlags::SymlinkFollow,
) {
Ok(()) => (),
Err(nix::errno::Errno::EXDEV) => {
if let Err(e) = fs::copy(&src_path, &dst_path) {
error!(
"copy {} => {}: {}",
src_path.display(),
dst_path.display(),
e,
);
}
},
Err(e) => {
error!(
"linkat {} => {}: {}",
src_path.display(),
dst_path.display(),
e,
);
},
}
}
}
for entry in fs::read_dir(dst)? {
let entry = entry?;
if !known_files.contains(&entry.file_name()) {
let path = entry.path();
if let Err(e) = remove_any(&path) {
error!("remove {}: {}", path.display(), e);
}
}
}
Ok(())
}
fn remove_any(path: &Path) -> io::Result<()> {
fs::remove_file(path).or_else(|_| fs::remove_dir_all(path))
}
pub fn chmod(path: impl AsRef<Path>, mode: u32) -> io::Result<()> {
fs::set_permissions(path, fs::Permissions::from_mode(mode))
}
pub trait IgnoreKinds {
fn ignore_already_exists(self) -> Self;
fn ignore_not_found(self) -> Self;
}
impl<R: Default> IgnoreKinds for Result<R, io::Error> {
fn ignore_already_exists(self) -> Self {
match self {
Ok(r) => Ok(r),
Err(e) if io::ErrorKind::AlreadyExists == e.kind() => {
Ok(R::default())
},
Err(e) => Err(e),
}
}
fn ignore_not_found(self) -> Self {
match self {
Ok(r) => Ok(r),
Err(e) if io::ErrorKind::NotFound == e.kind() => Ok(R::default()),
Err(e) => Err(e),
}
}
}
pub trait ErrorTransforms {
type Coerced;
fn on_exists(self, error: Error) -> Self::Coerced;
fn on_not_found(self, error: Error) -> Self::Coerced;
}
impl<R, E: Into<Error>> ErrorTransforms for Result<R, E> {
type Coerced = Result<R, Error>;
fn on_exists(self, error: Error) -> Result<R, Error> {
match self.map_err(|e| e.into()) {
Err(Error::Io(e)) if io::ErrorKind::AlreadyExists == e.kind() => {
Err(error)
},
Err(Error::Nix(nix::errno::Errno::EEXIST)) => Err(error),
s => s,
}
}
fn on_not_found(self, error: Error) -> Result<R, Error> {
match self.map_err(|e| e.into()) {
Err(Error::Io(e)) if io::ErrorKind::NotFound == e.kind() => {
Err(error)
},
Err(Error::Nix(nix::errno::Errno::ENOENT)) => Err(error),
s => s,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use tempfile::TempDir;
#[test]
fn test_replicate_directory_for_chroot() {
let root = TempDir::new().unwrap();
let root = root.path();
fs::create_dir_all(root.join("src/real")).unwrap();
fs::create_dir_all(root.join("src/alt")).unwrap();
fs::write(root.join("src/real/target.txt"), "hello world\n").unwrap();
std::os::unix::fs::symlink("../real", root.join("src/alt/up")).unwrap();
std::os::unix::fs::symlink(
"target.txt",
root.join("src/real/link.txt"),
)
.unwrap();
fs::write(root.join("src/secret"), "hunter2").unwrap();
chmod(root.join("src/secret"), 0o660).unwrap();
fs::create_dir_all(root.join("src/deleted-dir")).unwrap();
fs::write(root.join("src/deleted-dir/foo"), "foo").unwrap();
fs::write(root.join("src/deleted-file"), "bye").unwrap();
fs::create_dir_all(root.join("src/becomes-file")).unwrap();
fs::write(root.join("src/becomes-file/bar"), "bar").unwrap();
fs::write(root.join("src/becomes-dir"), "plugh").unwrap();
fs::write(root.join("src/content-changes"), "old-content").unwrap();
replicate_directory_for_chroot(
&root.join("src"),
&root.join("sub/dst"),
)
.unwrap();
assert_eq!(
"hello world\n",
fs::read_to_string(&root.join("sub/dst/real/target.txt")).unwrap(),
);
assert_eq!(
"hello world\n",
fs::read_to_string(&root.join("sub/dst/real/link.txt")).unwrap(),
);
assert_eq!(
"hello world\n",
fs::read_to_string(&root.join("sub/dst/alt/up/link.txt")).unwrap(),
);
assert_eq!(
io::ErrorKind::NotFound,
fs::read_to_string(&root.join("sub/dst/secret"))
.unwrap_err()
.kind(),
);
std::thread::sleep(std::time::Duration::from_secs(1));
fs::remove_dir_all(root.join("src/deleted-dir")).unwrap();
fs::remove_file(root.join("src/deleted-file")).unwrap();
fs::remove_dir_all(root.join("src/becomes-file")).unwrap();
fs::write(root.join("src/becomes-file"), "now a file").unwrap();
fs::remove_file(root.join("src/becomes-dir")).unwrap();
fs::create_dir_all(root.join("src/becomes-dir")).unwrap();
fs::write(root.join("src/becomes-dir/qux"), "qux").unwrap();
fs::remove_file(root.join("src/content-changes")).unwrap();
fs::write(root.join("src/content-changes"), "new-content").unwrap();
replicate_directory_for_chroot(
&root.join("src"),
&root.join("sub/dst"),
)
.unwrap();
assert_eq!(
io::ErrorKind::NotFound,
fs::read_to_string(&root.join("sub/dst/deleted-dir/foo"))
.unwrap_err()
.kind(),
);
assert_eq!(
io::ErrorKind::NotFound,
fs::read_to_string(&root.join("sub/dst/deleted-file"))
.unwrap_err()
.kind(),
);
assert_eq!(
"now a file".to_owned(),
fs::read_to_string(&root.join("sub/dst/becomes-file")).unwrap(),
);
assert_eq!(
"qux".to_owned(),
fs::read_to_string(&root.join("sub/dst/becomes-dir/qux")).unwrap(),
);
assert_eq!(
"new-content".to_owned(),
fs::read_to_string(&root.join("sub/dst/content-changes")).unwrap(),
);
}
}