Skip to main content

lean_ctx/core/
cloud_files.rs

1//! Detection of cloud-backed placeholder files (OneDrive "Files On-Demand" on
2//! Windows, iCloud Drive "dataless" files on macOS).
3//!
4//! Such files keep their *contents* in the cloud; merely opening or reading one
5//! forces the OS to download ("hydrate") it — slow, bandwidth-/quota-hungry, and
6//! on Windows it triggers OneDrive sync warnings (#363). lean-ctx must never
7//! hydrate files just to index them in the background, so every directory scan
8//! prunes placeholders via [`keep_entry`]. Detection is metadata-only (file
9//! attributes / stat flags) and therefore never triggers a download.
10
11use std::path::Path;
12
13/// True if `path` is a cloud placeholder whose content is not stored locally and
14/// would be downloaded on read. Metadata-only — never hydrates the file.
15///
16/// On platforms without an on-demand cloud-file convention this is always
17/// `false`.
18#[cfg(windows)]
19pub fn is_cloud_placeholder(path: &Path) -> bool {
20    use std::os::windows::fs::MetadataExt;
21    // `symlink_metadata` maps to GetFileAttributes, which reports the placeholder
22    // attributes without recalling (downloading) the content.
23    std::fs::symlink_metadata(path)
24        .map(|m| attrs_indicate_placeholder(m.file_attributes()))
25        .unwrap_or(false)
26}
27
28/// macOS variant: evicted iCloud Drive files carry `SF_DATALESS` in `st_flags`.
29/// `lstat` reads the flag without materialising the content.
30#[cfg(target_os = "macos")]
31pub fn is_cloud_placeholder(path: &Path) -> bool {
32    use std::os::unix::ffi::OsStrExt;
33    const SF_DATALESS: u32 = 0x4000_0000;
34    let Ok(cpath) = std::ffi::CString::new(path.as_os_str().as_bytes()) else {
35        return false;
36    };
37    // SAFETY: `cpath` is a valid NUL-terminated string for the call's duration;
38    // `st` is zero-initialised and only read after a successful `lstat`.
39    unsafe {
40        let mut st: libc::stat = std::mem::zeroed();
41        if libc::lstat(cpath.as_ptr(), &raw mut st) != 0 {
42            return false;
43        }
44        st.st_flags & SF_DATALESS != 0
45    }
46}
47
48#[cfg(not(any(windows, target_os = "macos")))]
49pub fn is_cloud_placeholder(_path: &Path) -> bool {
50    false
51}
52
53/// Predicate for `ignore::WalkBuilder::filter_entry`: prune cloud placeholders so
54/// a scan never descends into — or reads — an un-hydrated file or directory.
55pub fn keep_entry(entry: &ignore::DirEntry) -> bool {
56    !is_cloud_placeholder(entry.path())
57}
58
59/// Pure check for the Windows placeholder attribute bits, extracted so it can be
60/// unit-tested on every platform: `FILE_ATTRIBUTE_OFFLINE`,
61/// `FILE_ATTRIBUTE_RECALL_ON_OPEN`, `FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS`.
62#[cfg(any(windows, test))]
63pub(crate) fn attrs_indicate_placeholder(attrs: u32) -> bool {
64    const OFFLINE: u32 = 0x0000_1000;
65    const RECALL_ON_OPEN: u32 = 0x0004_0000;
66    const RECALL_ON_DATA_ACCESS: u32 = 0x0040_0000;
67    attrs & (OFFLINE | RECALL_ON_OPEN | RECALL_ON_DATA_ACCESS) != 0
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn placeholder_attribute_bits_are_detected() {
76        assert!(attrs_indicate_placeholder(0x0000_1000)); // OFFLINE
77        assert!(attrs_indicate_placeholder(0x0004_0000)); // RECALL_ON_OPEN
78        assert!(attrs_indicate_placeholder(0x0040_0000)); // RECALL_ON_DATA_ACCESS
79        assert!(attrs_indicate_placeholder(0x0000_1020)); // OFFLINE + ARCHIVE
80    }
81
82    #[test]
83    fn normal_attribute_bits_are_not_placeholders() {
84        assert!(!attrs_indicate_placeholder(0x0000_0020)); // ARCHIVE
85        assert!(!attrs_indicate_placeholder(0x0000_0010)); // DIRECTORY
86        assert!(!attrs_indicate_placeholder(0x0000_0080)); // NORMAL
87        assert!(!attrs_indicate_placeholder(0));
88    }
89
90    #[test]
91    fn regular_local_file_is_not_a_placeholder() {
92        let dir = tempfile::tempdir().unwrap();
93        let f = dir.path().join("local.txt");
94        std::fs::write(&f, "hello").unwrap();
95        assert!(!is_cloud_placeholder(&f));
96    }
97
98    #[test]
99    fn missing_path_is_not_a_placeholder() {
100        assert!(!is_cloud_placeholder(Path::new(
101            "/nonexistent/lean-ctx/cloud/xyz"
102        )));
103    }
104}