cap_primitives/rustix/fs/
copy_impl.rs

1// Implementation derived from `copy` in Rust's
2// library/std/src/sys/unix/fs.rs at revision
3// 108e90ca78f052c0c1c49c42a22c85620be19712.
4
5use crate::fs::{open, OpenOptions};
6#[cfg(any(target_os = "android", target_os = "linux"))]
7use rustix::fs::copy_file_range;
8#[cfg(any(
9    target_os = "macos",
10    target_os = "ios",
11    target_os = "tvos",
12    target_os = "watchos",
13    target_os = "visionos",
14))]
15use rustix::fs::{
16    copyfile_state_alloc, copyfile_state_free, copyfile_state_get_copied, copyfile_state_t,
17    fclonefileat, fcopyfile, CloneFlags, CopyfileFlags,
18};
19use std::path::Path;
20use std::{fs, io};
21
22fn open_from(start: &fs::File, path: &Path) -> io::Result<(fs::File, fs::Metadata)> {
23    let reader = open(start, path, OpenOptions::new().read(true))?;
24    let metadata = reader.metadata()?;
25    if !metadata.is_file() {
26        return Err(io::Error::new(
27            io::ErrorKind::InvalidInput,
28            "the source path is not an existing regular file",
29        ));
30    }
31    Ok((reader, metadata))
32}
33
34#[cfg(not(target_os = "wasi"))]
35fn open_to_and_set_permissions(
36    start: &fs::File,
37    path: &Path,
38    reader_metadata: fs::Metadata,
39) -> io::Result<(fs::File, fs::Metadata)> {
40    use crate::fs::OpenOptionsExt;
41    use std::os::unix::fs::PermissionsExt;
42
43    let perm = reader_metadata.permissions();
44    let writer = open(
45        start,
46        path,
47        OpenOptions::new()
48            // create the file with the correct mode right away
49            .mode(perm.mode())
50            .write(true)
51            .create(true)
52            .truncate(true),
53    )?;
54    let writer_metadata = writer.metadata()?;
55    if writer_metadata.is_file() {
56        // Set the correct file permissions, in case the file already existed.
57        // Don't set the permissions on already existing non-files like
58        // pipes/FIFOs or device nodes.
59        writer.set_permissions(perm)?;
60    }
61    Ok((writer, writer_metadata))
62}
63
64#[cfg(target_os = "wasi")]
65fn open_to_and_set_permissions(
66    start: &fs::File,
67    path: &Path,
68    reader_metadata: fs::Metadata,
69) -> io::Result<(fs::File, fs::Metadata)> {
70    let writer = open(
71        start,
72        path,
73        OpenOptions::new()
74            // create the file with the correct mode right away
75            .write(true)
76            .create(true)
77            .truncate(true),
78    )?;
79    let writer_metadata = writer.metadata()?;
80    Ok((writer, writer_metadata))
81}
82
83#[cfg(not(any(
84    target_os = "linux",
85    target_os = "android",
86    target_os = "macos",
87    target_os = "ios",
88    target_os = "tvos",
89    target_os = "watchos",
90    target_os = "visionos",
91)))]
92pub(crate) fn copy_impl(
93    from_start: &fs::File,
94    from_path: &Path,
95    to_start: &fs::File,
96    to_path: &Path,
97) -> io::Result<u64> {
98    let (mut reader, reader_metadata) = open_from(from_start, from_path)?;
99    let (mut writer, _) = open_to_and_set_permissions(to_start, to_path, reader_metadata)?;
100
101    io::copy(&mut reader, &mut writer)
102}
103
104#[cfg(any(target_os = "android", target_os = "linux"))]
105pub(crate) fn copy_impl(
106    from_start: &fs::File,
107    from_path: &Path,
108    to_start: &fs::File,
109    to_path: &Path,
110) -> io::Result<u64> {
111    use std::cmp;
112    use std::sync::atomic::{AtomicBool, Ordering};
113
114    // Kernel prior to 4.5 don't have copy_file_range
115    // We store the availability in a global to avoid unnecessary syscalls
116    static HAS_COPY_FILE_RANGE: AtomicBool = AtomicBool::new(true);
117
118    let (mut reader, reader_metadata) = open_from(from_start, from_path)?;
119    let len = reader_metadata.len();
120    let (mut writer, _) = open_to_and_set_permissions(to_start, to_path, reader_metadata)?;
121
122    let has_copy_file_range = HAS_COPY_FILE_RANGE.load(Ordering::Relaxed);
123    let mut written = 0_u64;
124    while written < len {
125        let copy_result = if has_copy_file_range {
126            let bytes_to_copy = cmp::min(len - written, usize::MAX as u64);
127
128            // `copy_file_range` takes a `usize`; convert with saturation so
129            // that we copy as many bytes as we can.
130            let bytes_to_copy = usize::try_from(bytes_to_copy).unwrap_or(usize::MAX);
131
132            // We actually don't have to adjust the offsets,
133            // because copy_file_range adjusts the file offset automatically
134            let copy_result = copy_file_range(&reader, None, &writer, None, bytes_to_copy);
135            if let Err(copy_err) = copy_result {
136                match copy_err {
137                    rustix::io::Errno::NOSYS | rustix::io::Errno::PERM => {
138                        HAS_COPY_FILE_RANGE.store(false, Ordering::Relaxed);
139                    }
140                    _ => {}
141                }
142            }
143            copy_result
144        } else {
145            Err(rustix::io::Errno::NOSYS)
146        };
147        match copy_result {
148            Ok(ret) => written += ret as u64,
149            Err(err) => {
150                match err {
151                    rustix::io::Errno::NOSYS
152                    | rustix::io::Errno::XDEV
153                    | rustix::io::Errno::INVAL
154                    | rustix::io::Errno::PERM => {
155                        // Try fallback io::copy if either:
156                        // - Kernel version is < 4.5 (ENOSYS)
157                        // - Files are mounted on different fs (EXDEV)
158                        // - copy_file_range is disallowed, for example by seccomp (EPERM)
159                        // - copy_file_range cannot be used with pipes or device nodes (EINVAL)
160                        assert_eq!(written, 0);
161                        return io::copy(&mut reader, &mut writer);
162                    }
163                    _ => return Err(err.into()),
164                }
165            }
166        }
167    }
168    Ok(written)
169}
170
171#[cfg(any(
172    target_os = "macos",
173    target_os = "ios",
174    target_os = "tvos",
175    target_os = "watchos",
176    target_os = "visionos",
177))]
178#[allow(non_upper_case_globals)]
179#[allow(unsafe_code)]
180pub(crate) fn copy_impl(
181    from_start: &fs::File,
182    from_path: &Path,
183    to_start: &fs::File,
184    to_path: &Path,
185) -> io::Result<u64> {
186    use std::sync::atomic::{AtomicBool, Ordering};
187
188    struct FreeOnDrop(copyfile_state_t);
189    impl Drop for FreeOnDrop {
190        fn drop(&mut self) {
191            // Safety: This is the only place where we free the state, and we
192            // never let it escape.
193            unsafe {
194                copyfile_state_free(self.0).ok();
195            }
196        }
197    }
198
199    // MacOS prior to 10.12 don't support `fclonefileat`
200    // We store the availability in a global to avoid unnecessary syscalls
201    static HAS_FCLONEFILEAT: AtomicBool = AtomicBool::new(true);
202
203    let (reader, reader_metadata) = open_from(from_start, from_path)?;
204
205    // Opportunistically attempt to create a copy-on-write clone of `from_path`
206    // using `fclonefileat`.
207    if HAS_FCLONEFILEAT.load(Ordering::Relaxed) {
208        let clonefile_result = fclonefileat(&reader, to_start, to_path, CloneFlags::empty());
209        match clonefile_result {
210            Ok(_) => return Ok(reader_metadata.len()),
211            Err(err) => match err {
212                // `fclonefileat` will fail on non-APFS volumes, if the
213                // destination already exists, or if the source and destination
214                // are on different devices. In all these cases `fcopyfile`
215                // should succeed.
216                rustix::io::Errno::NOTSUP | rustix::io::Errno::EXIST | rustix::io::Errno::XDEV => {
217                    ()
218                }
219                rustix::io::Errno::NOSYS => HAS_FCLONEFILEAT.store(false, Ordering::Relaxed),
220                _ => return Err(err.into()),
221            },
222        }
223    }
224
225    // Fall back to using `fcopyfile` if `fclonefileat` does not succeed.
226    let (writer, writer_metadata) =
227        open_to_and_set_permissions(to_start, to_path, reader_metadata)?;
228
229    // We ensure that `FreeOnDrop` never contains a null pointer so it is
230    // always safe to call `copyfile_state_free`
231    let state = {
232        let state = copyfile_state_alloc()?;
233        FreeOnDrop(state)
234    };
235
236    let flags = if writer_metadata.is_file() {
237        CopyfileFlags::ALL
238    } else {
239        CopyfileFlags::DATA
240    };
241
242    // Safety: We allocated `state` above so it's still live here.
243    unsafe {
244        fcopyfile(&reader, &writer, state.0, flags)?;
245
246        Ok(copyfile_state_get_copied(state.0)?)
247    }
248}