Skip to main content

microsandbox_utils/
copy.rs

1//! Sparse-aware fast copy with reflink fallback.
2//!
3//! Two-tier strategy that preserves sparseness on every supported
4//! platform:
5//!
6//! 1. **Reflink** (zero-copy COW). Tries `clonefile(2)` on macOS and
7//!    `ioctl(FICLONE)` on Linux via `reflink-copy`. Succeeds instantly
8//!    on APFS, btrfs, XFS (with `reflink=1`), and bcachefs. Returns
9//!    `EOPNOTSUPP` (or similar) on ext4 and other non-COW filesystems.
10//!
11//! 2. **Sparse-aware copy**. Walks the source's allocation map with
12//!    POSIX `SEEK_DATA` / `SEEK_HOLE` or Windows
13//!    `FSCTL_QUERY_ALLOCATED_RANGES`, then copies only allocated
14//!    extents. The destination is extended to the source size up
15//!    front so unallocated regions stay holes.
16//!
17//! Never falls back to a naive byte-for-byte copy — that would
18//! densify a 4 GiB sparse file with a few MB of data into 4 GiB on
19//! disk, which is the exact failure mode this module exists to
20//! prevent.
21//!
22//! See `planning/microsandbox/implementation/snapshots.md` for the
23//! full design and tradeoffs.
24
25use 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//--------------------------------------------------------------------------------------------------
55// Constants
56//--------------------------------------------------------------------------------------------------
57
58/// ReFS supports 4 KiB and 64 KiB clusters. Aligning to the larger unit is valid on both.
59#[cfg(windows)]
60const WINDOWS_CLONE_ALIGNMENT: u64 = 64 * 1024;
61
62/// Windows requires each duplicate-extents request to be strictly smaller than 4 GiB.
63#[cfg(windows)]
64const WINDOWS_MAX_CLONE_CHUNK: u64 =
65    (u32::MAX as u64 / WINDOWS_CLONE_ALIGNMENT) * WINDOWS_CLONE_ALIGNMENT;
66
67//--------------------------------------------------------------------------------------------------
68// Types
69//--------------------------------------------------------------------------------------------------
70
71/// Strategy that successfully created a destination in [`fast_copy_with_strategy`].
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum FastCopyStrategy {
74    /// The destination shares source extents through filesystem copy-on-write.
75    Reflink,
76    /// The destination is an independent sparse-aware copy.
77    SparseCopy,
78}
79
80/// Windows strategy used to materialize the sparse destination's data.
81#[cfg(windows)]
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83enum WindowsSparseCopyStrategy {
84    /// Copy the filesystem-allocated ranges reported by `FSCTL_QUERY_ALLOCATED_RANGES`.
85    AllocatedRanges,
86    /// Preserve holes by finding non-zero byte runs when allocation metadata is unavailable.
87    NonzeroRuns,
88}
89
90//--------------------------------------------------------------------------------------------------
91// Functions
92//--------------------------------------------------------------------------------------------------
93
94/// Copy `src` to `dst`, preserving sparseness. Returns the apparent
95/// size of the destination in bytes.
96///
97/// Tries reflink first (zero-copy COW); on filesystems without reflink
98/// support, walks the source's allocation map and copies only its
99/// data extents into a `ftruncate`-established sparse destination.
100///
101/// **Blocking.** Callers in async contexts should wrap in
102/// `tokio::task::spawn_blocking`.
103pub fn fast_copy(src: &Path, dst: &Path) -> io::Result<u64> {
104    fast_copy_with_strategy(src, dst).map(|(len, _)| len)
105}
106
107/// Copy using the fastest safe strategy and report which strategy resolved.
108pub fn fast_copy_with_strategy(src: &Path, dst: &Path) -> io::Result<(u64, FastCopyStrategy)> {
109    fast_copy_impl(src, dst, true)
110}
111
112/// Copy an ephemeral backing without flushing the destination to stable storage.
113///
114/// Completed writes are visible to other processes, but are not crash-durable. Use only
115/// for reconstructible/local handoffs whose contract does not require persistence across
116/// host failure. This retains sparse-copy and reflink behavior and independent contents.
117/// Existing snapshot callers must continue using [`fast_copy_with_strategy`].
118pub 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    // Stat the source up front. This makes the missing-source error
128    // kind platform-consistent (`NotFound` everywhere); without it,
129    // reflink-copy on Linux surfaces `InvalidInput` with no errno
130    // for a non-existent path, which our `is_reflink_unsupported`
131    // check can't recognize as a fall-through.
132    let src_len = std::fs::metadata(src)?.len();
133
134    // Tier 1: reflink. Errors on unsupported FSes; we fall through to
135    // Tier 2. We do NOT use `reflink_or_copy`, which densifies on
136    // fallback via `std::fs::copy`.
137    match reflink_impl(src, dst) {
138        Ok(()) => return Ok((src_len, FastCopyStrategy::Reflink)),
139        Err(e) if is_reflink_unsupported(&e) => {
140            // fall through to sparse copy
141        }
142        Err(e) => return Err(e),
143    }
144
145    sparse_copy_impl(src, dst, sync_destination).map(|len| (len, FastCopyStrategy::SparseCopy))
146}
147
148/// Require a filesystem copy-on-write clone with no fallback.
149pub 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
155/// Sparse-aware copy via platform allocation metadata and per-extent copy.
156///
157/// Public for callers that want to skip the reflink attempt — e.g.
158/// when they already know the destination filesystem doesn't support
159/// reflinks, or for tests that want to exercise the fallback path.
160pub 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/// Copy an ephemeral Linux memory generation between already-owned files, preserving holes.
178/// The destination must be a distinct writable object. No durability flush is performed.
179#[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    // Establish destination as a fully-sparse hole of `len` bytes;
195    // only data extents will materialize into allocated blocks below.
196    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        // Find next data extent.
204        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            // ENXIO: no more data past this offset → done.
208            if err.raw_os_error() == Some(libc::ENXIO) {
209                break;
210            }
211            return Err(err);
212        }
213        // Find the end of that extent (start of next hole, or EOF).
214        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            // Local handoffs require independent contents, not physically independent blocks.
227            // Keep the explicit/durable copy backend unchanged, but avoid a userspace bounce
228            // buffer when the kernel can transfer an ephemeral generation directly.
229            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/// Use the platform's native copy-on-write file-clone primitive.
245#[cfg(unix)]
246fn reflink_impl(src: &Path, dst: &Path) -> io::Result<()> {
247    reflink_copy::reflink(src, dst)
248}
249
250/// Clone a file on a Windows volume that explicitly supports block refcounting.
251#[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//--------------------------------------------------------------------------------------------------
302// Functions: Helpers
303//--------------------------------------------------------------------------------------------------
304
305#[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        // NTFS can enumerate the ranges that actually occupy filesystem blocks. Copy those ranges
500        // wholesale: inspecting zero/non-zero byte runs inside an allocated extent turns raw disk
501        // images into millions of tiny seeks and writes.
502        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        // Filesystems without FSCTL_QUERY_ALLOCATED_RANGES cannot expose their allocation map.
509        // Preserve sparseness there with the slower byte-run fallback instead of densifying the
510        // destination with a naive full-file copy.
511        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
533/// Reflink can fail with several different errnos depending on the
534/// filesystem and platform. Treat them all as "fall through to Tier 2"
535/// rather than propagating to the caller.
536///
537/// On Linux `ENOTSUP == EOPNOTSUPP`, so a single arm covers both;
538/// macOS / BSDs assign them distinct values and need both arms.
539fn 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,   // ERROR_INVALID_FUNCTION
555        17,  // ERROR_NOT_SAME_DEVICE
556        50,  // ERROR_NOT_SUPPORTED
557        87,  // ERROR_INVALID_PARAMETER
558        124, // ERROR_INVALID_LEVEL
559        775, // ERROR_NOT_CAPABLE
560    ];
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    // Explicit copy must never ask the filesystem to satisfy the transfer with shared COW extents.
575    read_write_extent(src_fd, dst_fd, off, len)
576}
577
578/// Preserve holes by transferring only the caller's allocated extent. A kernel copy may
579/// internally clone blocks; that is safe for immutable local generations and private children.
580#[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            // A transfer can succeed partially before discovering an unsupported extent.
616            // Continue at the exact next byte, never restart or densify the whole file.
617            return read_write_extent(src_fd, dst_fd, off + copied, len - copied);
618        }
619        return Err(error);
620    }
621    Ok(())
622}
623
624/// Copy `len` bytes from `src_fd` at `off` to `dst_fd` at `off` with
625/// `pread`/`pwrite`.
626///
627/// This is the explicit-copy backend for `copy_extent`; avoiding clone and
628/// `copy_file_range` operations prevents the destination from sharing COW
629/// extents with the source.
630#[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//--------------------------------------------------------------------------------------------------
709// Tests
710//--------------------------------------------------------------------------------------------------
711
712#[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    /// Build a sparse source file: total apparent size `len`, with
720    /// 64 KiB of data written at each of the given offsets.
721    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        // 16 MiB sparse file with 4 data extents at known offsets.
755        // Use sparse_copy directly to exercise Tier 2 regardless of
756        // the test-host filesystem.
757        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        // Apparent size matches.
769        let dst_meta = std::fs::metadata(&dst).unwrap();
770        assert_eq!(dst_meta.len(), len);
771
772        // Each data extent's bytes round-trip.
773        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        // Sparseness preservation: only meaningful if the source
782        // itself is sparse on this filesystem. Some test hosts (FAT,
783        // certain APFS configurations under tempfile mounts) don't
784        // produce a sparse source from `ftruncate + pwrite` — in that
785        // case sparseness is unachievable and we just confirm the
786        // destination didn't blow up beyond the source's footprint.
787        #[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                // Source IS sparse. Destination must also be sparse —
793                // this is the load-bearing regression test for the whole
794                // module.
795                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                // Without source sparseness we can't exceed source's
808                // footprint by much — guard against gross regressions.
809                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        // Force the fallback even on a reflink-capable test host. Omitting a flush
826        // must not omit bytes, fill holes, or alias writable contents with the source.
827        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}