use crate::error::{Error, Result};
use std::path::Path;
pub fn clone_tree(src: &Path, dst: &Path) -> Result<()> {
if let Some(parent) = dst.parent() {
std::fs::create_dir_all(parent).map_err(Error::io(parent))?;
}
#[cfg(target_os = "macos")]
{
use std::os::unix::ffi::OsStrExt as _;
let c_src = std::ffi::CString::new(src.as_os_str().as_bytes());
let c_dst = std::ffi::CString::new(dst.as_os_str().as_bytes());
if let (Ok(c_src), Ok(c_dst)) = (c_src, c_dst) {
let rc = unsafe { libc::clonefile(c_src.as_ptr(), c_dst.as_ptr(), 0) };
if rc == 0 {
return Ok(());
}
}
}
link_or_copy_tree(src, dst)
}
fn link_or_copy_tree(src: &Path, dst: &Path) -> Result<()> {
std::fs::create_dir_all(dst).map_err(Error::io(dst))?;
for entry in std::fs::read_dir(src).map_err(Error::io(src))? {
let entry = entry.map_err(Error::io(src))?;
let from = entry.path();
let to = dst.join(entry.file_name());
let ftype = entry.file_type().map_err(Error::io(&from))?;
if ftype.is_dir() {
link_or_copy_tree(&from, &to)?;
} else if ftype.is_symlink() {
let target = std::fs::read_link(&from).map_err(Error::io(&from))?;
#[cfg(unix)]
symlink_like_unzip(&target, &to)?;
#[cfg(windows)]
clone_symlink_windows(&from, &target, &to)?;
#[cfg(not(any(unix, windows)))]
{
let _ = target;
std::fs::copy(&from, &to).map_err(Error::io(&to))?;
}
} else {
#[cfg(target_os = "linux")]
let done = reflink(&from, &to);
#[cfg(not(target_os = "linux"))]
let done = false;
if !done {
if std::fs::hard_link(&from, &to).is_err() {
std::fs::copy(&from, &to).map_err(Error::io(&to))?;
}
}
}
}
Ok(())
}
#[cfg(target_os = "linux")]
fn reflink(from: &Path, to: &Path) -> bool {
use std::os::unix::io::AsRawFd as _;
static UNSUPPORTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
if UNSUPPORTED.load(std::sync::atomic::Ordering::Relaxed) {
return false;
}
let Ok(src) = std::fs::File::open(from) else {
return false;
};
let dst = match std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(to)
{
Ok(f) => f,
Err(_) => return false,
};
let rc = unsafe { libc::ioctl(dst.as_raw_fd(), libc::FICLONE, src.as_raw_fd()) };
if rc != 0 {
let err = std::io::Error::last_os_error();
drop(dst);
let _ = std::fs::remove_file(to); if matches!(
err.raw_os_error(),
Some(libc::ENOTTY) | Some(libc::EOPNOTSUPP) | Some(libc::EXDEV) | Some(libc::EINVAL)
) {
UNSUPPORTED.store(true, std::sync::atomic::Ordering::Relaxed);
}
return false;
}
if let Ok(meta) = src.metadata() {
let _ = std::fs::set_permissions(to, meta.permissions());
}
true
}
#[cfg(windows)]
fn clone_symlink_windows(from: &Path, target: &Path, to: &Path) -> Result<()> {
let is_dir = std::fs::metadata(from).map(|m| m.is_dir()).unwrap_or(false);
let made = if is_dir {
std::os::windows::fs::symlink_dir(target, to)
} else {
std::os::windows::fs::symlink_file(target, to)
};
if made.is_ok() {
return Ok(());
}
if is_dir {
link_or_copy_tree(from, to) } else {
if std::fs::hard_link(from, to).is_err() {
std::fs::copy(from, to).map_err(Error::io(to))?;
}
Ok(())
}
}
#[cfg(unix)]
pub fn symlink_like_unzip(target: &std::path::Path, link: &std::path::Path) -> Result<()> {
std::os::unix::fs::symlink(target, link).map_err(Error::io(link))?;
#[cfg(target_os = "macos")]
{
use std::os::unix::ffi::OsStrExt;
let c = std::ffi::CString::new(link.as_os_str().as_bytes()).map_err(|_| Error::Io {
path: link.to_path_buf(),
source: std::io::Error::new(std::io::ErrorKind::InvalidInput, "path contains NUL"),
})?;
let rc =
unsafe { libc::fchmodat(libc::AT_FDCWD, c.as_ptr(), 0o777, libc::AT_SYMLINK_NOFOLLOW) };
if rc != 0 {
return Err(Error::io(link)(std::io::Error::last_os_error()));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clones_files_dirs_symlinks_and_exec_bits() {
let tmp = tempfile::tempdir().expect("tmp");
let src = tmp.path().join("src");
std::fs::create_dir_all(src.join("sub")).expect("mkdir");
std::fs::write(src.join("a.txt"), b"hello").expect("write");
std::fs::write(src.join("sub/tool"), b"#!/bin/sh\n").expect("write");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(src.join("sub/tool"), std::fs::Permissions::from_mode(0o755))
.expect("chmod");
std::os::unix::fs::symlink("a.txt", src.join("link")).expect("ln");
}
#[cfg(windows)]
let with_link = match std::os::windows::fs::symlink_file("a.txt", src.join("link")) {
Ok(()) => true,
Err(e) => {
eprintln!("symlink refused on this host ({e}) — link portion not exercised");
false
}
};
let dst = tmp.path().join("dst/pkg");
clone_tree(&src, &dst).expect("clone");
assert_eq!(std::fs::read(dst.join("a.txt")).expect("read"), b"hello");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
let mode = std::fs::metadata(dst.join("sub/tool"))
.expect("meta")
.permissions()
.mode();
assert_eq!(mode & 0o111, 0o111, "executable bit lost on clone");
assert!(dst
.join("link")
.symlink_metadata()
.expect("meta")
.file_type()
.is_symlink());
}
#[cfg(windows)]
if with_link {
let meta = dst.join("link").symlink_metadata().expect("entry lost");
assert!(
meta.file_type().is_symlink() || meta.file_type().is_file(),
"neither link nor file"
);
assert_eq!(std::fs::read(dst.join("link")).expect("read"), b"hello");
}
std::fs::write(dst.join("a.txt"), b"changed").expect("write");
#[cfg(target_os = "macos")]
assert_eq!(std::fs::read(src.join("a.txt")).expect("read"), b"hello");
}
}