1use std::fs::{File, OpenOptions};
26use std::io;
27#[cfg(windows)]
28use std::io::{Read, Seek, SeekFrom, Write};
29#[cfg(unix)]
30use std::os::unix::io::{AsRawFd, RawFd};
31use std::path::Path;
32
33#[cfg(windows)]
34use crate::extent::mark_sparse;
35
36pub fn fast_copy(src: &Path, dst: &Path) -> io::Result<u64> {
50 let src_len = std::fs::metadata(src)?.len();
56
57 match reflink_copy::reflink(src, dst) {
61 Ok(()) => return Ok(src_len),
62 Err(e) if is_reflink_unsupported(&e) => {
63 }
65 Err(e) => return Err(e),
66 }
67
68 sparse_copy(src, dst)
69}
70
71pub fn sparse_copy(src: &Path, dst: &Path) -> io::Result<u64> {
77 sparse_copy_impl(src, dst)
78}
79
80#[cfg(unix)]
81fn sparse_copy_impl(src: &Path, dst: &Path) -> io::Result<u64> {
82 let src_file = File::open(src)?;
83 let len = src_file.metadata()?.len();
84
85 let dst_file = OpenOptions::new()
86 .read(true)
87 .write(true)
88 .create(true)
89 .truncate(true)
90 .open(dst)?;
91 dst_file.set_len(len)?;
94
95 let src_fd = src_file.as_raw_fd();
96 let dst_fd = dst_file.as_raw_fd();
97
98 let mut off: i64 = 0;
99 while (off as u64) < len {
100 let data_start = unsafe { libc::lseek(src_fd, off, libc::SEEK_DATA) };
102 if data_start < 0 {
103 let err = io::Error::last_os_error();
104 if err.raw_os_error() == Some(libc::ENXIO) {
106 break;
107 }
108 return Err(err);
109 }
110 let data_end = unsafe { libc::lseek(src_fd, data_start, libc::SEEK_HOLE) };
112 if data_end < 0 {
113 return Err(io::Error::last_os_error());
114 }
115 let data_end = (data_end as u64).min(len);
116 let data_start = data_start as u64;
117 if data_end <= data_start {
118 break;
119 }
120
121 copy_extent(src_fd, dst_fd, data_start, data_end - data_start)?;
122 off = data_end as i64;
123 }
124
125 dst_file.sync_all()?;
126 Ok(len)
127}
128
129#[cfg(windows)]
130fn sparse_copy_impl(src: &Path, dst: &Path) -> io::Result<u64> {
131 const BUF_SIZE: usize = 1024 * 1024;
132
133 let mut src_file = File::open(src)?;
134 let len = src_file.metadata()?.len();
135
136 let mut dst_file = OpenOptions::new()
137 .read(true)
138 .write(true)
139 .create(true)
140 .truncate(true)
141 .open(dst)?;
142 dst_file.set_len(len)?;
143 mark_sparse(&dst_file)?;
144
145 let mut offset = 0u64;
146 let mut buf = vec![0u8; BUF_SIZE];
147 loop {
148 let n = src_file.read(&mut buf)?;
149 if n == 0 {
150 break;
151 }
152
153 write_nonzero_runs(&mut dst_file, offset, &buf[..n])?;
154 offset += n as u64;
155 }
156
157 dst_file.sync_all()?;
158 Ok(len)
159}
160
161fn is_reflink_unsupported(e: &io::Error) -> bool {
172 if matches!(e.kind(), io::ErrorKind::Unsupported) {
173 return true;
174 }
175
176 let Some(code) = e.raw_os_error() else {
177 return false;
178 };
179
180 #[cfg(target_os = "linux")]
181 let aliases: &[i32] = &[libc::ENOTSUP, libc::EXDEV, libc::EINVAL];
182 #[cfg(all(unix, not(target_os = "linux")))]
183 let aliases: &[i32] = &[libc::ENOTSUP, libc::EOPNOTSUPP, libc::EXDEV, libc::EINVAL];
184 #[cfg(windows)]
185 let aliases: &[i32] = &[
186 1, 17, 50, 87, ];
191
192 #[cfg(windows)]
193 {
194 let win32_code = (code as u32 & 0xffff) as i32;
195 aliases.contains(&code) || aliases.contains(&win32_code)
196 }
197
198 #[cfg(unix)]
199 aliases.contains(&code)
200}
201
202#[cfg(target_os = "linux")]
203fn copy_extent(src_fd: RawFd, dst_fd: RawFd, off: u64, len: u64) -> io::Result<()> {
204 let mut src_off = off as i64;
205 let mut dst_off = off as i64;
206 let mut remaining = len;
207
208 while remaining > 0 {
209 let chunk = remaining.min(usize::MAX as u64 / 2) as usize;
210 let n =
211 unsafe { libc::copy_file_range(src_fd, &mut src_off, dst_fd, &mut dst_off, chunk, 0) };
212 if n < 0 {
213 let err = io::Error::last_os_error();
214 if matches!(
219 err.raw_os_error(),
220 Some(libc::ENOSYS)
221 | Some(libc::EXDEV)
222 | Some(libc::EINVAL)
223 | Some(libc::EOPNOTSUPP)
224 ) {
225 let consumed = len - remaining;
226 return read_write_extent(src_fd, dst_fd, off + consumed, remaining);
227 }
228 return Err(err);
229 }
230 if n == 0 {
231 break;
233 }
234 remaining -= n as u64;
235 }
236 Ok(())
237}
238
239#[cfg(all(unix, not(target_os = "linux")))]
240fn copy_extent(src_fd: RawFd, dst_fd: RawFd, off: u64, len: u64) -> io::Result<()> {
241 read_write_extent(src_fd, dst_fd, off, len)
242}
243
244#[cfg(unix)]
248fn read_write_extent(src_fd: RawFd, dst_fd: RawFd, off: u64, len: u64) -> io::Result<()> {
249 const BUF_SIZE: usize = 64 * 1024;
250 let mut buf = [0u8; BUF_SIZE];
251 let mut copied: u64 = 0;
252
253 while copied < len {
254 let to_read = (len - copied).min(BUF_SIZE as u64) as usize;
255 let read_off = (off + copied) as i64;
256 let n = unsafe {
257 libc::pread(
258 src_fd,
259 buf.as_mut_ptr() as *mut libc::c_void,
260 to_read,
261 read_off,
262 )
263 };
264 if n < 0 {
265 return Err(io::Error::last_os_error());
266 }
267 if n == 0 {
268 return Err(io::Error::new(
269 io::ErrorKind::UnexpectedEof,
270 "unexpected EOF mid-extent",
271 ));
272 }
273 let n = n as usize;
274
275 let mut written: usize = 0;
276 while written < n {
277 let w_off = (off + copied + written as u64) as i64;
278 let w = unsafe {
279 libc::pwrite(
280 dst_fd,
281 buf[written..n].as_ptr() as *const libc::c_void,
282 n - written,
283 w_off,
284 )
285 };
286 if w < 0 {
287 return Err(io::Error::last_os_error());
288 }
289 if w == 0 {
290 return Err(io::Error::new(
291 io::ErrorKind::WriteZero,
292 "pwrite returned 0",
293 ));
294 }
295 written += w as usize;
296 }
297 copied += n as u64;
298 }
299 Ok(())
300}
301
302#[cfg(windows)]
303fn write_nonzero_runs(dst: &mut File, base_offset: u64, bytes: &[u8]) -> io::Result<()> {
304 let mut cursor = 0;
305 while cursor < bytes.len() {
306 while cursor < bytes.len() && bytes[cursor] == 0 {
307 cursor += 1;
308 }
309 if cursor == bytes.len() {
310 break;
311 }
312
313 let start = cursor;
314 while cursor < bytes.len() && bytes[cursor] != 0 {
315 cursor += 1;
316 }
317
318 dst.seek(SeekFrom::Start(base_offset + start as u64))?;
319 dst.write_all(&bytes[start..cursor])?;
320 }
321
322 Ok(())
323}
324
325#[cfg(test)]
330mod tests {
331 use super::*;
332 use std::io::{Read, Seek, SeekFrom, Write};
333 #[cfg(unix)]
334 use std::os::unix::fs::MetadataExt;
335
336 fn make_sparse(path: &Path, len: u64, data_offsets: &[u64]) -> io::Result<()> {
339 let mut f = OpenOptions::new()
340 .read(true)
341 .write(true)
342 .create(true)
343 .truncate(true)
344 .open(path)?;
345 f.set_len(len)?;
346 for &off in data_offsets {
347 let buf = vec![0xAB_u8; 64 * 1024];
348 f.seek(SeekFrom::Start(off))?;
349 f.write_all(&buf)?;
350 }
351 f.sync_all()?;
352 Ok(())
353 }
354
355 #[test]
356 fn round_trip_small() {
357 let dir = tempfile::tempdir().unwrap();
358 let src = dir.path().join("src.bin");
359 let dst = dir.path().join("dst.bin");
360
361 std::fs::write(&src, b"hello world").unwrap();
362 let n = fast_copy(&src, &dst).unwrap();
363 assert_eq!(n, 11);
364 assert_eq!(std::fs::read(&dst).unwrap(), b"hello world");
365 }
366
367 #[test]
368 fn sparse_copy_preserves_holes_and_data() {
369 let dir = tempfile::tempdir().unwrap();
373 let src = dir.path().join("src.bin");
374 let dst = dir.path().join("dst.bin");
375
376 let len: u64 = 16 * 1024 * 1024;
377 let offsets = [0u64, 4 * 1024 * 1024, 8 * 1024 * 1024, 12 * 1024 * 1024];
378 make_sparse(&src, len, &offsets).unwrap();
379
380 let n = sparse_copy(&src, &dst).unwrap();
381 assert_eq!(n, len);
382
383 let dst_meta = std::fs::metadata(&dst).unwrap();
385 assert_eq!(dst_meta.len(), len);
386
387 let mut buf = [0u8; 64 * 1024];
389 let mut dst_file = File::open(&dst).unwrap();
390 for &off in &offsets {
391 dst_file.seek(SeekFrom::Start(off)).unwrap();
392 dst_file.read_exact(&mut buf).unwrap();
393 assert!(buf.iter().all(|&b| b == 0xAB));
394 }
395
396 #[cfg(unix)]
403 {
404 let src_bytes_on_disk = std::fs::metadata(&src).unwrap().blocks() * 512;
405 let dst_bytes_on_disk = dst_meta.blocks() * 512;
406 if src_bytes_on_disk < len / 2 {
407 assert!(
411 dst_bytes_on_disk < len / 2,
412 "source is sparse ({src_bytes_on_disk} bytes on disk) but destination densified to {dst_bytes_on_disk} bytes for an apparent size of {len}",
413 );
414 assert!(
415 dst_bytes_on_disk <= src_bytes_on_disk * 4 + 1024 * 1024,
416 "destination allocated significantly more than source: src={src_bytes_on_disk} dst={dst_bytes_on_disk}",
417 );
418 } else {
419 eprintln!(
420 "filesystem did not sparsify the source (src_bytes_on_disk={src_bytes_on_disk}, apparent={len}); sparseness preservation not exercised in this run",
421 );
422 assert!(
425 dst_bytes_on_disk <= src_bytes_on_disk + 1024 * 1024,
426 "destination grew beyond source footprint: src={src_bytes_on_disk} dst={dst_bytes_on_disk}",
427 );
428 }
429 }
430 }
431
432 #[test]
433 fn fast_copy_matches_source_size() {
434 let dir = tempfile::tempdir().unwrap();
435 let src = dir.path().join("src.bin");
436 let dst = dir.path().join("dst.bin");
437
438 let len: u64 = 4 * 1024 * 1024;
439 make_sparse(&src, len, &[0, 2 * 1024 * 1024]).unwrap();
440
441 let n = fast_copy(&src, &dst).unwrap();
442 assert_eq!(n, len);
443 assert_eq!(std::fs::metadata(&dst).unwrap().len(), len);
444 }
445
446 #[test]
447 fn missing_source_errors() {
448 let dir = tempfile::tempdir().unwrap();
449 let err = fast_copy(&dir.path().join("nope.bin"), &dir.path().join("dst.bin")).unwrap_err();
450 assert_eq!(err.kind(), io::ErrorKind::NotFound);
451 }
452}