use std::path::Path;
use crate::Capabilities;
#[cfg(windows)]
impl Default for Capabilities {
fn default() -> Self {
Capabilities {
precompose_unicode: false,
ignore_case: true,
executable_bit: false,
symlink: false,
}
}
}
#[cfg(target_os = "macos")]
impl Default for Capabilities {
fn default() -> Self {
Capabilities {
precompose_unicode: true,
ignore_case: true,
executable_bit: true,
symlink: true,
}
}
}
#[cfg(all(unix, not(target_os = "macos")))]
impl Default for Capabilities {
fn default() -> Self {
Capabilities {
precompose_unicode: false,
ignore_case: false,
executable_bit: true,
symlink: true,
}
}
}
enum Dir {
IsGit,
IsArbitrary,
}
impl Capabilities {
pub fn probe(git_dir: &Path) -> Self {
Self::probe_inner(git_dir, Dir::IsGit)
}
pub fn probe_dir(dir: &Path) -> Self {
Self::probe_inner(dir, Dir::IsArbitrary)
}
fn probe_inner(dir: &Path, mode: Dir) -> Self {
let ctx = Capabilities::default();
Capabilities {
symlink: Self::probe_symlink(dir).unwrap_or(ctx.symlink),
ignore_case: Self::probe_ignore_case(dir, mode).unwrap_or(ctx.ignore_case),
precompose_unicode: Self::probe_precompose_unicode(dir).unwrap_or(ctx.precompose_unicode),
executable_bit: Self::probe_file_mode(dir).unwrap_or(ctx.executable_bit),
}
}
#[cfg(unix)]
fn probe_file_mode(root: &Path) -> std::io::Result<bool> {
use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
let rand = fastrand::usize(..);
let test_path = root.join(format!("_test_executable_bit{rand}"));
let res = std::fs::OpenOptions::new()
.create_new(true)
.write(true)
.mode(0o777)
.open(&test_path)
.and_then(|file| {
let old_mode = file.metadata()?.mode();
let is_executable = old_mode & 0o100 == 0o100;
let exe_bit_flip_works_in_filesystem = {
let toggled_exe_bit = old_mode ^ 0o100;
match file.set_permissions(PermissionsExt::from_mode(toggled_exe_bit)) {
Ok(()) => toggled_exe_bit == file.metadata()?.mode(),
Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => false,
Err(err) => return Err(err),
}
};
Ok(is_executable && exe_bit_flip_works_in_filesystem)
});
std::fs::remove_file(test_path)?;
res
}
#[cfg(not(unix))]
fn probe_file_mode(_root: &Path) -> std::io::Result<bool> {
Ok(false)
}
fn probe_ignore_case(git_dir: &Path, mode: Dir) -> std::io::Result<bool> {
match mode {
Dir::IsGit => {
for (probe_name, canonical_name) in [("hEaD", "HEAD"), ("cOnFiG", "config")] {
match std::fs::metadata(git_dir.join(probe_name)) {
Ok(_) => return Ok(true),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
if git_dir.join(canonical_name).exists() {
return Ok(false);
}
}
Err(err) => return Err(err),
}
}
Ok(false)
}
Dir::IsArbitrary => std::fs::metadata(git_dir.join("cOnFiG")).map(|_| true).or_else(|err| {
if err.kind() == std::io::ErrorKind::NotFound {
if git_dir.join("config").exists() {
return Ok(false);
}
use std::io::ErrorKind;
let (mut lower, mut mixed) = (None, None);
let res = (|| {
let first = git_dir.join("config");
let second = git_dir.join("cOnFiG");
std::fs::OpenOptions::new().create_new(true).write(true).open(&first)?;
lower = Some(first);
match std::fs::OpenOptions::new().create_new(true).write(true).open(&second) {
Ok(_) => {
mixed = Some(second);
Ok(false)
}
Err(err) if err.kind() == ErrorKind::AlreadyExists => Ok(true),
Err(err) => Err(err),
}
})();
for file_to_remove in lower.into_iter().chain(mixed) {
std::fs::remove_file(file_to_remove).ok();
}
res
} else {
Err(err)
}
}),
}
}
fn probe_precompose_unicode(root: &Path) -> std::io::Result<bool> {
let rand = fastrand::usize(..);
let precomposed = format!("รค{rand}");
let decomposed = format!("a\u{308}{rand}");
let precomposed = root.join(precomposed);
std::fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&precomposed)?;
let res = root.join(decomposed).symlink_metadata().map(|_| true);
std::fs::remove_file(precomposed)?;
res
}
fn probe_symlink(root: &Path) -> std::io::Result<bool> {
let rand = fastrand::usize(..);
let link_path = root.join(format!("__file_link{rand}"));
if crate::symlink::create("dangling".as_ref(), &link_path).is_err() {
return Ok(false);
}
let res = std::fs::symlink_metadata(&link_path).map(|m| m.file_type().is_symlink());
crate::symlink::remove(&link_path).or_else(|_| std::fs::remove_file(&link_path))?;
res
}
}
#[cfg(target_os = "wasi")]
impl Default for Capabilities {
fn default() -> Self {
Capabilities {
precompose_unicode: false,
ignore_case: false,
executable_bit: false,
symlink: true,
}
}
}
#[cfg(not(any(windows, unix, target_os = "wasi")))]
impl Default for Capabilities {
fn default() -> Self {
Capabilities {
precompose_unicode: false,
ignore_case: false,
executable_bit: false,
symlink: false,
}
}
}