fn next_hfs_char(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Option<char> {
loop {
let ch = chars.next()?;
match ch {
'\u{200c}' | '\u{200d}' | '\u{200e}' | '\u{200f}' => continue,
'\u{202a}'..='\u{202e}' => continue,
'\u{206a}'..='\u{206f}' => continue,
'\u{feff}' => continue,
_ => return Some(ch),
}
}
}
fn is_hfs_dot_generic(path: &str, needle: &str) -> bool {
let mut chars = path.chars().peekable();
let mut c = match next_hfs_char(&mut chars) {
Some(x) => x,
None => return false,
};
if c != '.' {
return false;
}
for nc in needle.chars() {
c = match next_hfs_char(&mut chars) {
Some(x) => x,
None => return false,
};
if c as u32 > 127 {
return false;
}
if !c.eq_ignore_ascii_case(&nc) {
return false;
}
}
match next_hfs_char(&mut chars) {
None => true,
Some('/') => true,
Some(_) => false,
}
}
#[must_use]
pub fn is_hfs_dotgit(path: &str) -> bool {
is_hfs_dot_generic(path, "git")
}
#[must_use]
pub fn is_ntfs_dotgit(name: &str) -> bool {
is_ntfs_dot_generic(name, "git", "g7c29")
}
#[must_use]
pub fn is_hfs_dot_gitmodules(path: &str) -> bool {
is_hfs_dot_generic(path, "gitmodules")
}
#[must_use]
pub fn is_hfs_dot_gitignore(path: &str) -> bool {
is_hfs_dot_generic(path, "gitignore")
}
#[must_use]
pub fn is_hfs_dot_gitattributes(path: &str) -> bool {
is_hfs_dot_generic(path, "gitattributes")
}
#[must_use]
pub fn is_hfs_dot_mailmap(path: &str) -> bool {
is_hfs_dot_generic(path, "mailmap")
}
fn only_spaces_and_periods(name: &str, mut i: usize) -> bool {
let b = name.as_bytes();
loop {
let c = *b.get(i).unwrap_or(&0);
if c == 0 || c == b':' {
return true;
}
if c != b' ' && c != b'.' {
return false;
}
i += 1;
}
}
fn bytes_eq_ignore_ascii_case(left: &[u8], right: &[u8]) -> bool {
left.len() == right.len()
&& left
.iter()
.zip(right.iter())
.all(|(a, b)| a.eq_ignore_ascii_case(b))
}
fn is_ntfs_dot_generic(name: &str, dotgit_name: &str, short_prefix: &str) -> bool {
let b = name.as_bytes();
let len = dotgit_name.len();
let after_prefix = 1 + len;
if b.len() >= after_prefix
&& b[0] == b'.'
&& bytes_eq_ignore_ascii_case(&b[1..after_prefix], dotgit_name.as_bytes())
{
if b.len() == after_prefix {
return true;
}
return only_spaces_and_periods(name, after_prefix);
}
if b.len() > after_prefix + 1
&& b[0] == b'.'
&& bytes_eq_ignore_ascii_case(&b[1..after_prefix], dotgit_name.as_bytes())
&& b[after_prefix] == b'~'
&& (b'1'..=b'4').contains(&b[after_prefix + 1])
{
return only_spaces_and_periods(name, after_prefix + 2);
}
let prefix6 = dotgit_name.as_bytes();
if prefix6.len() >= 6
&& b.len() >= 8
&& bytes_eq_ignore_ascii_case(&b[..6], &prefix6[..6])
&& b[6] == b'~'
&& (b'1'..=b'4').contains(&b[7])
{
return only_spaces_and_periods(name, 8);
}
let mut i = 0usize;
let mut saw_tilde = false;
while i < 8 {
let c = *b.get(i).unwrap_or(&0);
if c == 0 {
return false;
}
if saw_tilde {
if !c.is_ascii_digit() {
return false;
}
} else if c == b'~' {
i += 1;
let d = *b.get(i).unwrap_or(&0);
if !(b'1'..=b'9').contains(&d) {
return false;
}
saw_tilde = true;
} else if i >= 6 || c & 0x80 != 0 {
return false;
} else {
let sc = short_prefix.as_bytes().get(i).copied().unwrap_or(0);
if (c as char).to_ascii_lowercase() != sc as char {
return false;
}
}
i += 1;
}
only_spaces_and_periods(name, i)
}
#[must_use]
pub fn is_ntfs_dot_gitmodules(name: &str) -> bool {
is_ntfs_dot_generic(name, "gitmodules", "gi7eba")
}
#[must_use]
pub fn is_ntfs_dot_gitignore(name: &str) -> bool {
is_ntfs_dot_generic(name, "gitignore", "gi250a")
}
#[must_use]
pub fn is_ntfs_dot_gitattributes(name: &str) -> bool {
is_ntfs_dot_generic(name, "gitattributes", "gi7d29")
}
#[must_use]
pub fn is_ntfs_dot_mailmap(name: &str) -> bool {
is_ntfs_dot_generic(name, "mailmap", "maba30")
}
#[must_use]
pub fn dotfile_matches(subcmd: &str, path: &str) -> bool {
match subcmd {
"is_dotgitmodules" => is_hfs_dot_gitmodules(path) || is_ntfs_dot_gitmodules(path),
"is_dotgitignore" => is_hfs_dot_gitignore(path) || is_ntfs_dot_gitignore(path),
"is_dotgitattributes" => is_hfs_dot_gitattributes(path) || is_ntfs_dot_gitattributes(path),
"is_dotmailmap" => is_hfs_dot_mailmap(path) || is_ntfs_dot_mailmap(path),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ntfs_dot_checks_multibyte_char_at_byte_six_do_not_panic() {
for name in ["abcde日.txt", "ab日本.txt", "file0日本.txt"] {
assert!(!is_ntfs_dot_gitmodules(name));
assert!(!is_ntfs_dot_gitattributes(name));
assert!(!is_ntfs_dot_gitignore(name));
assert!(!is_ntfs_dot_mailmap(name));
assert!(!is_ntfs_dotgit(name));
}
}
#[test]
fn ntfs_dos83_short_aliases_without_leading_dot() {
assert!(is_ntfs_dot_gitmodules("gitmod~1"));
assert!(is_ntfs_dot_gitmodules("GITMOD~2"));
assert!(is_ntfs_dot_gitattributes("gitatt~1"));
assert!(is_ntfs_dot_gitignore("gitign~1"));
assert!(is_ntfs_dot_mailmap("mailma~1"));
assert!(!is_ntfs_dot_gitmodules("gitmod~5"));
assert!(!is_ntfs_dot_gitmodules("gitmo~1"));
}
}