1use crate::error::{Error, Result};
10use std::path::Path;
11
12pub fn clone_tree(src: &Path, dst: &Path) -> Result<()> {
13 if let Some(parent) = dst.parent() {
14 std::fs::create_dir_all(parent).map_err(Error::io(parent))?;
15 }
16
17 #[cfg(target_os = "macos")]
18 {
19 use std::os::unix::ffi::OsStrExt as _;
20 let c_src = std::ffi::CString::new(src.as_os_str().as_bytes());
21 let c_dst = std::ffi::CString::new(dst.as_os_str().as_bytes());
22 if let (Ok(c_src), Ok(c_dst)) = (c_src, c_dst) {
23 let rc = unsafe { libc::clonefile(c_src.as_ptr(), c_dst.as_ptr(), 0) };
26 if rc == 0 {
27 return Ok(());
28 }
29 }
31 }
32
33 link_or_copy_tree(src, dst)
34}
35
36fn link_or_copy_tree(src: &Path, dst: &Path) -> Result<()> {
37 std::fs::create_dir_all(dst).map_err(Error::io(dst))?;
38 for entry in std::fs::read_dir(src).map_err(Error::io(src))? {
39 let entry = entry.map_err(Error::io(src))?;
40 let from = entry.path();
41 let to = dst.join(entry.file_name());
42 let ftype = entry.file_type().map_err(Error::io(&from))?;
43 if ftype.is_dir() {
44 link_or_copy_tree(&from, &to)?;
45 } else if ftype.is_symlink() {
46 let target = std::fs::read_link(&from).map_err(Error::io(&from))?;
47 #[cfg(unix)]
48 std::os::unix::fs::symlink(&target, &to).map_err(Error::io(&to))?;
49 #[cfg(windows)]
50 clone_symlink_windows(&from, &target, &to)?;
51 #[cfg(not(any(unix, windows)))]
52 {
53 let _ = target;
54 std::fs::copy(&from, &to).map_err(Error::io(&to))?;
55 }
56 } else {
57 if std::fs::hard_link(&from, &to).is_err() {
59 std::fs::copy(&from, &to).map_err(Error::io(&to))?;
60 }
61 }
62 }
63 Ok(())
64}
65
66#[cfg(windows)]
72fn clone_symlink_windows(from: &Path, target: &Path, to: &Path) -> Result<()> {
73 let is_dir = std::fs::metadata(from).map(|m| m.is_dir()).unwrap_or(false);
74 let made = if is_dir {
75 std::os::windows::fs::symlink_dir(target, to)
76 } else {
77 std::os::windows::fs::symlink_file(target, to)
78 };
79 if made.is_ok() {
80 return Ok(());
81 }
82 if is_dir {
83 link_or_copy_tree(from, to) } else {
85 if std::fs::hard_link(from, to).is_err() {
86 std::fs::copy(from, to).map_err(Error::io(to))?;
87 }
88 Ok(())
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn clones_files_dirs_symlinks_and_exec_bits() {
98 let tmp = tempfile::tempdir().expect("tmp");
99 let src = tmp.path().join("src");
100 std::fs::create_dir_all(src.join("sub")).expect("mkdir");
101 std::fs::write(src.join("a.txt"), b"hello").expect("write");
102 std::fs::write(src.join("sub/tool"), b"#!/bin/sh\n").expect("write");
103 #[cfg(unix)]
104 {
105 use std::os::unix::fs::PermissionsExt as _;
106 std::fs::set_permissions(src.join("sub/tool"), std::fs::Permissions::from_mode(0o755))
107 .expect("chmod");
108 std::os::unix::fs::symlink("a.txt", src.join("link")).expect("ln");
109 }
110 #[cfg(windows)]
115 let with_link = match std::os::windows::fs::symlink_file("a.txt", src.join("link")) {
116 Ok(()) => true,
117 Err(e) => {
118 eprintln!("symlink refused on this host ({e}) — link portion not exercised");
119 false
120 }
121 };
122
123 let dst = tmp.path().join("dst/pkg");
124 clone_tree(&src, &dst).expect("clone");
125 assert_eq!(std::fs::read(dst.join("a.txt")).expect("read"), b"hello");
126 #[cfg(unix)]
127 {
128 use std::os::unix::fs::PermissionsExt as _;
129 let mode = std::fs::metadata(dst.join("sub/tool"))
130 .expect("meta")
131 .permissions()
132 .mode();
133 assert_eq!(mode & 0o111, 0o111, "executable bit lost on clone");
134 assert!(dst
135 .join("link")
136 .symlink_metadata()
137 .expect("meta")
138 .file_type()
139 .is_symlink());
140 }
141 #[cfg(windows)]
142 if with_link {
143 let meta = dst.join("link").symlink_metadata().expect("entry lost");
148 assert!(
149 meta.file_type().is_symlink() || meta.file_type().is_file(),
150 "neither link nor file"
151 );
152 assert_eq!(std::fs::read(dst.join("link")).expect("read"), b"hello");
153 }
154 std::fs::write(dst.join("a.txt"), b"changed").expect("write");
157 #[cfg(target_os = "macos")]
158 assert_eq!(std::fs::read(src.join("a.txt")).expect("read"), b"hello");
159 }
160}