use std::path::{Component, Path, PathBuf};
pub const CASE_INSENSITIVE_FS: bool = cfg!(any(windows, target_os = "macos"));
pub fn relative_under(root: &Path, path: &Path) -> Option<PathBuf> {
relative_under_with(root, path, CASE_INSENSITIVE_FS)
}
pub fn relative_under_with(root: &Path, path: &Path, case_insensitive: bool) -> Option<PathBuf> {
if !case_insensitive {
return path.strip_prefix(root).ok().map(Path::to_path_buf);
}
let mut below = path.components();
for root_part in root.components() {
let part = below.next()?;
if fold(root_part) != fold(part) {
return None;
}
}
Some(below.as_path().to_path_buf())
}
fn fold(component: Component<'_>) -> String {
component.as_os_str().to_string_lossy().to_lowercase()
}
pub fn covers(root: &Path, path: &Path) -> bool {
relative_under(root, path).is_some()
}
pub fn is_inside(root: &Path, path: &Path) -> bool {
relative_under(root, path).is_some_and(|rest| rest.components().next().is_some())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_folder_covers_what_is_under_it_by_components() {
assert!(covers(Path::new("/h/docs"), Path::new("/h/docs/work")));
assert!(covers(Path::new("/h/docs"), Path::new("/h/docs")));
assert!(is_inside(Path::new("/h/docs"), Path::new("/h/docs/a/b")));
assert!(!is_inside(Path::new("/h/docs"), Path::new("/h/docs")));
assert!(!covers(Path::new("/h/docs/work"), Path::new("/h/docs")));
}
#[test]
fn the_component_rule_is_not_a_string_prefix() {
assert!(!covers(Path::new("/h/a"), Path::new("/h/a2")));
assert!(!covers(Path::new("/h/a"), Path::new("/h/a2/x")));
assert!(!is_inside(Path::new("/h/docs"), Path::new("/h/docs2")));
for ci in [false, true] {
assert_eq!(
relative_under_with(Path::new("/h/a"), Path::new("/h/a2"), ci),
None
);
}
}
#[test]
fn the_rest_is_what_lies_below_the_root() {
for ci in [false, true] {
assert_eq!(
relative_under_with(Path::new("/h/a"), Path::new("/h/a/b/c"), ci),
Some(PathBuf::from("b/c"))
);
assert_eq!(
relative_under_with(Path::new("/h/a"), Path::new("/h/a"), ci),
Some(PathBuf::new())
);
}
}
#[test]
fn case_differences_matter_only_where_the_file_system_ignores_them() {
let root = Path::new("/h/Docs");
let path = Path::new("/h/docs/Work");
assert_eq!(relative_under_with(root, path, false), None);
assert_eq!(
relative_under_with(root, path, true),
Some(PathBuf::from("Work"))
);
}
}