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};
31#[cfg(windows)]
32use std::os::windows::io::AsRawHandle;
33use std::path::Path;
34#[cfg(windows)]
35use std::ptr;
36
37#[cfg(windows)]
38use crate::extent::{ExtentMap, mark_sparse};
39#[cfg(windows)]
40use windows_sys::Win32::Foundation::HANDLE;
41#[cfg(windows)]
42use windows_sys::Win32::Storage::FileSystem::GetVolumeInformationByHandleW;
43#[cfg(windows)]
44use windows_sys::Win32::System::IO::DeviceIoControl;
45#[cfg(windows)]
46use windows_sys::Win32::System::Ioctl::{
47 DUPLICATE_EXTENTS_DATA, FSCTL_DUPLICATE_EXTENTS_TO_FILE, FSCTL_GET_INTEGRITY_INFORMATION,
48 FSCTL_GET_INTEGRITY_INFORMATION_BUFFER, FSCTL_SET_INTEGRITY_INFORMATION,
49 FSCTL_SET_INTEGRITY_INFORMATION_BUFFER,
50};
51#[cfg(windows)]
52use windows_sys::Win32::System::SystemServices::FILE_SUPPORTS_BLOCK_REFCOUNTING;
53
54#[cfg(windows)]
60const WINDOWS_CLONE_ALIGNMENT: u64 = 64 * 1024;
61
62#[cfg(windows)]
64const WINDOWS_MAX_CLONE_CHUNK: u64 =
65 (u32::MAX as u64 / WINDOWS_CLONE_ALIGNMENT) * WINDOWS_CLONE_ALIGNMENT;
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum FastCopyStrategy {
74 Reflink,
76 SparseCopy,
78}
79
80#[cfg(windows)]
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83enum WindowsSparseCopyStrategy {
84 AllocatedRanges,
86 NonzeroRuns,
88}
89
90pub fn fast_copy(src: &Path, dst: &Path) -> io::Result<u64> {
104 fast_copy_with_strategy(src, dst).map(|(len, _)| len)
105}
106
107pub fn fast_copy_with_strategy(src: &Path, dst: &Path) -> io::Result<(u64, FastCopyStrategy)> {
109 fast_copy_impl(src, dst, true)
110}
111
112pub fn fast_copy_without_sync(src: &Path, dst: &Path) -> io::Result<(u64, FastCopyStrategy)> {
119 fast_copy_impl(src, dst, false)
120}
121
122fn fast_copy_impl(
123 src: &Path,
124 dst: &Path,
125 sync_destination: bool,
126) -> io::Result<(u64, FastCopyStrategy)> {
127 let src_len = std::fs::metadata(src)?.len();
133
134 match reflink_impl(src, dst) {
138 Ok(()) => return Ok((src_len, FastCopyStrategy::Reflink)),
139 Err(e) if is_reflink_unsupported(&e) => {
140 }
142 Err(e) => return Err(e),
143 }
144
145 sparse_copy_impl(src, dst, sync_destination).map(|len| (len, FastCopyStrategy::SparseCopy))
146}
147
148pub fn reflink(src: &Path, dst: &Path) -> io::Result<u64> {
150 let src_len = std::fs::metadata(src)?.len();
151 reflink_impl(src, dst)?;
152 Ok(src_len)
153}
154
155pub fn sparse_copy(src: &Path, dst: &Path) -> io::Result<u64> {
161 sparse_copy_impl(src, dst, true)
162}
163
164#[cfg(unix)]
165fn sparse_copy_impl(src: &Path, dst: &Path, sync_destination: bool) -> io::Result<u64> {
166 let src_file = File::open(src)?;
167
168 let dst_file = OpenOptions::new()
169 .read(true)
170 .write(true)
171 .create(true)
172 .truncate(true)
173 .open(dst)?;
174 sparse_copy_files(&src_file, &dst_file, sync_destination)
175}
176
177#[cfg(target_os = "linux")]
180pub fn sparse_copy_file_without_sync(src: &File, dst: &File) -> io::Result<u64> {
181 use std::os::unix::fs::MetadataExt;
182 let source = src.metadata()?;
183 let target = dst.metadata()?;
184 if source.dev() == target.dev() && source.ino() == target.ino() {
185 return Err(io::Error::other("cannot copy memory backing onto itself"));
186 }
187 dst.set_len(0)?;
188 sparse_copy_files(src, dst, false)
189}
190
191#[cfg(unix)]
192fn sparse_copy_files(src_file: &File, dst_file: &File, sync_destination: bool) -> io::Result<u64> {
193 let len = src_file.metadata()?.len();
194 dst_file.set_len(len)?;
197
198 let src_fd = src_file.as_raw_fd();
199 let dst_fd = dst_file.as_raw_fd();
200
201 let mut off: i64 = 0;
202 while (off as u64) < len {
203 let data_start = unsafe { libc::lseek(src_fd, off, libc::SEEK_DATA) };
205 if data_start < 0 {
206 let err = io::Error::last_os_error();
207 if err.raw_os_error() == Some(libc::ENXIO) {
209 break;
210 }
211 return Err(err);
212 }
213 let data_end = unsafe { libc::lseek(src_fd, data_start, libc::SEEK_HOLE) };
215 if data_end < 0 {
216 return Err(io::Error::last_os_error());
217 }
218 let data_end = (data_end as u64).min(len);
219 let data_start = data_start as u64;
220 if data_end <= data_start {
221 break;
222 }
223
224 #[cfg(target_os = "linux")]
225 if !sync_destination {
226 copy_local_extent(src_fd, dst_fd, data_start, data_end - data_start)?;
230 } else {
231 copy_extent(src_fd, dst_fd, data_start, data_end - data_start)?;
232 }
233 #[cfg(not(target_os = "linux"))]
234 copy_extent(src_fd, dst_fd, data_start, data_end - data_start)?;
235 off = data_end as i64;
236 }
237
238 if sync_destination {
239 dst_file.sync_all()?;
240 }
241 Ok(len)
242}
243
244#[cfg(unix)]
246fn reflink_impl(src: &Path, dst: &Path) -> io::Result<()> {
247 reflink_copy::reflink(src, dst)
248}
249
250#[cfg(windows)]
252fn reflink_impl(src: &Path, dst: &Path) -> io::Result<()> {
253 let mut src_file = File::open(src)?;
254 let mut dst_file = OpenOptions::new()
255 .read(true)
256 .write(true)
257 .create_new(true)
258 .open(dst)?;
259
260 let result = reflink_windows_files(&mut src_file, &mut dst_file);
261 drop(dst_file);
262 drop(src_file);
263 if result.is_err() {
264 let _ = std::fs::remove_file(dst);
265 }
266 result
267}
268
269#[cfg(not(any(unix, windows)))]
270fn reflink_impl(_src: &Path, _dst: &Path) -> io::Result<()> {
271 Err(io::Error::new(
272 io::ErrorKind::Unsupported,
273 "filesystem reflinks are unsupported on this platform",
274 ))
275}
276
277#[cfg(windows)]
278fn sparse_copy_impl(src: &Path, dst: &Path, sync_destination: bool) -> io::Result<u64> {
279 const BUF_SIZE: usize = 1024 * 1024;
280
281 let mut src_file = File::open(src)?;
282 let len = src_file.metadata()?.len();
283
284 let mut dst_file = OpenOptions::new()
285 .read(true)
286 .write(true)
287 .create(true)
288 .truncate(true)
289 .open(dst)?;
290 dst_file.set_len(len)?;
291 mark_sparse(&dst_file)?;
292
293 copy_windows_sparse_data(&mut src_file, &mut dst_file, BUF_SIZE)?;
294
295 if sync_destination {
296 dst_file.sync_all()?;
297 }
298 Ok(len)
299}
300
301#[cfg(windows)]
306fn reflink_windows_files(src: &mut File, dst: &mut File) -> io::Result<()> {
307 let src_volume = windows_volume_identity(src)?;
308 let dst_volume = windows_volume_identity(dst)?;
309 if src_volume.0 != dst_volume.0 {
310 return Err(io::Error::new(
311 io::ErrorKind::Unsupported,
312 "Windows block cloning requires source and destination on the same volume",
313 ));
314 }
315 if src_volume.1 & FILE_SUPPORTS_BLOCK_REFCOUNTING == 0
316 || dst_volume.1 & FILE_SUPPORTS_BLOCK_REFCOUNTING == 0
317 {
318 return Err(io::Error::new(
319 io::ErrorKind::Unsupported,
320 "destination volume does not advertise block-refcounting support",
321 ));
322 }
323
324 mark_sparse(dst)?;
325 match_windows_integrity(src, dst)?;
326
327 let len = src.metadata()?.len();
328 dst.set_len(len)?;
329 let clone_len = len / WINDOWS_CLONE_ALIGNMENT * WINDOWS_CLONE_ALIGNMENT;
330 let mut offset = 0u64;
331 while offset < clone_len {
332 let chunk = (clone_len - offset).min(WINDOWS_MAX_CLONE_CHUNK);
333 duplicate_windows_extents(src, dst, offset, chunk)?;
334 offset += chunk;
335 }
336 if clone_len < len {
337 copy_windows_tail(src, dst, clone_len, len - clone_len)?;
338 }
339 Ok(())
340}
341
342#[cfg(windows)]
343fn windows_volume_identity(file: &File) -> io::Result<(u32, u32)> {
344 let mut serial = 0u32;
345 let mut flags = 0u32;
346 let ok = unsafe {
347 GetVolumeInformationByHandleW(
348 file.as_raw_handle() as HANDLE,
349 ptr::null_mut(),
350 0,
351 &mut serial,
352 ptr::null_mut(),
353 &mut flags,
354 ptr::null_mut(),
355 0,
356 )
357 };
358 if ok == 0 {
359 return Err(io::Error::last_os_error());
360 }
361 Ok((serial, flags))
362}
363
364#[cfg(windows)]
365fn match_windows_integrity(src: &File, dst: &File) -> io::Result<()> {
366 let Some(src_info) = get_windows_integrity(src)? else {
367 return Ok(());
368 };
369 let Some(dst_info) = get_windows_integrity(dst)? else {
370 return Ok(());
371 };
372 if src_info.ChecksumAlgorithm == dst_info.ChecksumAlgorithm && src_info.Flags == dst_info.Flags
373 {
374 return Ok(());
375 }
376
377 let info = FSCTL_SET_INTEGRITY_INFORMATION_BUFFER {
378 ChecksumAlgorithm: src_info.ChecksumAlgorithm,
379 Reserved: 0,
380 Flags: src_info.Flags,
381 };
382 let mut returned = 0u32;
383 let ok = unsafe {
384 DeviceIoControl(
385 dst.as_raw_handle() as HANDLE,
386 FSCTL_SET_INTEGRITY_INFORMATION,
387 &info as *const _ as *const _,
388 size_of::<FSCTL_SET_INTEGRITY_INFORMATION_BUFFER>() as u32,
389 ptr::null_mut(),
390 0,
391 &mut returned,
392 ptr::null_mut(),
393 )
394 };
395 if ok == 0 {
396 return Err(io::Error::last_os_error());
397 }
398 Ok(())
399}
400
401#[cfg(windows)]
402fn get_windows_integrity(
403 file: &File,
404) -> io::Result<Option<FSCTL_GET_INTEGRITY_INFORMATION_BUFFER>> {
405 let mut info = FSCTL_GET_INTEGRITY_INFORMATION_BUFFER::default();
406 let mut returned = 0u32;
407 let ok = unsafe {
408 DeviceIoControl(
409 file.as_raw_handle() as HANDLE,
410 FSCTL_GET_INTEGRITY_INFORMATION,
411 ptr::null(),
412 0,
413 &mut info as *mut _ as *mut _,
414 size_of::<FSCTL_GET_INTEGRITY_INFORMATION_BUFFER>() as u32,
415 &mut returned,
416 ptr::null_mut(),
417 )
418 };
419 if ok != 0 {
420 return Ok(Some(info));
421 }
422 let error = io::Error::last_os_error();
423 if is_reflink_unsupported(&error) {
424 Ok(None)
425 } else {
426 Err(error)
427 }
428}
429
430#[cfg(windows)]
431fn duplicate_windows_extents(src: &File, dst: &File, offset: u64, len: u64) -> io::Result<()> {
432 let request = DUPLICATE_EXTENTS_DATA {
433 FileHandle: src.as_raw_handle() as HANDLE,
434 SourceFileOffset: offset as i64,
435 TargetFileOffset: offset as i64,
436 ByteCount: len as i64,
437 };
438 let mut returned = 0u32;
439 let ok = unsafe {
440 DeviceIoControl(
441 dst.as_raw_handle() as HANDLE,
442 FSCTL_DUPLICATE_EXTENTS_TO_FILE,
443 &request as *const _ as *const _,
444 size_of::<DUPLICATE_EXTENTS_DATA>() as u32,
445 ptr::null_mut(),
446 0,
447 &mut returned,
448 ptr::null_mut(),
449 )
450 };
451 if ok == 0 {
452 return Err(io::Error::last_os_error());
453 }
454 Ok(())
455}
456
457#[cfg(windows)]
458fn copy_windows_tail(src: &mut File, dst: &mut File, offset: u64, len: u64) -> io::Result<()> {
459 src.seek(SeekFrom::Start(offset))?;
460 dst.seek(SeekFrom::Start(offset))?;
461 let copied = io::copy(&mut src.take(len), dst)?;
462 if copied != len {
463 return Err(io::Error::new(
464 io::ErrorKind::UnexpectedEof,
465 format!("Windows reflink tail copied {copied} of {len} bytes"),
466 ));
467 }
468 Ok(())
469}
470
471#[cfg(windows)]
472fn copy_windows_range(
473 src: &mut File,
474 dst: &mut File,
475 offset: u64,
476 len: u64,
477 buf: &mut [u8],
478) -> io::Result<()> {
479 src.seek(SeekFrom::Start(offset))?;
480 dst.seek(SeekFrom::Start(offset))?;
481
482 let mut remaining = len;
483 while remaining != 0 {
484 let chunk_len = remaining.min(buf.len() as u64) as usize;
485 src.read_exact(&mut buf[..chunk_len])?;
486 dst.write_all(&buf[..chunk_len])?;
487 remaining -= chunk_len as u64;
488 }
489 Ok(())
490}
491
492#[cfg(windows)]
493fn copy_windows_sparse_data(
494 src: &mut File,
495 dst: &mut File,
496 buf_size: usize,
497) -> io::Result<WindowsSparseCopyStrategy> {
498 if let Some(map) = ExtentMap::scan_file(src)? {
499 let mut buf = vec![0u8; buf_size];
503 for (offset, extent_len) in map.extents {
504 copy_windows_range(src, dst, offset, extent_len, &mut buf)?;
505 }
506 Ok(WindowsSparseCopyStrategy::AllocatedRanges)
507 } else {
508 copy_windows_nonzero_runs(src, dst, buf_size)?;
512 Ok(WindowsSparseCopyStrategy::NonzeroRuns)
513 }
514}
515
516#[cfg(windows)]
517fn copy_windows_nonzero_runs(src: &mut File, dst: &mut File, buf_size: usize) -> io::Result<()> {
518 src.seek(SeekFrom::Start(0))?;
519 let mut offset = 0u64;
520 let mut buf = vec![0u8; buf_size];
521 loop {
522 let n = src.read(&mut buf)?;
523 if n == 0 {
524 break;
525 }
526
527 write_nonzero_runs(dst, offset, &buf[..n])?;
528 offset += n as u64;
529 }
530 Ok(())
531}
532
533fn is_reflink_unsupported(e: &io::Error) -> bool {
540 if matches!(e.kind(), io::ErrorKind::Unsupported) {
541 return true;
542 }
543
544 let Some(code) = e.raw_os_error() else {
545 return false;
546 };
547
548 #[cfg(target_os = "linux")]
549 let aliases: &[i32] = &[libc::ENOTSUP, libc::EXDEV, libc::EINVAL];
550 #[cfg(all(unix, not(target_os = "linux")))]
551 let aliases: &[i32] = &[libc::ENOTSUP, libc::EOPNOTSUPP, libc::EXDEV, libc::EINVAL];
552 #[cfg(windows)]
553 let aliases: &[i32] = &[
554 1, 17, 50, 87, 124, 775, ];
561
562 #[cfg(windows)]
563 {
564 let win32_code = (code as u32 & 0xffff) as i32;
565 aliases.contains(&code) || aliases.contains(&win32_code)
566 }
567
568 #[cfg(unix)]
569 aliases.contains(&code)
570}
571
572#[cfg(unix)]
573fn copy_extent(src_fd: RawFd, dst_fd: RawFd, off: u64, len: u64) -> io::Result<()> {
574 read_write_extent(src_fd, dst_fd, off, len)
576}
577
578#[cfg(target_os = "linux")]
581fn copy_local_extent(src_fd: RawFd, dst_fd: RawFd, off: u64, len: u64) -> io::Result<()> {
582 let mut copied = 0u64;
583 while copied < len {
584 let mut source_offset = (off + copied) as libc::loff_t;
585 let mut destination_offset = source_offset;
586 let count = (len - copied).min(32 * 1024 * 1024) as usize;
587 let result = unsafe {
588 libc::copy_file_range(
589 src_fd,
590 &mut source_offset,
591 dst_fd,
592 &mut destination_offset,
593 count,
594 0,
595 )
596 };
597 if result > 0 {
598 copied += result as u64;
599 continue;
600 }
601 if result == 0 {
602 return Err(io::Error::new(
603 io::ErrorKind::UnexpectedEof,
604 "kernel copy reached EOF mid-extent",
605 ));
606 }
607 let error = io::Error::last_os_error();
608 if error.kind() == io::ErrorKind::Interrupted {
609 continue;
610 }
611 if matches!(
612 error.raw_os_error(),
613 Some(libc::EXDEV | libc::ENOSYS | libc::EOPNOTSUPP | libc::EINVAL)
614 ) {
615 return read_write_extent(src_fd, dst_fd, off + copied, len - copied);
618 }
619 return Err(error);
620 }
621 Ok(())
622}
623
624#[cfg(unix)]
631fn read_write_extent(src_fd: RawFd, dst_fd: RawFd, off: u64, len: u64) -> io::Result<()> {
632 const BUF_SIZE: usize = 1024 * 1024;
633 let mut buf = vec![0u8; BUF_SIZE];
634 let mut copied: u64 = 0;
635
636 while copied < len {
637 let to_read = (len - copied).min(BUF_SIZE as u64) as usize;
638 let read_off = (off + copied) as i64;
639 let n = unsafe {
640 libc::pread(
641 src_fd,
642 buf.as_mut_ptr() as *mut libc::c_void,
643 to_read,
644 read_off,
645 )
646 };
647 if n < 0 {
648 return Err(io::Error::last_os_error());
649 }
650 if n == 0 {
651 return Err(io::Error::new(
652 io::ErrorKind::UnexpectedEof,
653 "unexpected EOF mid-extent",
654 ));
655 }
656 let n = n as usize;
657
658 let mut written: usize = 0;
659 while written < n {
660 let w_off = (off + copied + written as u64) as i64;
661 let w = unsafe {
662 libc::pwrite(
663 dst_fd,
664 buf[written..n].as_ptr() as *const libc::c_void,
665 n - written,
666 w_off,
667 )
668 };
669 if w < 0 {
670 return Err(io::Error::last_os_error());
671 }
672 if w == 0 {
673 return Err(io::Error::new(
674 io::ErrorKind::WriteZero,
675 "pwrite returned 0",
676 ));
677 }
678 written += w as usize;
679 }
680 copied += n as u64;
681 }
682 Ok(())
683}
684
685#[cfg(windows)]
686fn write_nonzero_runs(dst: &mut File, base_offset: u64, bytes: &[u8]) -> io::Result<()> {
687 let mut cursor = 0;
688 while cursor < bytes.len() {
689 while cursor < bytes.len() && bytes[cursor] == 0 {
690 cursor += 1;
691 }
692 if cursor == bytes.len() {
693 break;
694 }
695
696 let start = cursor;
697 while cursor < bytes.len() && bytes[cursor] != 0 {
698 cursor += 1;
699 }
700
701 dst.seek(SeekFrom::Start(base_offset + start as u64))?;
702 dst.write_all(&bytes[start..cursor])?;
703 }
704
705 Ok(())
706}
707
708#[cfg(test)]
713mod tests {
714 use super::*;
715 use std::io::{Read, Seek, SeekFrom, Write};
716 #[cfg(unix)]
717 use std::os::unix::fs::MetadataExt;
718
719 fn make_sparse(path: &Path, len: u64, data_offsets: &[u64]) -> io::Result<()> {
722 let mut f = OpenOptions::new()
723 .read(true)
724 .write(true)
725 .create(true)
726 .truncate(true)
727 .open(path)?;
728 #[cfg(windows)]
729 mark_sparse(&f)?;
730 f.set_len(len)?;
731 for &off in data_offsets {
732 let buf = vec![0xAB_u8; 64 * 1024];
733 f.seek(SeekFrom::Start(off))?;
734 f.write_all(&buf)?;
735 }
736 f.sync_all()?;
737 Ok(())
738 }
739
740 #[test]
741 fn round_trip_small() {
742 let dir = tempfile::tempdir().unwrap();
743 let src = dir.path().join("src.bin");
744 let dst = dir.path().join("dst.bin");
745
746 std::fs::write(&src, b"hello world").unwrap();
747 let n = fast_copy(&src, &dst).unwrap();
748 assert_eq!(n, 11);
749 assert_eq!(std::fs::read(&dst).unwrap(), b"hello world");
750 }
751
752 #[test]
753 fn sparse_copy_preserves_holes_and_data() {
754 let dir = tempfile::tempdir().unwrap();
758 let src = dir.path().join("src.bin");
759 let dst = dir.path().join("dst.bin");
760
761 let len: u64 = 16 * 1024 * 1024;
762 let offsets = [0u64, 4 * 1024 * 1024, 8 * 1024 * 1024, 12 * 1024 * 1024];
763 make_sparse(&src, len, &offsets).unwrap();
764
765 let n = sparse_copy(&src, &dst).unwrap();
766 assert_eq!(n, len);
767
768 let dst_meta = std::fs::metadata(&dst).unwrap();
770 assert_eq!(dst_meta.len(), len);
771
772 let mut buf = [0u8; 64 * 1024];
774 let mut dst_file = File::open(&dst).unwrap();
775 for &off in &offsets {
776 dst_file.seek(SeekFrom::Start(off)).unwrap();
777 dst_file.read_exact(&mut buf).unwrap();
778 assert!(buf.iter().all(|&b| b == 0xAB));
779 }
780
781 #[cfg(unix)]
788 {
789 let src_bytes_on_disk = std::fs::metadata(&src).unwrap().blocks() * 512;
790 let dst_bytes_on_disk = dst_meta.blocks() * 512;
791 if src_bytes_on_disk < len / 2 {
792 assert!(
796 dst_bytes_on_disk < len / 2,
797 "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}",
798 );
799 assert!(
800 dst_bytes_on_disk <= src_bytes_on_disk * 4 + 1024 * 1024,
801 "destination allocated significantly more than source: src={src_bytes_on_disk} dst={dst_bytes_on_disk}",
802 );
803 } else {
804 eprintln!(
805 "filesystem did not sparsify the source (src_bytes_on_disk={src_bytes_on_disk}, apparent={len}); sparseness preservation not exercised in this run",
806 );
807 assert!(
810 dst_bytes_on_disk <= src_bytes_on_disk + 1024 * 1024,
811 "destination grew beyond source footprint: src={src_bytes_on_disk} dst={dst_bytes_on_disk}",
812 );
813 }
814 }
815 }
816
817 #[test]
818 fn unsynced_sparse_copy_is_complete_and_independent() {
819 let dir = tempfile::tempdir().unwrap();
820 let src = dir.path().join("source");
821 let dst = dir.path().join("local-backing");
822 let length = 8 * 1024 * 1024;
823 make_sparse(&src, length, &[0, 4 * 1024 * 1024]).unwrap();
824
825 assert_eq!(sparse_copy_impl(&src, &dst, false).unwrap(), length);
828 assert_eq!(std::fs::read(&src).unwrap(), std::fs::read(&dst).unwrap());
829 #[cfg(unix)]
830 {
831 let source = std::fs::metadata(&src).unwrap();
832 let copied = std::fs::metadata(&dst).unwrap();
833 if source.blocks() * 512 < length / 2 {
834 assert!(copied.blocks() * 512 < length / 2);
835 }
836 }
837 let mut copied = OpenOptions::new().write(true).open(&dst).unwrap();
838 copied.write_all(b"child").unwrap();
839 let original = std::fs::read(&src).unwrap();
840 assert_eq!(&original[..5], &[0xAB; 5]);
841 std::fs::remove_file(&src).unwrap();
842 assert_eq!(&std::fs::read(&dst).unwrap()[..5], b"child");
843 }
844
845 #[test]
846 fn unsynced_fast_copy_preserves_contents() {
847 let dir = tempfile::tempdir().unwrap();
848 let src = dir.path().join("source");
849 let dst = dir.path().join("local-backing");
850 std::fs::write(&src, b"local generation").unwrap();
851 let (length, _) = fast_copy_without_sync(&src, &dst).unwrap();
852 assert_eq!(length, 16);
853 assert_eq!(std::fs::read(&dst).unwrap(), b"local generation");
854 }
855
856 #[cfg(target_os = "linux")]
857 #[test]
858 fn descriptor_copy_rejects_alias_before_truncating() {
859 let mut source = tempfile::tempfile().unwrap();
860 source.write_all(b"keep this generation").unwrap();
861 let alias = source.try_clone().unwrap();
862 assert!(sparse_copy_file_without_sync(&source, &alias).is_err());
863 assert_eq!(source.metadata().unwrap().len(), 20);
864 source.rewind().unwrap();
865 let mut bytes = Vec::new();
866 source.read_to_end(&mut bytes).unwrap();
867 assert_eq!(bytes, b"keep this generation");
868 }
869
870 #[cfg(target_os = "linux")]
871 #[test]
872 fn descriptor_copy_clears_old_contents_and_preserves_holes() {
873 let dir = tempfile::tempdir().unwrap();
874 let path = dir.path().join("source");
875 let length = 8 * 1024 * 1024;
876 make_sparse(&path, length, &[0, 4 * 1024 * 1024]).unwrap();
877 let source = File::open(&path).unwrap();
878 let mut target = tempfile::tempfile().unwrap();
879 target
880 .write_all(&vec![0xEE; length as usize + 4096])
881 .unwrap();
882 assert_eq!(
883 sparse_copy_file_without_sync(&source, &target).unwrap(),
884 length
885 );
886 target.rewind().unwrap();
887 let mut bytes = Vec::new();
888 target.read_to_end(&mut bytes).unwrap();
889 assert_eq!(bytes, std::fs::read(path).unwrap());
890 if source.metadata().unwrap().blocks() * 512 < length / 2 {
891 assert!(target.metadata().unwrap().blocks() * 512 < length / 2);
892 }
893 }
894
895 #[cfg(target_os = "linux")]
896 #[test]
897 fn local_kernel_copy_reports_truncated_extent() {
898 let dir = tempfile::tempdir().unwrap();
899 let src = dir.path().join("source");
900 let dst = dir.path().join("destination");
901 std::fs::write(&src, b"short").unwrap();
902 let source = File::open(src).unwrap();
903 let destination = File::create(dst).unwrap();
904 let error =
905 copy_local_extent(source.as_raw_fd(), destination.as_raw_fd(), 0, 4096).unwrap_err();
906 assert_eq!(error.kind(), io::ErrorKind::UnexpectedEof);
907 }
908
909 #[cfg(windows)]
910 #[test]
911 fn windows_sparse_copy_uses_allocated_ranges_when_available() {
912 let dir = tempfile::tempdir().unwrap();
913 let src = dir.path().join("src.bin");
914 let dst = dir.path().join("dst.bin");
915 let len = 8 * 1024 * 1024;
916
917 make_sparse(&src, len, &[0, 4 * 1024 * 1024]).unwrap();
918 let mut src_file = File::open(&src).unwrap();
919 if ExtentMap::scan_file(&src_file).unwrap().is_none() {
920 eprintln!("filesystem cannot enumerate allocated ranges; strategy not exercised");
921 return;
922 }
923
924 let mut dst_file = OpenOptions::new()
925 .read(true)
926 .write(true)
927 .create(true)
928 .truncate(true)
929 .open(&dst)
930 .unwrap();
931 dst_file.set_len(len).unwrap();
932 mark_sparse(&dst_file).unwrap();
933
934 let strategy = copy_windows_sparse_data(&mut src_file, &mut dst_file, 1024 * 1024).unwrap();
935 assert_eq!(strategy, WindowsSparseCopyStrategy::AllocatedRanges);
936 }
937
938 #[test]
939 fn fast_copy_matches_source_size() {
940 let dir = tempfile::tempdir().unwrap();
941 let src = dir.path().join("src.bin");
942 let dst = dir.path().join("dst.bin");
943
944 let len: u64 = 4 * 1024 * 1024;
945 make_sparse(&src, len, &[0, 2 * 1024 * 1024]).unwrap();
946
947 let n = fast_copy(&src, &dst).unwrap();
948 assert_eq!(n, len);
949 assert_eq!(std::fs::metadata(&dst).unwrap().len(), len);
950 }
951
952 #[test]
953 fn missing_source_errors() {
954 let dir = tempfile::tempdir().unwrap();
955 let err = fast_copy(&dir.path().join("nope.bin"), &dir.path().join("dst.bin")).unwrap_err();
956 assert_eq!(err.kind(), io::ErrorKind::NotFound);
957 }
958}