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 let src_len = std::fs::metadata(src)?.len();
115
116 match reflink_impl(src, dst) {
120 Ok(()) => return Ok((src_len, FastCopyStrategy::Reflink)),
121 Err(e) if is_reflink_unsupported(&e) => {
122 }
124 Err(e) => return Err(e),
125 }
126
127 sparse_copy(src, dst).map(|len| (len, FastCopyStrategy::SparseCopy))
128}
129
130pub fn reflink(src: &Path, dst: &Path) -> io::Result<u64> {
132 let src_len = std::fs::metadata(src)?.len();
133 reflink_impl(src, dst)?;
134 Ok(src_len)
135}
136
137pub fn sparse_copy(src: &Path, dst: &Path) -> io::Result<u64> {
143 sparse_copy_impl(src, dst)
144}
145
146#[cfg(unix)]
147fn sparse_copy_impl(src: &Path, dst: &Path) -> io::Result<u64> {
148 let src_file = File::open(src)?;
149 let len = src_file.metadata()?.len();
150
151 let dst_file = OpenOptions::new()
152 .read(true)
153 .write(true)
154 .create(true)
155 .truncate(true)
156 .open(dst)?;
157 dst_file.set_len(len)?;
160
161 let src_fd = src_file.as_raw_fd();
162 let dst_fd = dst_file.as_raw_fd();
163
164 let mut off: i64 = 0;
165 while (off as u64) < len {
166 let data_start = unsafe { libc::lseek(src_fd, off, libc::SEEK_DATA) };
168 if data_start < 0 {
169 let err = io::Error::last_os_error();
170 if err.raw_os_error() == Some(libc::ENXIO) {
172 break;
173 }
174 return Err(err);
175 }
176 let data_end = unsafe { libc::lseek(src_fd, data_start, libc::SEEK_HOLE) };
178 if data_end < 0 {
179 return Err(io::Error::last_os_error());
180 }
181 let data_end = (data_end as u64).min(len);
182 let data_start = data_start as u64;
183 if data_end <= data_start {
184 break;
185 }
186
187 copy_extent(src_fd, dst_fd, data_start, data_end - data_start)?;
188 off = data_end as i64;
189 }
190
191 dst_file.sync_all()?;
192 Ok(len)
193}
194
195#[cfg(unix)]
197fn reflink_impl(src: &Path, dst: &Path) -> io::Result<()> {
198 reflink_copy::reflink(src, dst)
199}
200
201#[cfg(windows)]
203fn reflink_impl(src: &Path, dst: &Path) -> io::Result<()> {
204 let mut src_file = File::open(src)?;
205 let mut dst_file = OpenOptions::new()
206 .read(true)
207 .write(true)
208 .create_new(true)
209 .open(dst)?;
210
211 let result = reflink_windows_files(&mut src_file, &mut dst_file);
212 drop(dst_file);
213 drop(src_file);
214 if result.is_err() {
215 let _ = std::fs::remove_file(dst);
216 }
217 result
218}
219
220#[cfg(not(any(unix, windows)))]
221fn reflink_impl(_src: &Path, _dst: &Path) -> io::Result<()> {
222 Err(io::Error::new(
223 io::ErrorKind::Unsupported,
224 "filesystem reflinks are unsupported on this platform",
225 ))
226}
227
228#[cfg(windows)]
229fn sparse_copy_impl(src: &Path, dst: &Path) -> io::Result<u64> {
230 const BUF_SIZE: usize = 1024 * 1024;
231
232 let mut src_file = File::open(src)?;
233 let len = src_file.metadata()?.len();
234
235 let mut dst_file = OpenOptions::new()
236 .read(true)
237 .write(true)
238 .create(true)
239 .truncate(true)
240 .open(dst)?;
241 dst_file.set_len(len)?;
242 mark_sparse(&dst_file)?;
243
244 copy_windows_sparse_data(&mut src_file, &mut dst_file, BUF_SIZE)?;
245
246 dst_file.sync_all()?;
247 Ok(len)
248}
249
250#[cfg(windows)]
255fn reflink_windows_files(src: &mut File, dst: &mut File) -> io::Result<()> {
256 let src_volume = windows_volume_identity(src)?;
257 let dst_volume = windows_volume_identity(dst)?;
258 if src_volume.0 != dst_volume.0 {
259 return Err(io::Error::new(
260 io::ErrorKind::Unsupported,
261 "Windows block cloning requires source and destination on the same volume",
262 ));
263 }
264 if src_volume.1 & FILE_SUPPORTS_BLOCK_REFCOUNTING == 0
265 || dst_volume.1 & FILE_SUPPORTS_BLOCK_REFCOUNTING == 0
266 {
267 return Err(io::Error::new(
268 io::ErrorKind::Unsupported,
269 "destination volume does not advertise block-refcounting support",
270 ));
271 }
272
273 mark_sparse(dst)?;
274 match_windows_integrity(src, dst)?;
275
276 let len = src.metadata()?.len();
277 dst.set_len(len)?;
278 let clone_len = len / WINDOWS_CLONE_ALIGNMENT * WINDOWS_CLONE_ALIGNMENT;
279 let mut offset = 0u64;
280 while offset < clone_len {
281 let chunk = (clone_len - offset).min(WINDOWS_MAX_CLONE_CHUNK);
282 duplicate_windows_extents(src, dst, offset, chunk)?;
283 offset += chunk;
284 }
285 if clone_len < len {
286 copy_windows_tail(src, dst, clone_len, len - clone_len)?;
287 }
288 Ok(())
289}
290
291#[cfg(windows)]
292fn windows_volume_identity(file: &File) -> io::Result<(u32, u32)> {
293 let mut serial = 0u32;
294 let mut flags = 0u32;
295 let ok = unsafe {
296 GetVolumeInformationByHandleW(
297 file.as_raw_handle() as HANDLE,
298 ptr::null_mut(),
299 0,
300 &mut serial,
301 ptr::null_mut(),
302 &mut flags,
303 ptr::null_mut(),
304 0,
305 )
306 };
307 if ok == 0 {
308 return Err(io::Error::last_os_error());
309 }
310 Ok((serial, flags))
311}
312
313#[cfg(windows)]
314fn match_windows_integrity(src: &File, dst: &File) -> io::Result<()> {
315 let Some(src_info) = get_windows_integrity(src)? else {
316 return Ok(());
317 };
318 let Some(dst_info) = get_windows_integrity(dst)? else {
319 return Ok(());
320 };
321 if src_info.ChecksumAlgorithm == dst_info.ChecksumAlgorithm && src_info.Flags == dst_info.Flags
322 {
323 return Ok(());
324 }
325
326 let info = FSCTL_SET_INTEGRITY_INFORMATION_BUFFER {
327 ChecksumAlgorithm: src_info.ChecksumAlgorithm,
328 Reserved: 0,
329 Flags: src_info.Flags,
330 };
331 let mut returned = 0u32;
332 let ok = unsafe {
333 DeviceIoControl(
334 dst.as_raw_handle() as HANDLE,
335 FSCTL_SET_INTEGRITY_INFORMATION,
336 &info as *const _ as *const _,
337 size_of::<FSCTL_SET_INTEGRITY_INFORMATION_BUFFER>() as u32,
338 ptr::null_mut(),
339 0,
340 &mut returned,
341 ptr::null_mut(),
342 )
343 };
344 if ok == 0 {
345 return Err(io::Error::last_os_error());
346 }
347 Ok(())
348}
349
350#[cfg(windows)]
351fn get_windows_integrity(
352 file: &File,
353) -> io::Result<Option<FSCTL_GET_INTEGRITY_INFORMATION_BUFFER>> {
354 let mut info = FSCTL_GET_INTEGRITY_INFORMATION_BUFFER::default();
355 let mut returned = 0u32;
356 let ok = unsafe {
357 DeviceIoControl(
358 file.as_raw_handle() as HANDLE,
359 FSCTL_GET_INTEGRITY_INFORMATION,
360 ptr::null(),
361 0,
362 &mut info as *mut _ as *mut _,
363 size_of::<FSCTL_GET_INTEGRITY_INFORMATION_BUFFER>() as u32,
364 &mut returned,
365 ptr::null_mut(),
366 )
367 };
368 if ok != 0 {
369 return Ok(Some(info));
370 }
371 let error = io::Error::last_os_error();
372 if is_reflink_unsupported(&error) {
373 Ok(None)
374 } else {
375 Err(error)
376 }
377}
378
379#[cfg(windows)]
380fn duplicate_windows_extents(src: &File, dst: &File, offset: u64, len: u64) -> io::Result<()> {
381 let request = DUPLICATE_EXTENTS_DATA {
382 FileHandle: src.as_raw_handle() as HANDLE,
383 SourceFileOffset: offset as i64,
384 TargetFileOffset: offset as i64,
385 ByteCount: len as i64,
386 };
387 let mut returned = 0u32;
388 let ok = unsafe {
389 DeviceIoControl(
390 dst.as_raw_handle() as HANDLE,
391 FSCTL_DUPLICATE_EXTENTS_TO_FILE,
392 &request as *const _ as *const _,
393 size_of::<DUPLICATE_EXTENTS_DATA>() as u32,
394 ptr::null_mut(),
395 0,
396 &mut returned,
397 ptr::null_mut(),
398 )
399 };
400 if ok == 0 {
401 return Err(io::Error::last_os_error());
402 }
403 Ok(())
404}
405
406#[cfg(windows)]
407fn copy_windows_tail(src: &mut File, dst: &mut File, offset: u64, len: u64) -> io::Result<()> {
408 src.seek(SeekFrom::Start(offset))?;
409 dst.seek(SeekFrom::Start(offset))?;
410 let copied = io::copy(&mut src.take(len), dst)?;
411 if copied != len {
412 return Err(io::Error::new(
413 io::ErrorKind::UnexpectedEof,
414 format!("Windows reflink tail copied {copied} of {len} bytes"),
415 ));
416 }
417 Ok(())
418}
419
420#[cfg(windows)]
421fn copy_windows_range(
422 src: &mut File,
423 dst: &mut File,
424 offset: u64,
425 len: u64,
426 buf: &mut [u8],
427) -> io::Result<()> {
428 src.seek(SeekFrom::Start(offset))?;
429 dst.seek(SeekFrom::Start(offset))?;
430
431 let mut remaining = len;
432 while remaining != 0 {
433 let chunk_len = remaining.min(buf.len() as u64) as usize;
434 src.read_exact(&mut buf[..chunk_len])?;
435 dst.write_all(&buf[..chunk_len])?;
436 remaining -= chunk_len as u64;
437 }
438 Ok(())
439}
440
441#[cfg(windows)]
442fn copy_windows_sparse_data(
443 src: &mut File,
444 dst: &mut File,
445 buf_size: usize,
446) -> io::Result<WindowsSparseCopyStrategy> {
447 if let Some(map) = ExtentMap::scan_file(src)? {
448 let mut buf = vec![0u8; buf_size];
452 for (offset, extent_len) in map.extents {
453 copy_windows_range(src, dst, offset, extent_len, &mut buf)?;
454 }
455 Ok(WindowsSparseCopyStrategy::AllocatedRanges)
456 } else {
457 copy_windows_nonzero_runs(src, dst, buf_size)?;
461 Ok(WindowsSparseCopyStrategy::NonzeroRuns)
462 }
463}
464
465#[cfg(windows)]
466fn copy_windows_nonzero_runs(src: &mut File, dst: &mut File, buf_size: usize) -> io::Result<()> {
467 src.seek(SeekFrom::Start(0))?;
468 let mut offset = 0u64;
469 let mut buf = vec![0u8; buf_size];
470 loop {
471 let n = src.read(&mut buf)?;
472 if n == 0 {
473 break;
474 }
475
476 write_nonzero_runs(dst, offset, &buf[..n])?;
477 offset += n as u64;
478 }
479 Ok(())
480}
481
482fn is_reflink_unsupported(e: &io::Error) -> bool {
489 if matches!(e.kind(), io::ErrorKind::Unsupported) {
490 return true;
491 }
492
493 let Some(code) = e.raw_os_error() else {
494 return false;
495 };
496
497 #[cfg(target_os = "linux")]
498 let aliases: &[i32] = &[libc::ENOTSUP, libc::EXDEV, libc::EINVAL];
499 #[cfg(all(unix, not(target_os = "linux")))]
500 let aliases: &[i32] = &[libc::ENOTSUP, libc::EOPNOTSUPP, libc::EXDEV, libc::EINVAL];
501 #[cfg(windows)]
502 let aliases: &[i32] = &[
503 1, 17, 50, 87, 124, 775, ];
510
511 #[cfg(windows)]
512 {
513 let win32_code = (code as u32 & 0xffff) as i32;
514 aliases.contains(&code) || aliases.contains(&win32_code)
515 }
516
517 #[cfg(unix)]
518 aliases.contains(&code)
519}
520
521#[cfg(unix)]
522fn copy_extent(src_fd: RawFd, dst_fd: RawFd, off: u64, len: u64) -> io::Result<()> {
523 read_write_extent(src_fd, dst_fd, off, len)
525}
526
527#[cfg(unix)]
534fn read_write_extent(src_fd: RawFd, dst_fd: RawFd, off: u64, len: u64) -> io::Result<()> {
535 const BUF_SIZE: usize = 1024 * 1024;
536 let mut buf = vec![0u8; BUF_SIZE];
537 let mut copied: u64 = 0;
538
539 while copied < len {
540 let to_read = (len - copied).min(BUF_SIZE as u64) as usize;
541 let read_off = (off + copied) as i64;
542 let n = unsafe {
543 libc::pread(
544 src_fd,
545 buf.as_mut_ptr() as *mut libc::c_void,
546 to_read,
547 read_off,
548 )
549 };
550 if n < 0 {
551 return Err(io::Error::last_os_error());
552 }
553 if n == 0 {
554 return Err(io::Error::new(
555 io::ErrorKind::UnexpectedEof,
556 "unexpected EOF mid-extent",
557 ));
558 }
559 let n = n as usize;
560
561 let mut written: usize = 0;
562 while written < n {
563 let w_off = (off + copied + written as u64) as i64;
564 let w = unsafe {
565 libc::pwrite(
566 dst_fd,
567 buf[written..n].as_ptr() as *const libc::c_void,
568 n - written,
569 w_off,
570 )
571 };
572 if w < 0 {
573 return Err(io::Error::last_os_error());
574 }
575 if w == 0 {
576 return Err(io::Error::new(
577 io::ErrorKind::WriteZero,
578 "pwrite returned 0",
579 ));
580 }
581 written += w as usize;
582 }
583 copied += n as u64;
584 }
585 Ok(())
586}
587
588#[cfg(windows)]
589fn write_nonzero_runs(dst: &mut File, base_offset: u64, bytes: &[u8]) -> io::Result<()> {
590 let mut cursor = 0;
591 while cursor < bytes.len() {
592 while cursor < bytes.len() && bytes[cursor] == 0 {
593 cursor += 1;
594 }
595 if cursor == bytes.len() {
596 break;
597 }
598
599 let start = cursor;
600 while cursor < bytes.len() && bytes[cursor] != 0 {
601 cursor += 1;
602 }
603
604 dst.seek(SeekFrom::Start(base_offset + start as u64))?;
605 dst.write_all(&bytes[start..cursor])?;
606 }
607
608 Ok(())
609}
610
611#[cfg(test)]
616mod tests {
617 use super::*;
618 use std::io::{Read, Seek, SeekFrom, Write};
619 #[cfg(unix)]
620 use std::os::unix::fs::MetadataExt;
621
622 fn make_sparse(path: &Path, len: u64, data_offsets: &[u64]) -> io::Result<()> {
625 let mut f = OpenOptions::new()
626 .read(true)
627 .write(true)
628 .create(true)
629 .truncate(true)
630 .open(path)?;
631 #[cfg(windows)]
632 mark_sparse(&f)?;
633 f.set_len(len)?;
634 for &off in data_offsets {
635 let buf = vec![0xAB_u8; 64 * 1024];
636 f.seek(SeekFrom::Start(off))?;
637 f.write_all(&buf)?;
638 }
639 f.sync_all()?;
640 Ok(())
641 }
642
643 #[test]
644 fn round_trip_small() {
645 let dir = tempfile::tempdir().unwrap();
646 let src = dir.path().join("src.bin");
647 let dst = dir.path().join("dst.bin");
648
649 std::fs::write(&src, b"hello world").unwrap();
650 let n = fast_copy(&src, &dst).unwrap();
651 assert_eq!(n, 11);
652 assert_eq!(std::fs::read(&dst).unwrap(), b"hello world");
653 }
654
655 #[test]
656 fn sparse_copy_preserves_holes_and_data() {
657 let dir = tempfile::tempdir().unwrap();
661 let src = dir.path().join("src.bin");
662 let dst = dir.path().join("dst.bin");
663
664 let len: u64 = 16 * 1024 * 1024;
665 let offsets = [0u64, 4 * 1024 * 1024, 8 * 1024 * 1024, 12 * 1024 * 1024];
666 make_sparse(&src, len, &offsets).unwrap();
667
668 let n = sparse_copy(&src, &dst).unwrap();
669 assert_eq!(n, len);
670
671 let dst_meta = std::fs::metadata(&dst).unwrap();
673 assert_eq!(dst_meta.len(), len);
674
675 let mut buf = [0u8; 64 * 1024];
677 let mut dst_file = File::open(&dst).unwrap();
678 for &off in &offsets {
679 dst_file.seek(SeekFrom::Start(off)).unwrap();
680 dst_file.read_exact(&mut buf).unwrap();
681 assert!(buf.iter().all(|&b| b == 0xAB));
682 }
683
684 #[cfg(unix)]
691 {
692 let src_bytes_on_disk = std::fs::metadata(&src).unwrap().blocks() * 512;
693 let dst_bytes_on_disk = dst_meta.blocks() * 512;
694 if src_bytes_on_disk < len / 2 {
695 assert!(
699 dst_bytes_on_disk < len / 2,
700 "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}",
701 );
702 assert!(
703 dst_bytes_on_disk <= src_bytes_on_disk * 4 + 1024 * 1024,
704 "destination allocated significantly more than source: src={src_bytes_on_disk} dst={dst_bytes_on_disk}",
705 );
706 } else {
707 eprintln!(
708 "filesystem did not sparsify the source (src_bytes_on_disk={src_bytes_on_disk}, apparent={len}); sparseness preservation not exercised in this run",
709 );
710 assert!(
713 dst_bytes_on_disk <= src_bytes_on_disk + 1024 * 1024,
714 "destination grew beyond source footprint: src={src_bytes_on_disk} dst={dst_bytes_on_disk}",
715 );
716 }
717 }
718 }
719
720 #[cfg(windows)]
721 #[test]
722 fn windows_sparse_copy_uses_allocated_ranges_when_available() {
723 let dir = tempfile::tempdir().unwrap();
724 let src = dir.path().join("src.bin");
725 let dst = dir.path().join("dst.bin");
726 let len = 8 * 1024 * 1024;
727
728 make_sparse(&src, len, &[0, 4 * 1024 * 1024]).unwrap();
729 let mut src_file = File::open(&src).unwrap();
730 if ExtentMap::scan_file(&src_file).unwrap().is_none() {
731 eprintln!("filesystem cannot enumerate allocated ranges; strategy not exercised");
732 return;
733 }
734
735 let mut dst_file = OpenOptions::new()
736 .read(true)
737 .write(true)
738 .create(true)
739 .truncate(true)
740 .open(&dst)
741 .unwrap();
742 dst_file.set_len(len).unwrap();
743 mark_sparse(&dst_file).unwrap();
744
745 let strategy = copy_windows_sparse_data(&mut src_file, &mut dst_file, 1024 * 1024).unwrap();
746 assert_eq!(strategy, WindowsSparseCopyStrategy::AllocatedRanges);
747 }
748
749 #[test]
750 fn fast_copy_matches_source_size() {
751 let dir = tempfile::tempdir().unwrap();
752 let src = dir.path().join("src.bin");
753 let dst = dir.path().join("dst.bin");
754
755 let len: u64 = 4 * 1024 * 1024;
756 make_sparse(&src, len, &[0, 2 * 1024 * 1024]).unwrap();
757
758 let n = fast_copy(&src, &dst).unwrap();
759 assert_eq!(n, len);
760 assert_eq!(std::fs::metadata(&dst).unwrap().len(), len);
761 }
762
763 #[test]
764 fn missing_source_errors() {
765 let dir = tempfile::tempdir().unwrap();
766 let err = fast_copy(&dir.path().join("nope.bin"), &dir.path().join("dst.bin")).unwrap_err();
767 assert_eq!(err.kind(), io::ErrorKind::NotFound);
768 }
769}