#[cfg(any(target_os = "linux", target_os = "macos"))]
use crate::Repository;
#[cfg(any(target_os = "linux", target_os = "macos"))]
mod volume;
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(crate) use volume::NamePolicy;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Protection {
ntfs: bool,
hfs: bool,
}
impl Protection {
pub(crate) const ALL: Self = Self {
ntfs: true,
hfs: true,
};
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(crate) fn from_config(repo: &Repository) -> Result<Self, &'static str> {
let config = repo.config();
let ntfs = config
.boolean("core", None, "protectntfs")
.map_err(|_| "core.protectNTFS")?;
let hfs = config
.boolean("core", None, "protecthfs")
.map_err(|_| "core.protectHFS")?;
Ok(Self {
ntfs: ntfs.unwrap_or(true),
hfs: hfs.unwrap_or(cfg!(target_os = "macos")),
})
}
#[cfg(any(test, target_os = "linux", target_os = "macos"))]
pub(crate) fn refuses(self, path: &[u8]) -> bool {
path.split(|byte| *byte == b'/')
.any(|component| self.refuses_component(component))
}
pub(crate) fn refuses_component(self, component: &[u8]) -> bool {
if component.eq_ignore_ascii_case(b".git") {
return true;
}
if self.ntfs {
let base = component
.split(|byte| *byte == b':')
.next()
.unwrap_or(component);
let end = base
.iter()
.rposition(|byte| !matches!(byte, b'.' | b' '))
.map_or(0, |last| last + 1);
let base = &base[..end];
if base.eq_ignore_ascii_case(b".git") || base.eq_ignore_ascii_case(b"git~1") {
return true;
}
}
self.hfs && without_hfs_ignorables(component).eq_ignore_ascii_case(b".git")
}
}
fn is_hfs_ignorable(c: char) -> bool {
matches!(
c,
'\u{200c}'..='\u{200f}' | '\u{202a}'..='\u{202e}' | '\u{206a}'..='\u{206f}' | '\u{feff}'
)
}
fn without_hfs_ignorables(name: &[u8]) -> Vec<u8> {
let Ok(text) = std::str::from_utf8(name) else {
return name.to_vec();
};
text.chars()
.filter(|c| !is_hfs_ignorable(*c))
.collect::<String>()
.into_bytes()
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::Protection;
const ALL: Protection = Protection {
ntfs: true,
hfs: true,
};
const NONE: Protection = Protection {
ntfs: false,
hfs: false,
};
#[rstest]
#[case::exact(".git", true, true, true)]
#[case::ascii_case("a/.GIT/b", true, true, true)]
#[case::short_name("git~1", true, false, true)]
#[case::short_name_case("GIT~1", true, false, true)]
#[case::trailing_space(".git ", true, false, true)]
#[case::trailing_dots(".git..", true, false, true)]
#[case::stream(".git::$INDEX_ALLOCATION", true, false, true)]
#[case::hfs_ignorable(".g\u{200c}it", true, true, false)]
#[case::bom("\u{feff}.git", true, true, false)]
#[case::colon("c:d", false, false, false)]
#[case::backslash("a\\b", false, false, false)]
#[case::device("aux", false, false, false)]
#[case::gitignore(".gitignore", false, false, false)]
#[case::git_dot_x(".git.x", false, false, false)]
fn matches_git_metadata_rules(
#[case] path: &str,
#[case] all: bool,
#[case] hfs_only: bool,
#[case] ntfs_only: bool,
) {
let path = path.as_bytes();
assert_eq!(ALL.refuses(path), all);
assert_eq!(
Protection {
ntfs: false,
hfs: true
}
.refuses(path),
hfs_only
);
assert_eq!(
Protection {
ntfs: true,
hfs: false
}
.refuses(path),
ntfs_only
);
assert_eq!(NONE.refuses(path), all && hfs_only && ntfs_only);
}
}