use std::fs;
use std::os::unix::ffi::OsStrExt;
use rustix::fs::{unlinkat, AtFlags};
use super::atomic::{atomic_symlink_swap, open_dir_nofollow};
use super::backup::create_snapshot;
use crate::types::safepath::SafePath;
pub fn replace_file_with_symlink(
source: &SafePath,
target: &SafePath,
dry_run: bool,
allow_degraded: bool,
backup_tag: &str,
) -> std::io::Result<(bool, u64)> {
replace_file_with_symlink_with_override(
source,
target,
dry_run,
allow_degraded,
backup_tag,
None,
)
}
#[allow(
clippy::too_many_lines,
reason = "Will be broken down into smaller helpers in a follow-up refactor"
)]
pub fn replace_file_with_symlink_with_override(
source: &SafePath,
target: &SafePath,
dry_run: bool,
allow_degraded: bool,
backup_tag: &str,
force_exdev: Option<bool>,
) -> std::io::Result<(bool, u64)> {
let source_path = source.as_path();
let target_path = target.as_path();
if dry_run {
return Ok((false, 0));
}
if source_path == target_path {
return Ok((false, 0));
}
if let Some(parent) = target_path.parent() {
let _ = fs::create_dir_all(parent);
let _dirfd = open_dir_nofollow(parent)?; }
let metadata = fs::symlink_metadata(&target_path);
let existed = metadata.is_ok();
let is_symlink = metadata
.as_ref()
.map(|m| m.file_type().is_symlink())
.unwrap_or(false);
let current_dest = if is_symlink {
fs::read_link(&target_path).ok()
} else {
None
};
if is_symlink {
let desired = fs::canonicalize(&source_path).unwrap_or_else(|_| source_path.clone());
let mut resolved_current = current_dest.clone().unwrap_or_default();
if resolved_current.is_relative() {
if let Some(parent) = target_path.parent() {
resolved_current = parent.join(resolved_current);
}
}
let resolved_current = fs::canonicalize(&resolved_current).unwrap_or(resolved_current);
if resolved_current == desired {
return Ok((false, 0));
}
if let Err(e) = create_snapshot(&target_path, backup_tag) {
if !dry_run {
return Err(e);
}
}
if let Some(parent) = target_path.parent() {
let dirfd = open_dir_nofollow(parent)?;
let fname_c = if let Some(name_os) = target_path.file_name() {
std::ffi::CString::new(name_os.as_bytes()).map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid cstring")
})?
} else {
std::ffi::CString::new("target").map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid cstring")
})?
};
match unlinkat(&dirfd, fname_c.as_c_str(), AtFlags::empty()) {
Ok(()) => {}
Err(e) if e == rustix::io::Errno::NOENT => {}
Err(e) => return Err(std::io::Error::from_raw_os_error(e.raw_os_error())),
}
}
let res = atomic_symlink_swap(&source_path, &target_path, allow_degraded, force_exdev)?;
return Ok(res);
}
if existed {
if let Ok(_meta) = metadata {
if let Err(e) = create_snapshot(&target_path, backup_tag) {
if !dry_run {
return Err(e);
}
}
if let Some(parent) = target_path.parent() {
let dirfd = open_dir_nofollow(parent)?;
let fname_c = if let Some(name_os) = target_path.file_name() {
std::ffi::CString::new(name_os.as_bytes()).map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid cstring")
})?
} else {
std::ffi::CString::new("target").map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid cstring")
})?
};
match unlinkat(&dirfd, fname_c.as_c_str(), AtFlags::empty()) {
Ok(()) => {}
Err(e) if e == rustix::io::Errno::NOENT => {}
Err(e) => return Err(std::io::Error::from_raw_os_error(e.raw_os_error())),
}
} else {
let _ = fs::remove_file(&target_path);
}
}
} else {
if let Err(e) = create_snapshot(&target_path, backup_tag) {
if !dry_run {
return Err(e);
}
}
}
if let Some(parent) = target_path.parent() {
fs::create_dir_all(parent)?;
}
if let Some(parent) = target_path.parent() {
let dirfd = open_dir_nofollow(parent)?;
let fname_c = if let Some(name_os) = target_path.file_name() {
std::ffi::CString::new(name_os.as_bytes()).map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid cstring")
})?
} else {
std::ffi::CString::new("target").map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid cstring")
})?
};
match unlinkat(&dirfd, fname_c.as_c_str(), AtFlags::empty()) {
Ok(()) => {}
Err(e) if e == rustix::io::Errno::NOENT => {}
Err(e) => return Err(std::io::Error::from_raw_os_error(e.raw_os_error())),
}
}
let res = atomic_symlink_swap(&source_path, &target_path, allow_degraded, force_exdev)?;
Ok(res)
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::constants::DEFAULT_BACKUP_TAG;
use crate::fs::restore::restore_file;
use crate::types::safepath::SafePath;
use std::io::Write;
fn tmpdir() -> tempfile::TempDir {
tempfile::tempdir().unwrap_or_else(|_| panic!("Failed to create tempdir"))
}
#[test]
fn atomic_swap_creates_symlink_pointing_to_source() {
let td = tmpdir();
let root = td.path();
let src = root.join("source.txt");
let tgt = root.join("target.txt");
fs::write(&src, b"hello").expect("Failed to write source file");
let sp_src = SafePath::from_rooted(root, &src).expect("Failed to create source SafePath");
let sp_tgt = SafePath::from_rooted(root, &tgt).expect("Failed to create target SafePath");
let _ = replace_file_with_symlink(&sp_src, &sp_tgt, false, false, DEFAULT_BACKUP_TAG)
.expect("Failed to replace file with symlink");
let md = fs::symlink_metadata(&tgt).expect("Failed to get symlink metadata");
assert!(md.file_type().is_symlink(), "target should be a symlink");
let link = fs::read_link(&tgt).expect("Failed to read symlink");
assert_eq!(link, src);
}
#[test]
fn replace_and_restore_roundtrip() {
let td = tmpdir();
let root = td.path();
let src = root.join("bin-new");
let tgt = root.join("bin-old");
fs::write(&src, b"new").unwrap_or_else(|e| panic!("Failed to write source file: {e}"));
{
let mut f = fs::File::create(&tgt)
.unwrap_or_else(|e| panic!("Failed to create target file: {e}"));
writeln!(f, "old").unwrap_or_else(|e| panic!("Failed to write to target file: {e}"));
}
let sp_src = SafePath::from_rooted(root, &src)
.unwrap_or_else(|e| panic!("Failed to create source SafePath: {e}"));
let sp_tgt = SafePath::from_rooted(root, &tgt)
.unwrap_or_else(|e| panic!("Failed to create target SafePath: {e}"));
let _ = replace_file_with_symlink(&sp_src, &sp_tgt, false, false, DEFAULT_BACKUP_TAG)
.unwrap_or_else(|e| panic!("Failed to replace file with symlink: {e}"));
let md = fs::symlink_metadata(&tgt)
.unwrap_or_else(|e| panic!("Failed to get symlink metadata: {e}"));
assert!(
md.file_type().is_symlink(),
"target should be a symlink after replace"
);
restore_file(&sp_tgt, false, false, DEFAULT_BACKUP_TAG)
.unwrap_or_else(|e| panic!("Failed to restore file: {e}"));
let md2 = fs::symlink_metadata(&tgt)
.unwrap_or_else(|e| panic!("Failed to get symlink metadata: {e}"));
assert!(
md2.file_type().is_file(),
"target should be a regular file after restore"
);
let content =
fs::read_to_string(&tgt).unwrap_or_else(|e| panic!("Failed to read target file: {e}"));
assert!(content.starts_with("old"));
}
}