use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct FsProbe {
pub name: Option<String>,
pub is_local: Option<bool>,
pub total_bytes: Option<u64>,
}
impl FsProbe {
fn unknown() -> Self {
Self::default()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CacheFsVerdict {
Local,
NotLocal { name: String },
Unknown,
}
pub fn classify(probe: &FsProbe) -> CacheFsVerdict {
match probe.is_local {
Some(true) => CacheFsVerdict::Local,
Some(false) => CacheFsVerdict::NotLocal {
name: probe
.name
.clone()
.unwrap_or_else(|| "network or virtual filesystem".to_string()),
},
None => CacheFsVerdict::Unknown,
}
}
pub fn advisory_message(name: &str, cache_dir: &Path) -> String {
format!(
"[kache] the cache directory is on {name} ({dir}), which is not host-local\n\
[kache] storage. The cache index is a WAL-mode SQLite database: it needs\n\
[kache] working file locking and a single writing machine. On a shared or\n\
[kache] network mount it can be silently CORRUPTED — builds keep working\n\
[kache] until they don't, and the cache is then rebuilt from scratch.\n\
[kache] → set KACHE_CACHE_DIR to a fast, local, single-machine path\n\
[kache] → to share artifacts BETWEEN machines, use a remote cache (S3 or\n\
[kache] a filesystem remote) rather than a shared cache directory",
name = name,
dir = cache_dir.display(),
)
}
pub fn advisory_for(probe: &FsProbe, cache_dir: &Path) -> Option<String> {
match classify(probe) {
CacheFsVerdict::NotLocal { name } => Some(advisory_message(&name, cache_dir)),
CacheFsVerdict::Local | CacheFsVerdict::Unknown => None,
}
}
pub fn probe(path: &Path) -> FsProbe {
#[cfg(target_os = "macos")]
let probe = probe_macos(path);
#[cfg(target_os = "linux")]
let probe = probe_linux(path);
#[cfg(windows)]
let probe = probe_windows(path);
#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
let probe = probe_unsupported(path);
probe
}
#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), allow(dead_code))]
fn disk_bytes(block_size: u64, blocks: u64) -> Option<u64> {
if block_size == 0 {
return None;
}
block_size.checked_mul(blocks)
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn linux_fragment_bytes(frsize: u64, bsize: u64) -> u64 {
if frsize > 0 { frsize } else { bsize }
}
#[cfg_attr(not(windows), allow(dead_code))]
fn accepted_volume_total(ok: bool, total: u64) -> Option<u64> {
(ok && total > 0).then_some(total)
}
#[cfg_attr(not(windows), allow(dead_code))]
fn win32_succeeded(ok: i32) -> bool {
ok != 0
}
#[cfg(target_os = "macos")]
fn probe_macos(path: &Path) -> FsProbe {
let Some(stat) = statfs_of(path) else {
return FsProbe::unknown();
};
FsProbe {
name: c_str_field_to_string(&stat.f_fstypename),
is_local: Some(stat.f_flags & (libc::MNT_LOCAL as u32) != 0),
total_bytes: statfs_total_bytes_macos(&stat),
}
}
#[cfg(target_os = "macos")]
fn c_str_field_to_string(field: &[libc::c_char]) -> Option<String> {
let bytes: Vec<u8> = field
.iter()
.take_while(|&&c| c != 0)
.map(|&c| c as u8)
.collect();
if bytes.is_empty() {
return None;
}
String::from_utf8(bytes).ok()
}
#[cfg(target_os = "linux")]
fn probe_linux(path: &Path) -> FsProbe {
let Some(stat) = statfs_of(path) else {
return FsProbe::unknown();
};
#[allow(clippy::unnecessary_cast)]
let magic = stat.f_type as i64;
let mut probe = classify_linux_magic(magic);
probe.total_bytes = statfs_total_bytes_linux(&stat);
probe
}
#[allow(dead_code)]
mod magic {
pub const NFS: i64 = 0x0000_6969;
pub const SMB: i64 = 0x0000_517B; pub const CIFS: i64 = 0xFF53_4D42; pub const SMB2: i64 = 0xFE53_4D42; pub const V9FS: i64 = 0x0102_1997; pub const CEPH: i64 = 0x00C3_6400;
pub const GFS2: i64 = 0x0116_1970;
pub const LUSTRE: i64 = 0x0BD0_0BD0;
pub const NCP: i64 = 0x0000_564C;
pub const AFS: i64 = 0x5346_414F; pub const AFS_FS: i64 = 0x6B41_4653; pub const FUSE: i64 = 0x6573_5546;
pub const EXT: i64 = 0x0000_EF53; pub const BTRFS: i64 = 0x9123_683E;
pub const XFS: i64 = 0x5846_5342;
pub const F2FS: i64 = 0xF2F5_2010;
pub const ZFS: i64 = 0x2FC1_2FC1;
pub const TMPFS: i64 = 0x0102_1994;
pub const OVERLAYFS: i64 = 0x794C_7630;
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn classify_linux_magic(magic: i64) -> FsProbe {
let named = |name: &str, is_local: bool| FsProbe {
name: Some(name.to_string()),
is_local: Some(is_local),
total_bytes: None,
};
match magic {
magic::NFS => named("nfs", false),
magic::SMB => named("smbfs", false),
magic::CIFS => named("cifs (SMB)", false),
magic::SMB2 => named("smb2/smb3", false),
magic::V9FS => named("9p", false),
magic::CEPH => named("ceph", false),
magic::GFS2 => named("gfs2", false),
magic::LUSTRE => named("lustre", false),
magic::NCP => named("ncpfs", false),
magic::AFS | magic::AFS_FS => named("afs", false),
magic::FUSE => named("a FUSE filesystem", false),
magic::EXT => named("ext2/3/4", true),
magic::BTRFS => named("btrfs", true),
magic::XFS => named("xfs", true),
magic::F2FS => named("f2fs", true),
magic::ZFS => named("zfs", true),
magic::TMPFS => named("tmpfs", true),
magic::OVERLAYFS => named("overlayfs", true),
_ => FsProbe::unknown(),
}
}
#[cfg(target_os = "macos")]
fn statfs_total_bytes_macos(stat: &libc::statfs) -> Option<u64> {
disk_bytes(u64::from(stat.f_bsize), stat.f_blocks)
}
#[cfg(target_os = "linux")]
fn statfs_total_bytes_linux(stat: &libc::statfs) -> Option<u64> {
let frsize = u64::try_from(stat.f_frsize).unwrap_or(0);
let bsize = u64::try_from(stat.f_bsize).unwrap_or(0);
disk_bytes(linux_fragment_bytes(frsize, bsize), stat.f_blocks)
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn statfs_of(path: &Path) -> Option<libc::statfs> {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let target = nearest_existing(path)?;
let c_path = CString::new(target.as_os_str().as_bytes()).ok()?;
let mut stat: libc::statfs = unsafe { std::mem::zeroed() };
let rc = unsafe { libc::statfs(c_path.as_ptr(), &mut stat) };
(rc == 0).then_some(stat)
}
#[cfg(any(target_os = "macos", target_os = "linux", windows))]
fn nearest_existing(path: &Path) -> Option<std::path::PathBuf> {
let mut current = path;
loop {
if current.exists() {
return Some(current.to_path_buf());
}
current = current.parent()?;
}
}
#[cfg(windows)]
fn probe_windows(path: &Path) -> FsProbe {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Storage::FileSystem::{GetDriveTypeW, GetVolumeInformationW};
const DRIVE_UNKNOWN: u32 = 0;
const DRIVE_NO_ROOT_DIR: u32 = 1;
const DRIVE_REMOTE: u32 = 4;
let Some(target) = nearest_existing(path) else {
return FsProbe::unknown();
};
if is_unc_path(&target) {
return FsProbe {
name: Some("a network share (UNC path)".to_string()),
is_local: Some(false),
total_bytes: volume_total_bytes_windows(&target),
};
}
let Some(root) = volume_root(&target) else {
return FsProbe::unknown();
};
let wide: Vec<u16> = std::ffi::OsStr::new(&root)
.encode_wide()
.chain(Some(0))
.collect();
let drive_type = unsafe { GetDriveTypeW(wide.as_ptr()) };
let is_local = match drive_type {
DRIVE_REMOTE => Some(false),
DRIVE_UNKNOWN | DRIVE_NO_ROOT_DIR => None,
_ => Some(true),
};
let mut fs_name = [0u16; 64];
let ok = unsafe {
GetVolumeInformationW(
wide.as_ptr(),
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
fs_name.as_mut_ptr(),
fs_name.len() as u32,
)
};
let name = (ok != 0).then(|| {
let len = fs_name
.iter()
.position(|&c| c == 0)
.unwrap_or(fs_name.len());
String::from_utf16_lossy(&fs_name[..len])
});
FsProbe {
name,
is_local,
total_bytes: volume_total_bytes_windows(&target),
}
}
#[cfg(windows)]
fn volume_total_bytes_windows(path: &Path) -> Option<u64> {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW;
let wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
let mut total: u64 = 0;
let ok = unsafe {
GetDiskFreeSpaceExW(
wide.as_ptr(),
std::ptr::null_mut(),
&mut total,
std::ptr::null_mut(),
)
};
accepted_volume_total(win32_succeeded(ok), total)
}
#[cfg(windows)]
fn volume_root(path: &Path) -> Option<String> {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Storage::FileSystem::GetVolumePathNameW;
let wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
let mut root = [0u16; 260];
let ok = unsafe { GetVolumePathNameW(wide.as_ptr(), root.as_mut_ptr(), root.len() as u32) };
if ok == 0 {
return None;
}
let len = root.iter().position(|&c| c == 0).unwrap_or(root.len());
Some(String::from_utf16_lossy(&root[..len]))
}
#[cfg_attr(not(windows), allow(dead_code))]
fn is_unc_path(path: &Path) -> bool {
let s = path.to_string_lossy();
let s = s.replace('/', "\\");
if let Some(rest) = s.strip_prefix("\\\\?\\") {
return rest.to_ascii_uppercase().starts_with("UNC\\");
}
s.starts_with("\\\\") && !s.starts_with("\\\\.\\")
}
#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
fn probe_unsupported(_path: &Path) -> FsProbe {
FsProbe::unknown()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_filesystem_says_nothing() {
let probe = FsProbe {
name: Some("apfs".to_string()),
is_local: Some(true),
total_bytes: None,
};
assert_eq!(classify(&probe), CacheFsVerdict::Local);
}
#[test]
fn network_filesystem_is_reported_by_name() {
let probe = FsProbe {
name: Some("nfs".to_string()),
is_local: Some(false),
total_bytes: None,
};
assert_eq!(
classify(&probe),
CacheFsVerdict::NotLocal {
name: "nfs".to_string()
}
);
}
#[test]
fn non_local_without_a_name_still_warns_generically() {
let probe = FsProbe {
name: None,
is_local: Some(false),
total_bytes: None,
};
let CacheFsVerdict::NotLocal { name } = classify(&probe) else {
panic!("a non-local filesystem must warn even when unnamed");
};
assert_eq!(name, "network or virtual filesystem");
}
#[test]
fn an_unknown_probe_is_silent_not_a_guess() {
assert_eq!(classify(&FsProbe::unknown()), CacheFsVerdict::Unknown);
let named_but_unplaced = FsProbe {
name: Some("weirdfs".to_string()),
is_local: None,
total_bytes: None,
};
assert_eq!(classify(&named_but_unplaced), CacheFsVerdict::Unknown);
}
#[test]
fn the_advisory_names_the_filesystem_and_the_directory() {
let msg = advisory_message("nfs", Path::new("/mnt/shared/kache"));
assert!(
msg.contains("nfs"),
"message must name the filesystem: {msg}"
);
assert!(
msg.contains("/mnt/shared/kache"),
"message must show which directory is affected: {msg}"
);
assert!(
msg.contains("KACHE_CACHE_DIR"),
"message must give the actionable remedy: {msg}"
);
assert!(
msg.contains("CORRUPTED"),
"message must state the actual risk: {msg}"
);
for line in msg.lines() {
assert!(
line.starts_with("[kache]"),
"every line carries the prefix so it can't be mistaken for compiler output: {line}"
);
}
}
#[test]
fn advisory_is_produced_only_for_a_non_local_filesystem() {
let dir = Path::new("/mnt/shared/kache");
let advisory = advisory_for(
&FsProbe {
name: Some("nfs".to_string()),
is_local: Some(false),
total_bytes: None,
},
dir,
);
assert_eq!(
advisory.as_deref(),
Some(advisory_message("nfs", dir)).as_deref()
);
for quiet in [
FsProbe {
name: Some("apfs".to_string()),
is_local: Some(true),
total_bytes: None,
},
FsProbe::unknown(),
] {
assert_eq!(
advisory_for(&quiet, dir),
None,
"must stay silent for {quiet:?}"
);
}
}
#[test]
fn disk_bytes_rejects_zero_block_size_and_multiplies() {
assert_eq!(disk_bytes(0, 100), None);
assert_eq!(disk_bytes(4096, 0), Some(0));
assert_eq!(disk_bytes(4096, 2), Some(8192));
assert_eq!(disk_bytes(u64::MAX, 2), None);
}
#[test]
fn linux_fragment_bytes_prefers_frsize_when_nonzero() {
assert_eq!(linux_fragment_bytes(4096, 512), 4096);
assert_eq!(linux_fragment_bytes(0, 512), 512);
assert_eq!(linux_fragment_bytes(0, 0), 0);
}
#[test]
fn accepted_volume_total_requires_success_and_nonzero() {
assert_eq!(accepted_volume_total(false, 99), None);
assert_eq!(accepted_volume_total(true, 0), None);
assert_eq!(accepted_volume_total(true, 1), Some(1));
assert_eq!(accepted_volume_total(false, 0), None);
assert!(win32_succeeded(1));
assert!(!win32_succeeded(0));
}
#[test]
fn probe_reports_a_positive_volume_size_for_a_real_directory() {
let dir = tempfile::tempdir().unwrap();
let probed = probe(dir.path());
assert!(
probed.total_bytes.is_some_and(|n| n > 1_000_000),
"a real directory must yield a volume size, got {:?}",
probed.total_bytes
);
assert_ne!(
probed,
FsProbe::default(),
"a real local probe must not be the empty default"
);
}
#[test]
fn a_linux_shared_mount_produces_an_advisory_end_to_end() {
let advisory = advisory_for(&classify_linux_magic(0x0102_1997), Path::new("/kache"))
.expect("a 9p mount must produce an advisory");
assert!(advisory.contains("9p"), "{advisory}");
assert!(advisory.contains("/kache"), "{advisory}");
assert_eq!(
advisory_for(&classify_linux_magic(0x794C_7630), Path::new("/kache")),
None
);
}
#[test]
fn unc_paths_are_recognised_in_every_spelling() {
assert!(is_unc_path(Path::new(r"\\server\share\kache")));
assert!(is_unc_path(Path::new(r"\\?\UNC\server\share\kache")));
assert!(!is_unc_path(Path::new(r"\\.\PhysicalDrive0")));
assert!(!is_unc_path(Path::new(r"C:\Users\me\.cache\kache")));
assert!(!is_unc_path(Path::new(r"\\?\C:\Users\me\.cache\kache")));
assert!(!is_unc_path(Path::new("/home/me/.cache/kache")));
}
#[test]
fn linux_magic_table_flags_every_shared_mount() {
let not_local = |magic: i64| {
let probe = classify_linux_magic(magic);
assert_eq!(
probe.is_local,
Some(false),
"magic {magic:#x} must be classified as non-local"
);
probe.name.unwrap_or_default()
};
assert_eq!(not_local(0x0000_6969), "nfs");
assert_eq!(not_local(0x0000_517B), "smbfs");
assert_eq!(not_local(0xFF53_4D42), "cifs (SMB)");
assert_eq!(not_local(0xFE53_4D42), "smb2/smb3");
assert_eq!(not_local(0x0102_1997), "9p");
assert_eq!(not_local(0x00C3_6400), "ceph");
assert_eq!(not_local(0x0116_1970), "gfs2");
assert_eq!(not_local(0x0BD0_0BD0), "lustre");
assert_eq!(not_local(0x0000_564C), "ncpfs");
assert_eq!(not_local(0x5346_414F), "afs");
assert_eq!(not_local(0x6B41_4653), "afs");
assert_eq!(not_local(0x6573_5546), "a FUSE filesystem");
}
#[test]
fn linux_magic_table_leaves_local_disks_alone() {
for (magic, expected) in [
(0x0000_EF53_i64, "ext2/3/4"),
(0x9123_683E, "btrfs"),
(0x5846_5342, "xfs"),
(0xF2F5_2010, "f2fs"),
(0x2FC1_2FC1, "zfs"),
] {
let probe = classify_linux_magic(magic);
assert_eq!(
classify(&probe),
CacheFsVerdict::Local,
"{expected} must read as local, got {probe:?}"
);
assert_eq!(probe.name.as_deref(), Some(expected));
}
}
#[test]
fn container_filesystems_do_not_warn() {
for (magic, name) in [(0x794C_7630_i64, "overlayfs"), (0x0102_1994, "tmpfs")] {
assert_eq!(
classify(&classify_linux_magic(magic)),
CacheFsVerdict::Local,
"{name} must not produce an advisory"
);
}
}
#[test]
fn an_unrecognised_magic_falls_through_to_silence() {
assert_eq!(
classify(&classify_linux_magic(0x1234_5678)),
CacheFsVerdict::Unknown
);
}
#[test]
fn probing_a_real_local_directory_never_reports_a_network_mount() {
let probe = probe(&std::env::temp_dir());
assert_ne!(
probe.is_local,
Some(false),
"a local temp dir must never be classified as non-local (probe: {probe:?})"
);
}
#[test]
fn probing_a_path_that_does_not_exist_yet_uses_its_parent() {
let missing = std::env::temp_dir().join("kache-cache-fs-probe-does-not-exist");
let _ = std::fs::remove_dir_all(&missing);
let probe = probe(&missing);
assert_ne!(probe.is_local, Some(false));
#[cfg(any(target_os = "macos", target_os = "linux", windows))]
assert!(
probe.is_local.is_some(),
"a missing cache dir must still resolve through its parent: {probe:?}"
);
}
}