#[cfg(any(windows, test))]
use std::ffi::OsString;
use std::path::Path;
#[cfg(target_os = "linux")]
use std::path::PathBuf;
#[cfg(target_os = "linux")]
pub(super) fn unsafe_place(directory: &Path) -> std::io::Result<Option<&'static str>> {
let info = rustix::fs::statfs(directory)?;
let kind = info.f_type as u32;
if kind == magic::FUSE {
let canonical = std::fs::canonicalize(directory)?;
let table = std::fs::read_to_string("/proc/self/mountinfo").unwrap_or_default();
return Ok((mount_type(&table, &canonical) != Some("fuseblk"))
.then_some("on a FUSE mount, such as sshfs or a cloud-sync client"));
}
Ok(remote_file_system(kind))
}
#[cfg(target_os = "linux")]
mod magic {
pub(super) const NFS: u32 = 0x6969;
pub(super) const SMB: u32 = 0x517b;
pub(super) const CIFS: u32 = 0xff53_4d42;
pub(super) const SMB2: u32 = 0xfe53_4d42;
pub(super) const V9FS: u32 = 0x0102_1997;
pub(super) const VBOXSF: u32 = 0x786f_4256;
pub(super) const PRL_FS: u32 = 0x7c7c_6673;
pub(super) const CEPH: u32 = 0x00c3_6400;
pub(super) const AFS: u32 = 0x5346_414f;
pub(super) const AFS_FS: u32 = 0x6b41_4653;
pub(super) const CODA: u32 = 0x7375_7245;
pub(super) const LUSTRE: u32 = 0x0bd0_0bd0;
pub(super) const GFS2: u32 = 0x0116_1970;
pub(super) const OCFS2: u32 = 0x7461_636f;
pub(super) const ORANGEFS: u32 = 0x2003_0528;
pub(super) const BEEGFS: u32 = 0x1983_0326;
pub(super) const GPFS: u32 = 0x4750_4653;
pub(super) const FUSE: u32 = 0x6573_5546;
}
#[cfg(target_os = "linux")]
fn remote_file_system(kind: u32) -> Option<&'static str> {
use magic::*;
Some(match kind {
NFS => "on NFS",
SMB | CIFS | SMB2 => "on an SMB share",
V9FS => "on a 9p mount, such as WSL's view of a Windows drive",
VBOXSF | PRL_FS => "in a virtual machine's shared folder",
CEPH => "on Ceph",
AFS | AFS_FS => "on AFS",
CODA => "on Coda",
LUSTRE => "on Lustre",
GFS2 | OCFS2 => "on a cluster file system",
ORANGEFS => "on OrangeFS",
BEEGFS => "on BeeGFS",
GPFS => "on GPFS",
_ => return None,
})
}
#[cfg(target_os = "linux")]
fn mount_type<'t>(table: &'t str, path: &Path) -> Option<&'t str> {
table
.lines()
.filter_map(|line| {
let fields: Vec<&str> = line.split(' ').collect();
let point = PathBuf::from(unescape_mount_field(fields.get(4)?));
let separator = fields.iter().position(|field| *field == "-")?;
let kind = *fields.get(separator + 1)?;
path.starts_with(&point)
.then(|| (point.as_os_str().len(), kind))
})
.max_by_key(|(length, _)| *length)
.map(|(_, kind)| kind)
}
#[cfg(target_os = "linux")]
fn unescape_mount_field(field: &str) -> String {
let bytes = field.as_bytes();
let mut decoded = Vec::with_capacity(bytes.len());
let mut at = 0;
while at < bytes.len() {
match bytes.get(at + 1..at + 4) {
Some(digits)
if bytes[at] == b'\\' && digits.iter().all(|d| (b'0'..=b'7').contains(d)) =>
{
let value = digits
.iter()
.fold(0u32, |value, digit| value * 8 + u32::from(digit - b'0'));
decoded.push(u8::try_from(value).unwrap_or(b'?'));
at += 4;
}
_ => {
decoded.push(bytes[at]);
at += 1;
}
}
}
String::from_utf8_lossy(&decoded).into_owned()
}
#[cfg(windows)]
pub(super) fn unsafe_place(directory: &Path) -> std::io::Result<Option<&'static str>> {
let canonical = std::fs::canonicalize(directory)?;
Ok((!local_drive(&canonical))
.then_some("not on a local drive (a network share, or WSL's files seen from Windows)"))
}
#[cfg(windows)]
fn local_drive(path: &Path) -> bool {
use std::path::{Component, Prefix};
matches!(
path.components().next(),
Some(Component::Prefix(prefix))
if matches!(prefix.kind(), Prefix::Disk(_) | Prefix::VerbatimDisk(_))
)
}
#[cfg(not(any(target_os = "linux", windows)))]
pub(super) fn unsafe_place(_directory: &Path) -> std::io::Result<Option<&'static str>> {
Ok(None)
}
#[cfg(windows)]
pub(super) fn in_onedrive(directory: &Path) -> bool {
let roots = ["OneDrive", "OneDriveCommercial", "OneDriveConsumer"]
.into_iter()
.filter_map(std::env::var_os);
inside_any(directory, roots)
}
#[cfg(not(windows))]
pub(super) fn in_onedrive(_directory: &Path) -> bool {
false
}
#[cfg(any(windows, test))]
fn inside_any(directory: &Path, roots: impl IntoIterator<Item = OsString>) -> bool {
let Ok(directory) = std::fs::canonicalize(directory) else {
return false;
};
roots
.into_iter()
.filter_map(|root| std::fs::canonicalize(root).ok())
.any(|root| directory.starts_with(root))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_local_directory_takes_an_index() {
let directory = tempfile::tempdir().expect("directory");
assert_eq!(unsafe_place(directory.path()).expect("checked"), None);
}
#[cfg(target_os = "linux")]
#[test]
fn network_and_cluster_file_systems_are_unsafe_for_an_index() {
for kind in [
0x6969,
0xff53_4d42,
0xfe53_4d42,
0x0102_1997,
0x786f_4256,
0x00c3_6400,
0x2003_0528,
0x1983_0326,
] {
assert!(remote_file_system(kind).is_some(), "{kind:#x}");
}
for kind in [
0xef53,
0x9123_683e,
0x5846_5342,
0x0102_1994,
0x2fc1_2fc1,
0xf2f5_2010,
] {
assert_eq!(remote_file_system(kind), None, "{kind:#x}");
}
}
#[cfg(target_os = "linux")]
#[test]
fn a_fuse_mount_is_local_only_when_a_block_device_backs_it() {
let table = "\
22 1 8:2 / / rw,relatime shared:1 - ext4 /dev/sda2 rw
40 22 0:36 / /media/data rw,relatime shared:20 - fuseblk /dev/sdb1 rw,user_id=0
41 22 0:37 / /home/me/remote\\040box rw,nosuid shared:21 - fuse.sshfs me@host:/srv rw
42 41 0:38 / /home/me/remote\\040box/cache rw shared:22 - tmpfs tmpfs rw
";
let kind = |path: &str| mount_type(table, Path::new(path));
assert_eq!(kind("/media/data/src"), Some("fuseblk"));
assert_eq!(kind("/home/me/remote box/src"), Some("fuse.sshfs"));
assert_eq!(kind("/home/me/remote box/cache/x"), Some("tmpfs"));
assert_eq!(kind("/home/me/remote"), Some("ext4"));
assert_eq!(kind("/media/database"), Some("ext4"));
}
#[cfg(windows)]
#[test]
fn a_share_is_unsafe_for_an_index() {
assert!(!local_drive(Path::new(r"\\?\UNC\server\share\project")));
assert!(!local_drive(Path::new(r"\\wsl.localhost\Ubuntu\home")));
assert!(local_drive(Path::new(r"\\?\C:\Users\project")));
assert!(local_drive(Path::new(r"D:\project")));
}
#[test]
fn a_folder_inside_a_synchronised_root_is_synchronised() {
let home = tempfile::tempdir().expect("home");
let root = home.path().join("OneDrive - Example");
let project = root.join("src");
let beside = home.path().join("OneDrive");
for directory in [&project, &beside] {
std::fs::create_dir_all(directory).expect("directory");
}
let roots = || {
[
root.clone().into_os_string(),
home.path().join("none").into(),
]
};
assert!(inside_any(&project, roots()));
assert!(inside_any(&root, roots()));
assert!(inside_any(
&beside.join("../OneDrive - Example/src"),
roots()
));
assert!(!inside_any(&beside, roots()));
assert!(!inside_any(home.path(), roots()));
assert!(!inside_any(
&project,
[home.path().join("none").into_os_string()]
));
assert!(!inside_any(&root.join("missing"), roots()));
}
}