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 #[cfg(target_os = "linux")]
62 let done = reflink(&from, &to);
63 #[cfg(not(target_os = "linux"))]
64 let done = false;
65 if !done {
66 if std::fs::hard_link(&from, &to).is_err() {
68 std::fs::copy(&from, &to).map_err(Error::io(&to))?;
69 }
70 }
71 }
72 }
73 Ok(())
74}
75
76#[cfg(target_os = "linux")]
83fn reflink(from: &Path, to: &Path) -> bool {
84 use std::os::unix::io::AsRawFd as _;
85 static UNSUPPORTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
89 if UNSUPPORTED.load(std::sync::atomic::Ordering::Relaxed) {
90 return false;
91 }
92 let Ok(src) = std::fs::File::open(from) else {
93 return false;
94 };
95 let dst = match std::fs::OpenOptions::new()
96 .write(true)
97 .create(true)
98 .truncate(true)
99 .open(to)
100 {
101 Ok(f) => f,
102 Err(_) => return false,
103 };
104 let rc = unsafe { libc::ioctl(dst.as_raw_fd(), libc::FICLONE, src.as_raw_fd()) };
107 if rc != 0 {
108 let err = std::io::Error::last_os_error();
109 drop(dst);
110 let _ = std::fs::remove_file(to); if matches!(
114 err.raw_os_error(),
115 Some(libc::ENOTTY) | Some(libc::EOPNOTSUPP) | Some(libc::EXDEV) | Some(libc::EINVAL)
116 ) {
117 UNSUPPORTED.store(true, std::sync::atomic::Ordering::Relaxed);
118 }
119 return false;
120 }
121 if let Ok(meta) = src.metadata() {
122 let _ = std::fs::set_permissions(to, meta.permissions());
123 }
124 true
125}
126
127#[cfg(windows)]
133fn clone_symlink_windows(from: &Path, target: &Path, to: &Path) -> Result<()> {
134 let is_dir = std::fs::metadata(from).map(|m| m.is_dir()).unwrap_or(false);
135 let made = if is_dir {
136 std::os::windows::fs::symlink_dir(target, to)
137 } else {
138 std::os::windows::fs::symlink_file(target, to)
139 };
140 if made.is_ok() {
141 return Ok(());
142 }
143 if is_dir {
144 link_or_copy_tree(from, to) } else {
146 if std::fs::hard_link(from, to).is_err() {
147 std::fs::copy(from, to).map_err(Error::io(to))?;
148 }
149 Ok(())
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn clones_files_dirs_symlinks_and_exec_bits() {
159 let tmp = tempfile::tempdir().expect("tmp");
160 let src = tmp.path().join("src");
161 std::fs::create_dir_all(src.join("sub")).expect("mkdir");
162 std::fs::write(src.join("a.txt"), b"hello").expect("write");
163 std::fs::write(src.join("sub/tool"), b"#!/bin/sh\n").expect("write");
164 #[cfg(unix)]
165 {
166 use std::os::unix::fs::PermissionsExt as _;
167 std::fs::set_permissions(src.join("sub/tool"), std::fs::Permissions::from_mode(0o755))
168 .expect("chmod");
169 std::os::unix::fs::symlink("a.txt", src.join("link")).expect("ln");
170 }
171 #[cfg(windows)]
176 let with_link = match std::os::windows::fs::symlink_file("a.txt", src.join("link")) {
177 Ok(()) => true,
178 Err(e) => {
179 eprintln!("symlink refused on this host ({e}) — link portion not exercised");
180 false
181 }
182 };
183
184 let dst = tmp.path().join("dst/pkg");
185 clone_tree(&src, &dst).expect("clone");
186 assert_eq!(std::fs::read(dst.join("a.txt")).expect("read"), b"hello");
187 #[cfg(unix)]
188 {
189 use std::os::unix::fs::PermissionsExt as _;
190 let mode = std::fs::metadata(dst.join("sub/tool"))
191 .expect("meta")
192 .permissions()
193 .mode();
194 assert_eq!(mode & 0o111, 0o111, "executable bit lost on clone");
195 assert!(dst
196 .join("link")
197 .symlink_metadata()
198 .expect("meta")
199 .file_type()
200 .is_symlink());
201 }
202 #[cfg(windows)]
203 if with_link {
204 let meta = dst.join("link").symlink_metadata().expect("entry lost");
209 assert!(
210 meta.file_type().is_symlink() || meta.file_type().is_file(),
211 "neither link nor file"
212 );
213 assert_eq!(std::fs::read(dst.join("link")).expect("read"), b"hello");
214 }
215 std::fs::write(dst.join("a.txt"), b"changed").expect("write");
218 #[cfg(target_os = "macos")]
219 assert_eq!(std::fs::read(src.join("a.txt")).expect("read"), b"hello");
220 }
221}