use std::io;
use std::path::{Path, PathBuf};
use std::time::Duration;
use crate::error::{Error, Result};
#[cfg(unix)]
mod unix;
#[cfg(windows)]
mod windows;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FilesystemCapabilities {
pub name: String,
pub case_sensitive: bool,
pub max_file_size: Option<u64>,
pub windows_naming_rules: bool,
pub timestamp_granularity: Duration,
pub write_integrity_risk: bool,
}
pub(crate) async fn probe(path: &Path) -> Result<FilesystemCapabilities> {
let path = path.to_path_buf();
tokio::task::spawn_blocking(move || probe_blocking(&path))
.await
.expect("filesystem probe blocking task panicked")
}
fn probe_blocking(path: &Path) -> Result<FilesystemCapabilities> {
let existing = nearest_existing_ancestor(path)?;
raw_probe(&existing)
}
fn nearest_existing_ancestor(path: &Path) -> Result<PathBuf> {
let mut current = path;
loop {
if current.exists() {
return Ok(current.to_path_buf());
}
match current.parent() {
Some(parent) => current = parent,
None => return Err(Error::SourceNotFound { path: path.to_path_buf() }),
}
}
}
#[cfg(unix)]
fn raw_probe(path: &Path) -> Result<FilesystemCapabilities> {
unix::probe(path)
}
#[cfg(windows)]
fn raw_probe(path: &Path) -> Result<FilesystemCapabilities> {
windows::probe(path)
}
#[cfg(not(any(unix, windows)))]
fn raw_probe(_path: &Path) -> Result<FilesystemCapabilities> {
Ok(FilesystemCapabilities {
name: "unknown".to_string(),
case_sensitive: true,
max_file_size: None,
windows_naming_rules: false,
timestamp_granularity: Duration::ZERO,
write_integrity_risk: false,
})
}
#[cfg(any(unix, windows))]
fn classify_io_error(err: io::Error, path: PathBuf) -> Error {
match err.kind() {
io::ErrorKind::NotFound => Error::SourceNotFound { path },
io::ErrorKind::PermissionDenied => Error::PermissionDenied { path },
_ => Error::Io { path, source: err },
}
}
fn classify_by_name(name: &str, host_is_macos: bool) -> (Option<u64>, bool, Duration, bool) {
match name {
"exfat" => {
let write_integrity_risk = host_is_macos;
(None, true, Duration::from_secs(2), write_integrity_risk)
}
"msdos" | "vfat" | "fat32" | "fat16" | "fat" => {
(Some(4_294_967_295), true, Duration::from_secs(2), false)
}
"ntfs" => (None, true, Duration::ZERO, false),
"apfs" | "hfs" | "ext2" | "ext3" | "ext4" | "btrfs" | "xfs" => {
(None, false, Duration::ZERO, false)
}
_ => (None, false, Duration::ZERO, false),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exfat_on_macos_is_flagged_as_integrity_risk() {
let (_, windows_naming, granularity, risk) = classify_by_name("exfat", true);
assert!(windows_naming);
assert_eq!(granularity, Duration::from_secs(2));
assert!(risk);
}
#[test]
fn exfat_on_non_macos_is_not_flagged_as_integrity_risk() {
let (_, _, _, risk) = classify_by_name("exfat", false);
assert!(!risk);
}
#[test]
fn fat32_has_the_four_gigabyte_ceiling() {
let (max_size, windows_naming, granularity, risk) = classify_by_name("msdos", false);
assert_eq!(max_size, Some(4_294_967_295));
assert!(windows_naming);
assert_eq!(granularity, Duration::from_secs(2));
assert!(!risk);
}
#[test]
fn apfs_has_no_special_restrictions() {
let (max_size, windows_naming, granularity, risk) = classify_by_name("apfs", true);
assert_eq!(max_size, None);
assert!(!windows_naming);
assert_eq!(granularity, Duration::ZERO);
assert!(!risk);
}
#[test]
fn unrecognized_filesystem_gets_conservative_defaults() {
let (max_size, windows_naming, granularity, risk) = classify_by_name("zfs", false);
assert_eq!(max_size, None);
assert!(!windows_naming);
assert_eq!(granularity, Duration::ZERO);
assert!(!risk);
}
#[tokio::test]
async fn probing_a_path_that_does_not_exist_yet_walks_up_to_an_existing_ancestor() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("does").join("not").join("exist");
let caps = probe(&missing).await.unwrap();
assert!(!caps.name.is_empty());
}
#[tokio::test]
async fn probing_an_existing_path_directly_succeeds() {
let dir = tempfile::tempdir().unwrap();
let caps = probe(dir.path()).await.unwrap();
assert!(!caps.name.is_empty());
}
}