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};
61
62        // test it exactly as we typically create executable files, not using chmod.
63        let rand = fastrand::usize(..);
64        let test_path = root.join(format!("_test_executable_bit{rand}"));
65        let res = std::fs::OpenOptions::new()
66            .create_new(true)
67            .write(true)
68            .mode(0o777)
69            .open(&test_path)
70            .and_then(|f| f.metadata().map(|m| m.mode() & 0o100 == 0o100));
71        std::fs::remove_file(test_path)?;
72        res
73    }
74
75    #[cfg(not(unix))]
76    fn probe_file_mode(_root: &Path) -> std::io::Result<bool> {
77        Ok(false)
78    }
79
80    fn probe_ignore_case(git_dir: &Path) -> std::io::Result<bool> {
81        std::fs::metadata(git_dir.join("cOnFiG")).map(|_| true).or_else(|err| {
82            if err.kind() == std::io::ErrorKind::NotFound {
83                Ok(false)
84            } else {
85                Err(err)
86            }
87        })
88    }
89
90    fn probe_precompose_unicode(root: &Path) -> std::io::Result<bool> {
91        let rand = fastrand::usize(..);
92        let precomposed = format!("รค{rand}");
93        let decomposed = format!("a\u{308}{rand}");
94
95        let precomposed = root.join(precomposed);
96        std::fs::OpenOptions::new()
97            .create_new(true)
98            .write(true)
99            .open(&precomposed)?;
100        let res = root.join(decomposed).symlink_metadata().map(|_| true);
101        std::fs::remove_file(precomposed)?;
102        res
103    }
104
105    fn probe_symlink(root: &Path) -> std::io::Result<bool> {
106        let rand = fastrand::usize(..);
107        let link_path = root.join(format!("__file_link{rand}"));
108        if crate::symlink::create("dangling".as_ref(), &link_path).is_err() {
109            return Ok(false);
110        }
111
112        let res = std::fs::symlink_metadata(&link_path).map(|m| m.file_type().is_symlink());
113        crate::symlink::remove(&link_path).or_else(|_| std::fs::remove_file(&link_path))?;
114        res
115    }
116}