1use std::path::Path;
3
4use crate::Capabilities;
5
6#[cfg(windows)]
7impl Default for Capabilities {
8 fn default() -> Self {
9 Capabilities {
10 precompose_unicode: false,
11 ignore_case: true,
12 executable_bit: false,
13 symlink: false,
14 }
15 }
16}
17
18#[cfg(target_os = "macos")]
19impl Default for Capabilities {
20 fn default() -> Self {
21 Capabilities {
22 precompose_unicode: true,
23 ignore_case: true,
24 executable_bit: true,
25 symlink: true,
26 }
27 }
28}
29
30#[cfg(all(unix, not(target_os = "macos")))]
31impl Default for Capabilities {
32 fn default() -> Self {
33 Capabilities {
34 precompose_unicode: false,
35 ignore_case: false,
36 executable_bit: true,
37 symlink: true,
38 }
39 }
40}
41
42impl Capabilities {
43 pub fn probe(git_dir: &Path) -> Self {
49 let ctx = Capabilities::default();
50 Capabilities {
51 symlink: Self::probe_symlink(git_dir).unwrap_or(ctx.symlink),
52 ignore_case: Self::probe_ignore_case(git_dir).unwrap_or(ctx.ignore_case),
53 precompose_unicode: Self::probe_precompose_unicode(git_dir).unwrap_or(ctx.precompose_unicode),
54 executable_bit: Self::probe_file_mode(git_dir).unwrap_or(ctx.executable_bit),
55 }
56 }
57
58 #[cfg(unix)]
59 fn probe_file_mode(root: &Path) -> std::io::Result<bool> {
60 use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
61
62 let rand = fastrand::usize(..);
67 let test_path = root.join(format!("_test_executable_bit{rand}"));
68 let res = std::fs::OpenOptions::new()
69 .create_new(true)
70 .write(true)
71 .mode(0o777)
72 .open(&test_path)
73 .and_then(|file| {
74 let old_mode = file.metadata()?.mode();
75 let is_executable = old_mode & 0o100 == 0o100;
76 let exe_bit_flip_works_in_filesystem = {
77 let toggled_exe_bit = old_mode ^ 0o100;
78 match file.set_permissions(PermissionsExt::from_mode(toggled_exe_bit)) {
79 Ok(()) => toggled_exe_bit == file.metadata()?.mode(),
80 Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => false,
81 Err(err) => return Err(err),
82 }
83 };
84 Ok(is_executable && exe_bit_flip_works_in_filesystem)
85 });
86 std::fs::remove_file(test_path)?;
87 res
88 }
89
90 #[cfg(not(unix))]
91 fn probe_file_mode(_root: &Path) -> std::io::Result<bool> {
92 Ok(false)
93 }
94
95 fn probe_ignore_case(git_dir: &Path) -> std::io::Result<bool> {
96 std::fs::metadata(git_dir.join("cOnFiG")).map(|_| true).or_else(|err| {
97 if err.kind() == std::io::ErrorKind::NotFound {
98 Ok(false)
99 } else {
100 Err(err)
101 }
102 })
103 }
104
105 fn probe_precompose_unicode(root: &Path) -> std::io::Result<bool> {
106 let rand = fastrand::usize(..);
107 let precomposed = format!("รค{rand}");
108 let decomposed = format!("a\u{308}{rand}");
109
110 let precomposed = root.join(precomposed);
111 std::fs::OpenOptions::new()
112 .create_new(true)
113 .write(true)
114 .open(&precomposed)?;
115 let res = root.join(decomposed).symlink_metadata().map(|_| true);
116 std::fs::remove_file(precomposed)?;
117 res
118 }
119
120 fn probe_symlink(root: &Path) -> std::io::Result<bool> {
121 let rand = fastrand::usize(..);
122 let link_path = root.join(format!("__file_link{rand}"));
123 if crate::symlink::create("dangling".as_ref(), &link_path).is_err() {
124 return Ok(false);
125 }
126
127 let res = std::fs::symlink_metadata(&link_path).map(|m| m.file_type().is_symlink());
128 crate::symlink::remove(&link_path).or_else(|_| std::fs::remove_file(&link_path))?;
129 res
130 }
131}