cap_primitives/rustix/fs/
copy_impl.rs1use 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 .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 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 .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 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 let bytes_to_copy = usize::try_from(bytes_to_copy).unwrap_or(usize::MAX);
131
132 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 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 unsafe {
194 copyfile_state_free(self.0).ok();
195 }
196 }
197 }
198
199 static HAS_FCLONEFILEAT: AtomicBool = AtomicBool::new(true);
202
203 let (reader, reader_metadata) = open_from(from_start, from_path)?;
204
205 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 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 let (writer, writer_metadata) =
227 open_to_and_set_permissions(to_start, to_path, reader_metadata)?;
228
229 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 unsafe {
244 fcopyfile(&reader, &writer, state.0, flags)?;
245
246 Ok(copyfile_state_get_copied(state.0)?)
247 }
248}