gix_fs/
capabilities.rs

1// TODO: tests
2use 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    /// try to determine all values in this context by probing them in the given `git_dir`, which
44    /// should be on the file system the git repository is located on.
45    /// `git_dir` is a typical git repository, expected to be populated with the typical files like `config`.
46    ///
47    /// All errors are ignored and interpreted on top of the default for the platform the binary is compiled for.
48    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        // First check that we can create an executable file, then check that we
63        // can change the executable bit.
64        // The equivalent test by git itself is here:
65        // https://github.com/git/git/blob/f0ef5b6d9bcc258e4cbef93839d1b7465d5212b9/setup.c#L2367-L2379
66        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}