Skip to main content

fxcp_core/operations/
capabilities.rs

1// SPDX-License-Identifier: GPL-2.0-or-later
2// Copyright (C) 2025 Joel Wirāmu Pauling <aenertia@aenertia.net>
3//
4//! `probe_capabilities()`, copy tier selection, `CopyCapabilities`.
5
6use std::path::{Path, PathBuf};
7use std::os::unix::io::{AsRawFd, RawFd};
8use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
9use std::sync::OnceLock;
10use std::ffi::CString;
11use uuid::Uuid;
12use libc;
13use nix::sys::statfs;
14use tracing::debug;
15
16use std::os::unix::fs::MetadataExt;
17use std::sync::Arc;
18use dashmap::DashMap;
19use lazy_static::lazy_static;
20
21use super::container::{ContainerInfo, DmStackInfo, detect_container, probe_dm_stack};
22
23pub(crate) const NFS_SUPER_MAGIC: u64 = 0x6969;
24pub(crate) const BTRFS_SUPER_MAGIC: u64 = 0x9123683E;
25pub(crate) const TMPFS_SUPER_MAGIC: u64 = 0x01021994;
26pub(crate) const RAMFS_SUPER_MAGIC: u64 = 0x09041934;
27pub(crate) const HUGETLBFS_MAGIC: u64 = 0x958458f6_u64;
28pub(crate) const OVERLAYFS_MAGIC: u64 = 0x794c7630;
29pub(crate) const FS_IOC_GETFLAGS: u64 = 0x80086601;
30pub(crate) const FS_IOC_SETFLAGS: u64 = 0x40086602;
31#[allow(dead_code)]
32pub(crate) const FS_IMMUTABLE_FL: u32 = 0x00000010;
33#[allow(dead_code)]
34pub(crate) const FS_APPEND_FL: u32 = 0x00000020;
35pub(crate) const FS_NOCOW_FL: u32 = 0x00800000;
36const F2FS_SUPER_MAGIC: u64 = 0xF2F52010;
37const FICLONE: u64 = crate::constants::FICLONE_IOCTL;
38const STATX_WRITE_ATOMIC: u32 = 0x00010000;
39const STATX_DIOALIGN: u32 = 0x00002000;
40const BTRFS_IOC_FS_INFO: u64 = 0x8400941F;
41
42lazy_static! {
43    static ref GLOBAL_CAPS_CACHE: DashMap<PathBuf, Arc<Capabilities>> = DashMap::new();
44}
45
46#[repr(C)]
47struct StatxAtomic {
48    stx_mask: u32,
49    stx_blksize: u32,
50    stx_attributes: u64,
51    stx_nlink: u32,
52    stx_uid: u32,
53    stx_gid: u32,
54    stx_mode: u16,
55    __spare0: [u16; 1],
56    stx_ino: u64,
57    stx_size: u64,
58    stx_blocks: u64,
59    stx_attributes_mask: u64,
60    stx_atime: libc::statx_timestamp,
61    stx_btime: libc::statx_timestamp,
62    stx_ctime: libc::statx_timestamp,
63    stx_mtime: libc::statx_timestamp,
64    stx_rdev_major: u32,
65    stx_rdev_minor: u32,
66    stx_dev_major: u32,
67    stx_dev_minor: u32,
68    stx_mnt_id: u64,
69    stx_dio_mem_align: u32,
70    stx_dio_offset_align: u32,
71    stx_atomic_write_unit_min: u32,
72    stx_atomic_write_unit_max: u32,
73    stx_atomic_write_segments_max: u32,
74    __spare1: [u64; 9],
75}
76
77#[repr(C)]
78struct BtrfsIoctlFsInfoArgs {
79    max_id: u64,
80    num_devices: u64,
81    fsid: [u8; 16],
82    nodesize: u32,
83    sectorsize: u32,
84    clone_alignment: u32,
85    reserved32: u32,
86    reserved: [u64; 122],
87}
88
89/// Filesystem and hardware capabilities detected by [`probe_capabilities`]
90pub struct Capabilities {
91    /// Hardware supports RWF_ATOMIC writes
92    pub atomic_writes: AtomicBool,
93    /// Minimum atomic write unit in bytes
94    pub atomic_min_bytes: AtomicU32,
95    /// Maximum atomic write unit in bytes
96    pub atomic_max_bytes: AtomicU32,
97    /// Kernel supports RWF_UNCACHED (bypass page cache)
98    pub uncached_io: AtomicBool,
99    /// Filesystem supports RENAME_EXCHANGE or atomic exchange ioctl
100    pub exchange_range: AtomicBool,
101    /// Filesystem supports FICLONE (CoW reflink)
102    pub reflink: AtomicBool,
103    /// Filesystem supports SEEK_HOLE/SEEK_DATA for sparse file mapping
104    pub seek_hole: AtomicBool,
105    /// Btrfs subvolume operations available
106    pub btrfs_subvol: AtomicBool,
107    /// Btrfs qgroups (quota groups) detected
108    pub btrfs_quotas: AtomicBool,
109    /// F2FS legacy atomic write ioctl available
110    pub f2fs_atomic_legacy: AtomicBool,
111    /// Filesystem is NFS (magic 0x6969)
112    pub is_nfs: AtomicBool,
113    /// STATX_DIOALIGN: memory alignment required for O_DIRECT
114    pub dio_mem_align: AtomicU32,
115    /// STATX_DIOALIGN: file offset alignment required for O_DIRECT
116    pub dio_offset_align: AtomicU32,
117    /// Device-mapper stack layers beneath this filesystem
118    pub dm_stack: Option<DmStackInfo>,
119    /// Container runtime environment, if any
120    pub container: Option<ContainerInfo>,
121}
122
123impl Default for Capabilities {
124    fn default() -> Self {
125        Self {
126            atomic_writes: AtomicBool::new(false),
127            atomic_min_bytes: AtomicU32::new(0),
128            atomic_max_bytes: AtomicU32::new(0),
129            uncached_io: AtomicBool::new(false),
130            exchange_range: AtomicBool::new(true),
131            reflink: AtomicBool::new(false),
132            seek_hole: AtomicBool::new(false),
133            btrfs_subvol: AtomicBool::new(false),
134            btrfs_quotas: AtomicBool::new(false),
135            f2fs_atomic_legacy: AtomicBool::new(false),
136            is_nfs: AtomicBool::new(false),
137            dio_mem_align: AtomicU32::new(0),
138            dio_offset_align: AtomicU32::new(0),
139            dm_stack: None,
140            container: None,
141        }
142    }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub(crate) enum CopyStrategy {
147    Reflink,
148    StandardCopy,
149}
150
151pub(crate) fn probe_reflink_support(target_root: &Path) -> bool {
152    let uuid = Uuid::new_v4();
153    let src_path = target_root.join(format!(".foxing_probe_src_{}", uuid));
154    let dst_path = target_root.join(format!(".foxing_probe_dst_{}", uuid));
155    
156    let _guard = super::CleanupGuard::new(vec![src_path.clone(), dst_path.clone()]);
157    
158    let mut src_file = match std::fs::File::create(&src_path) {
159        Ok(f) => f,
160        Err(_) => return false,
161    };
162    use std::io::Write;
163    let buf = [0xAAu8; 4096];
164    if src_file.write_all(&buf).is_err() { return false; }
165    if src_file.sync_all().is_err() { return false; }
166    
167    let dst_file = match std::fs::File::create(&dst_path) {
168        Ok(f) => f,
169        Err(_) => return false,
170    };
171    
172    let src_fd = src_file.as_raw_fd();
173    let dst_fd = dst_file.as_raw_fd();
174    
175    // SAFETY: dst_fd and src_fd are valid open file descriptors from File::as_raw_fd().
176    let ret = unsafe { libc::ioctl(dst_fd, FICLONE, src_fd) };
177    
178    let supported = if ret == 0 {
179        if let Ok(meta) = dst_file.metadata() {
180            meta.len() == 4096
181        } else {
182            false
183        }
184    } else {
185        false
186    };
187    
188    drop(src_file);
189    drop(dst_file);
190    
191    supported
192}
193
194fn has_nocow_flag(path: &Path) -> bool {
195    if let Ok(f) = std::fs::File::open(path) {
196        let fd = f.as_raw_fd();
197        let mut flags: u32 = 0;
198        // SAFETY: fd is a valid open file descriptor, flags is a valid u32 pointer.
199        let ret = unsafe { libc::ioctl(fd, FS_IOC_GETFLAGS, &mut flags) };
200        if ret == 0 {
201            return (flags & FS_NOCOW_FL) != 0;
202        }
203    }
204    false
205}
206
207pub(crate) fn determine_copy_strategy(
208    src_path: &Path,
209    dst_path: &Path,
210    src_meta: &std::fs::Metadata,
211    caps: &Capabilities
212) -> CopyStrategy {
213    if !caps.reflink.load(Ordering::Relaxed) {
214        return CopyStrategy::StandardCopy;
215    }
216    
217    let dst_dev = if let Ok(m) = std::fs::metadata(dst_path) {
218        m.dev()
219    } else if let Some(parent) = dst_path.parent() {
220        if let Ok(m) = std::fs::metadata(parent) {
221            m.dev()
222        } else {
223            return CopyStrategy::StandardCopy;
224        }
225    } else {
226        return CopyStrategy::StandardCopy;
227    };
228
229    if src_meta.dev() != dst_dev {
230        return CopyStrategy::StandardCopy;
231    }
232
233    if has_nocow_flag(src_path) {
234        debug!("Strategy: Skipping reflink for NOCOW file: {:?}", src_path);
235        return CopyStrategy::StandardCopy;
236    }
237
238    if src_meta.len() < 4096 {
239        return CopyStrategy::StandardCopy;
240    }
241
242    CopyStrategy::Reflink
243}
244
245/// Detect filesystem and hardware capabilities for a path (cached per mount point)
246pub fn probe_capabilities(path: &Path) -> Arc<Capabilities> {
247    let cache_key = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
248    if let Some(cached) = GLOBAL_CAPS_CACHE.get(&cache_key) {
249        return cached.clone();
250    }
251
252    debug!("probe_capabilities: START {:?}", path);
253    let mut caps_inner = Capabilities::default();
254
255    if let Ok(c_path) = CString::new(path.to_string_lossy().as_bytes()) {
256        // SAFETY: StatxAtomic is repr(C) with no validity invariants beyond
257        // zero-initialization. All-zeros is a valid initial state.
258        let mut stx: StatxAtomic = unsafe { std::mem::zeroed() };
259        // SAFETY: c_path is a valid null-terminated C string. stx is a valid
260        // mutable pointer to a zeroed StatxAtomic struct matching the kernel layout.
261        let ret = unsafe {
262            libc::syscall(
263                libc::SYS_statx,
264                libc::AT_FDCWD,
265                c_path.as_ptr(),
266                libc::AT_EMPTY_PATH | libc::AT_NO_AUTOMOUNT,
267                STATX_WRITE_ATOMIC | STATX_DIOALIGN,
268                &mut stx as *mut _
269            )
270        };
271
272        if ret == 0 {
273            if (stx.stx_mask & STATX_WRITE_ATOMIC) != 0 && stx.stx_atomic_write_unit_max > 0 {
274                caps_inner.atomic_min_bytes.store(stx.stx_atomic_write_unit_min, Ordering::Relaxed);
275                caps_inner.atomic_max_bytes.store(stx.stx_atomic_write_unit_max, Ordering::Relaxed);
276                caps_inner.atomic_writes.store(true, Ordering::Relaxed);
277                debug!("probe_capabilities: Atomic writes detected");
278            }
279            if (stx.stx_mask & STATX_DIOALIGN) != 0 {
280                let mut mem_align = stx.stx_dio_mem_align;
281                let mut off_align = stx.stx_dio_offset_align;
282                // KVDO OVERRIDE: clamp to 4096 if kvdo detected in dm stack
283                if let Some(ref dm) = caps_inner.dm_stack
284                    && dm.has_vdo {
285                        mem_align = 4096;
286                        off_align = 4096;
287                        debug!("probe_capabilities: kvdo detected, clamping DIO alignment to 4096");
288                    }
289                caps_inner.dio_mem_align.store(mem_align, Ordering::Relaxed);
290                caps_inner.dio_offset_align.store(off_align, Ordering::Relaxed);
291                debug!("probe_capabilities: DIO alignment: mem={} offset={}", mem_align, off_align);
292            }
293        }
294    }
295
296    debug!("probe_capabilities: checking uncached_io");
297    caps_inner.uncached_io.store(crate::security::probe_rwf_uncached(path), Ordering::Relaxed);
298
299    debug!("probe_capabilities: checking statfs magic");
300    if let Ok(s) = statfs::statfs(path) {
301        let magic = s.filesystem_type().0 as u64;
302        if magic == NFS_SUPER_MAGIC {
303            caps_inner.is_nfs.store(true, Ordering::Relaxed);
304            debug!("probe_capabilities: NFS detected");
305        }
306        if magic == F2FS_SUPER_MAGIC {
307            caps_inner.f2fs_atomic_legacy.store(true, Ordering::Relaxed);
308            if !caps_inner.atomic_writes.load(Ordering::Relaxed) {
309                caps_inner.atomic_writes.store(true, Ordering::Relaxed);
310                caps_inner.atomic_min_bytes.store(4096, Ordering::Relaxed);
311                caps_inner.atomic_max_bytes.store(u32::MAX, Ordering::Relaxed);
312                debug!("Probe: F2FS Detected. Enabling Legacy Atomic Writes (IOCTL).");
313            }
314        }
315        if magic == TMPFS_SUPER_MAGIC || magic == RAMFS_SUPER_MAGIC || magic == HUGETLBFS_MAGIC {
316            // tmpfs/ramfs: no reflinks, no O_DIRECT, no registered io_uring buffers
317            debug!("Probe: tmpfs/ramfs/hugetlbfs detected  --  disabling O_DIRECT and reflink");
318        }
319        if magic == OVERLAYFS_MAGIC {
320            debug!("Probe: overlayfs detected");
321        }
322    }
323
324    debug!("probe_capabilities: checking file IOCTLs");
325    if let Ok(f) = std::fs::File::open(path) {
326        let fd = f.as_raw_fd();
327        // SAFETY: fd is a valid open file descriptor.
328        if unsafe { libc::lseek(fd, 0, libc::SEEK_DATA) } >= 0 {
329             caps_inner.seek_hole.store(true, Ordering::Relaxed);
330        }
331        if probe_btrfs_quotas(fd) {
332            caps_inner.btrfs_quotas.store(true, Ordering::Relaxed);
333            debug!("Probe: Btrfs Qgroups detected on {:?}", path);
334        }
335    }
336
337    debug!("probe_capabilities: checking reflink");
338    if probe_reflink_support(path) {
339        caps_inner.reflink.store(true, Ordering::Relaxed);
340        debug!("probe_capabilities: Reflink supported");
341    }
342
343    debug!("probe_capabilities: checking dm-stack");
344    caps_inner.dm_stack = probe_dm_stack(path);
345    caps_inner.container = Some(detect_container());
346    if let Some(ref dm) = caps_inner.dm_stack {
347        debug!("probe_capabilities: dm-stack depth={} crypt={} integrity={} base={:?}",
348               dm.stack_depth, dm.has_crypt, dm.has_integrity, dm.base_device);
349    }
350
351    let caps = Arc::new(caps_inner);
352    debug!("probe_capabilities: END {:?}", path);
353    GLOBAL_CAPS_CACHE.insert(cache_key, caps.clone());
354    caps
355}
356
357
358fn probe_btrfs_quotas(fd: RawFd) -> bool {
359    // SAFETY: BtrfsIoctlFsInfoArgs is repr(C). All-zeros is a valid initial state.
360    let mut info_args: BtrfsIoctlFsInfoArgs = unsafe { std::mem::zeroed() };
361    // SAFETY: fd is a valid open file descriptor, info_args is properly initialized.
362    if unsafe { libc::ioctl(fd, BTRFS_IOC_FS_INFO, &mut info_args) } < 0 {
363        return false;
364    }
365    
366    let uuid_bytes = info_args.fsid;
367    let uuid_str = format!(
368        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
369        uuid_bytes[0], uuid_bytes[1], uuid_bytes[2], uuid_bytes[3],
370        uuid_bytes[4], uuid_bytes[5], uuid_bytes[6], uuid_bytes[7],
371        uuid_bytes[8], uuid_bytes[9], uuid_bytes[10], uuid_bytes[11],
372        uuid_bytes[12], uuid_bytes[13], uuid_bytes[14], uuid_bytes[15]
373    );
374    
375    let sysfs_path = PathBuf::from(format!("/sys/fs/btrfs/{}/qgroups", uuid_str));
376    if sysfs_path.exists() {
377        return true;
378    }
379    
380    let sysfs_path_old = PathBuf::from(format!("/sys/fs/btrfs/{}/quota_override", uuid_str));
381    if sysfs_path_old.exists() {
382        return true;
383    }
384    
385    false
386}
387
388// -- cachestat(2) page cache residency probe (Linux >= 6.5, x86_64 nr 451) --
389
390static CACHESTAT_AVAILABLE: OnceLock<bool> = OnceLock::new();
391
392/// Probe whether `cachestat(2)` is available on this kernel.
393/// Result is cached via `OnceLock`  --  probes once per process.
394pub fn probe_cachestat() -> bool {
395    *CACHESTAT_AVAILABLE.get_or_init(|| {
396        // fd=-1 probe: EBADF means syscall exists, ENOSYS means unavailable.
397        let dummy_range = [0u64; 2]; // off=0, len=0
398        let dummy_cs = [0u64; 5];
399        // SAFETY: Probing with invalid fd (-1). dummy_range and dummy_cs are
400        // valid stack pointers matching the cachestat(2) kernel ABI.
401        let ret = unsafe {
402            libc::syscall(451, -1i64, dummy_range.as_ptr(), dummy_cs.as_ptr(), 0u32)
403        };
404        ret != -1 || unsafe { *libc::__errno_location() } != libc::ENOSYS
405    })
406}
407
408/// Query page cache residency for an open file via `cachestat(2)`.
409/// Returns fraction of pages in cache (0.0-1.0), or `None` if unavailable.
410pub fn query_cache_residency(fd: RawFd, file_size: u64) -> Option<f64> {
411    if !probe_cachestat() { return None; }
412
413    #[repr(C)]
414    struct CachestatRange { off: u64, len: u64 }
415    #[repr(C)]
416    struct Cachestat { nr_cache: u64, nr_dirty: u64, nr_writeback: u64, nr_evicted: u64, nr_recently_evicted: u64 }
417
418    let range = CachestatRange { off: 0, len: 0 }; // whole file
419    let mut cs = Cachestat {
420        nr_cache: 0, nr_dirty: 0, nr_writeback: 0,
421        nr_evicted: 0, nr_recently_evicted: 0,
422    };
423    // SAFETY: fd is a valid open file descriptor. range and cs are repr(C)
424    // structs with layout matching the kernel cachestat(2) interface.
425    let ret = unsafe {
426        libc::syscall(451, fd as i64, &range as *const _, &mut cs as *mut _, 0u32)
427    };
428    if ret != 0 { return None; }
429
430    let total_pages = file_size.div_ceil(4096);
431    if total_pages == 0 { return None; }
432    Some(cs.nr_cache as f64 / total_pages as f64)
433}
434
435#[cfg(test)]
436mod tests {
437    #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
438    use super::*;
439    use std::sync::atomic::Ordering;
440
441    fn caps_with_reflink(reflink: bool) -> Capabilities {
442        let caps = Capabilities::default();
443        caps.reflink.store(reflink, Ordering::Relaxed);
444        caps
445    }
446
447    #[test]
448    fn test_strategy_no_reflink_returns_standard() {
449        let dir = tempfile::tempdir().unwrap();
450        let src = dir.path().join("src.bin");
451        let dst = dir.path().join("dst.bin");
452        std::fs::write(&src, vec![0u8; 8192]).unwrap();
453        std::fs::write(&dst, b"").unwrap();
454
455        let meta = std::fs::metadata(&src).unwrap();
456        let caps = caps_with_reflink(false);
457        assert_eq!(determine_copy_strategy(&src, &dst, &meta, &caps), CopyStrategy::StandardCopy);
458    }
459
460    #[test]
461    fn test_strategy_reflink_same_device_large_file() {
462        let dir = tempfile::tempdir().unwrap();
463        let src = dir.path().join("src.bin");
464        let dst = dir.path().join("dst.bin");
465        std::fs::write(&src, vec![0u8; 8192]).unwrap();
466        std::fs::write(&dst, b"").unwrap();
467
468        let meta = std::fs::metadata(&src).unwrap();
469        let caps = caps_with_reflink(true);
470        assert_eq!(determine_copy_strategy(&src, &dst, &meta, &caps), CopyStrategy::Reflink);
471    }
472
473    #[test]
474    fn test_strategy_reflink_small_file_returns_standard() {
475        let dir = tempfile::tempdir().unwrap();
476        let src = dir.path().join("src_small.bin");
477        let dst = dir.path().join("dst_small.bin");
478        std::fs::write(&src, vec![0u8; 100]).unwrap();
479        std::fs::write(&dst, b"").unwrap();
480
481        let meta = std::fs::metadata(&src).unwrap();
482        let caps = caps_with_reflink(true);
483        assert_eq!(determine_copy_strategy(&src, &dst, &meta, &caps), CopyStrategy::StandardCopy);
484    }
485
486    #[test]
487    fn test_strategy_exactly_4096_returns_reflink() {
488        let dir = tempfile::tempdir().unwrap();
489        let src = dir.path().join("src_4k.bin");
490        let dst = dir.path().join("dst_4k.bin");
491        std::fs::write(&src, vec![0u8; 4096]).unwrap();
492        std::fs::write(&dst, b"").unwrap();
493
494        let meta = std::fs::metadata(&src).unwrap();
495        let caps = caps_with_reflink(true);
496        assert_eq!(determine_copy_strategy(&src, &dst, &meta, &caps), CopyStrategy::Reflink);
497    }
498
499    #[test]
500    fn test_strategy_dst_not_exist_uses_parent_device() {
501        let dir = tempfile::tempdir().unwrap();
502        let src = dir.path().join("src.bin");
503        let dst = dir.path().join("nonexistent.bin");
504        std::fs::write(&src, vec![0u8; 8192]).unwrap();
505
506        let meta = std::fs::metadata(&src).unwrap();
507        let caps = caps_with_reflink(true);
508        assert_eq!(determine_copy_strategy(&src, &dst, &meta, &caps), CopyStrategy::Reflink);
509    }
510
511    #[test]
512    fn test_strategy_dst_no_parent_returns_standard() {
513        let src_dir = tempfile::tempdir().unwrap();
514        let src = src_dir.path().join("src.bin");
515        std::fs::write(&src, vec![0u8; 8192]).unwrap();
516
517        let meta = std::fs::metadata(&src).unwrap();
518        let caps = caps_with_reflink(true);
519        let dst = Path::new("/nonexistent_mount_abc123/sub/file.bin");
520        assert_eq!(determine_copy_strategy(&src, dst, &meta, &caps), CopyStrategy::StandardCopy);
521    }
522
523    #[test]
524    fn test_capabilities_default() {
525        let caps = Capabilities::default();
526        assert!(!caps.reflink.load(Ordering::Relaxed));
527        assert!(!caps.atomic_writes.load(Ordering::Relaxed));
528        assert!(!caps.is_nfs.load(Ordering::Relaxed));
529        assert!(!caps.seek_hole.load(Ordering::Relaxed));
530        assert!(caps.exchange_range.load(Ordering::Relaxed));
531        assert_eq!(caps.atomic_min_bytes.load(Ordering::Relaxed), 0);
532        assert_eq!(caps.atomic_max_bytes.load(Ordering::Relaxed), 0);
533        assert!(caps.dm_stack.is_none());
534        assert!(caps.container.is_none());
535    }
536
537    #[test]
538    fn test_copy_strategy_debug_and_eq() {
539        assert_eq!(CopyStrategy::Reflink, CopyStrategy::Reflink);
540        assert_eq!(CopyStrategy::StandardCopy, CopyStrategy::StandardCopy);
541        assert_ne!(CopyStrategy::Reflink, CopyStrategy::StandardCopy);
542        let _ = format!("{:?}", CopyStrategy::Reflink);
543    }
544
545    #[test]
546    fn test_probe_cachestat_does_not_panic() {
547        let available = probe_cachestat();
548        // On kernel >= 6.5 this is true; on older kernels false.
549        // OnceLock caching: second call must return the same value.
550        assert_eq!(available, probe_cachestat());
551    }
552
553    #[test]
554    fn test_query_cache_residency_on_real_file() {
555        if !probe_cachestat() { return; }
556        let dir = tempfile::tempdir().unwrap();
557        let path = dir.path().join("cached_test.bin");
558        std::fs::write(&path, vec![0xABu8; 65536]).unwrap();
559        // Just-written file should be in page cache
560        let f = std::fs::File::open(&path).unwrap();
561        let residency = query_cache_residency(f.as_raw_fd(), 65536);
562        assert!(residency.is_some());
563        let r = residency.unwrap();
564        assert!((0.0..=1.0).contains(&r));
565        assert!(r > 0.5, "expected >50% cache residency for just-written file, got {:.0}%", r * 100.0);
566    }
567
568    #[test]
569    fn test_query_cache_residency_invalid_fd() {
570        if !probe_cachestat() { return; }
571        // Invalid fd should return None (syscall fails with EBADF)
572        assert!(query_cache_residency(-1, 4096).is_none());
573    }
574
575    #[test]
576    fn test_query_cache_residency_zero_size() {
577        if !probe_cachestat() { return; }
578        let dir = tempfile::tempdir().unwrap();
579        let path = dir.path().join("empty.bin");
580        std::fs::write(&path, b"").unwrap();
581        let f = std::fs::File::open(&path).unwrap();
582        // Zero-size file -> total_pages=0 -> None
583        assert!(query_cache_residency(f.as_raw_fd(), 0).is_none());
584    }
585
586    #[test]
587    fn test_strategy_cross_device_returns_standard() {
588        let dir = tempfile::tempdir().unwrap();
589        let src = dir.path().join("src.bin");
590        let dst = dir.path().join("dst.bin");
591        std::fs::write(&src, vec![0u8; 8192]).unwrap();
592        std::fs::write(&dst, vec![0u8; 8192]).unwrap();
593
594        let meta = std::fs::metadata(&src).unwrap();
595        let caps = caps_with_reflink(true);
596        let strategy = determine_copy_strategy(&src, &dst, &meta, &caps);
597        assert_eq!(strategy, CopyStrategy::Reflink,
598            "same-device large file with reflink caps should return Reflink");
599    }
600
601    #[test]
602    fn test_strategy_file_below_4096_returns_standard() {
603        let dir = tempfile::tempdir().unwrap();
604        let src = dir.path().join("tiny.bin");
605        let dst = dir.path().join("dst.bin");
606        std::fs::write(&src, vec![0u8; 4095]).unwrap();
607        std::fs::write(&dst, vec![0u8; 4095]).unwrap();
608
609        let meta = std::fs::metadata(&src).unwrap();
610        let caps = caps_with_reflink(true);
611        let strategy = determine_copy_strategy(&src, &dst, &meta, &caps);
612        assert_eq!(strategy, CopyStrategy::StandardCopy,
613            "files < 4096 bytes must use StandardCopy (FICLONE overhead exceeds benefit)");
614    }
615}