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 symlink_like_unzip(&target, &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(unix)]
157pub fn symlink_like_unzip(target: &std::path::Path, link: &std::path::Path) -> Result<()> {
158 std::os::unix::fs::symlink(target, link).map_err(Error::io(link))?;
159 #[cfg(target_os = "macos")]
160 {
161 use std::os::unix::ffi::OsStrExt;
162 let c = std::ffi::CString::new(link.as_os_str().as_bytes()).map_err(|_| Error::Io {
163 path: link.to_path_buf(),
164 source: std::io::Error::new(std::io::ErrorKind::InvalidInput, "path contains NUL"),
165 })?;
166 let rc =
169 unsafe { libc::fchmodat(libc::AT_FDCWD, c.as_ptr(), 0o777, libc::AT_SYMLINK_NOFOLLOW) };
170 if rc != 0 {
171 return Err(Error::io(link)(std::io::Error::last_os_error()));
172 }
173 }
174 Ok(())
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn clones_files_dirs_symlinks_and_exec_bits() {
183 let tmp = tempfile::tempdir().expect("tmp");
184 let src = tmp.path().join("src");
185 std::fs::create_dir_all(src.join("sub")).expect("mkdir");
186 std::fs::write(src.join("a.txt"), b"hello").expect("write");
187 std::fs::write(src.join("sub/tool"), b"#!/bin/sh\n").expect("write");
188 #[cfg(unix)]
189 {
190 use std::os::unix::fs::PermissionsExt as _;
191 std::fs::set_permissions(src.join("sub/tool"), std::fs::Permissions::from_mode(0o755))
192 .expect("chmod");
193 std::os::unix::fs::symlink("a.txt", src.join("link")).expect("ln");
194 }
195 #[cfg(windows)]
200 let with_link = match std::os::windows::fs::symlink_file("a.txt", src.join("link")) {
201 Ok(()) => true,
202 Err(e) => {
203 eprintln!("symlink refused on this host ({e}) — link portion not exercised");
204 false
205 }
206 };
207
208 let dst = tmp.path().join("dst/pkg");
209 clone_tree(&src, &dst).expect("clone");
210 assert_eq!(std::fs::read(dst.join("a.txt")).expect("read"), b"hello");
211 #[cfg(unix)]
212 {
213 use std::os::unix::fs::PermissionsExt as _;
214 let mode = std::fs::metadata(dst.join("sub/tool"))
215 .expect("meta")
216 .permissions()
217 .mode();
218 assert_eq!(mode & 0o111, 0o111, "executable bit lost on clone");
219 assert!(dst
220 .join("link")
221 .symlink_metadata()
222 .expect("meta")
223 .file_type()
224 .is_symlink());
225 }
226 #[cfg(windows)]
227 if with_link {
228 let meta = dst.join("link").symlink_metadata().expect("entry lost");
233 assert!(
234 meta.file_type().is_symlink() || meta.file_type().is_file(),
235 "neither link nor file"
236 );
237 assert_eq!(std::fs::read(dst.join("link")).expect("read"), b"hello");
238 }
239 std::fs::write(dst.join("a.txt"), b"changed").expect("write");
242 #[cfg(target_os = "macos")]
243 assert_eq!(std::fs::read(src.join("a.txt")).expect("read"), b"hello");
244 }
245}