tidy/
bins.rs

1//! Tidy check to ensure that there are no binaries checked into the source tree
2//! by accident.
3//!
4//! In the past we've accidentally checked in test binaries and such which add a
5//! huge amount of bloat to the Git history, so it's good to just ensure we
6//! don't do that again.
7
8pub use os_impl::*;
9
10// All files are executable on Windows, so just check on Unix.
11#[cfg(windows)]
12mod os_impl {
13    use std::path::Path;
14
15    pub fn check_filesystem_support(_sources: &[&Path], _output: &Path) -> bool {
16        return false;
17    }
18
19    pub fn check(_path: &Path, _bad: &mut bool) {}
20}
21
22#[cfg(unix)]
23mod os_impl {
24    use std::fs;
25    use std::os::unix::prelude::*;
26    use std::path::Path;
27    use std::process::{Command, Stdio};
28
29    use crate::walk::{filter_dirs, walk_no_read};
30
31    enum FilesystemSupport {
32        Supported,
33        Unsupported,
34        ReadOnlyFs,
35    }
36
37    use FilesystemSupport::*;
38
39    fn is_executable(path: &Path) -> std::io::Result<bool> {
40        Ok(path.metadata()?.mode() & 0o111 != 0)
41    }
42
43    pub fn check_filesystem_support(sources: &[&Path], output: &Path) -> bool {
44        // We want to avoid false positives on filesystems that do not support the
45        // executable bit. This occurs on some versions of Window's linux subsystem,
46        // for example.
47        //
48        // We try to create the temporary file first in the src directory, which is
49        // the preferred location as it's most likely to be on the same filesystem,
50        // and then in the output (`build`) directory if that fails. Sometimes we
51        // see the source directory mounted as read-only which means we can't
52        // readily create a file there to test.
53        //
54        // See #36706 and #74753 for context.
55
56        fn check_dir(dir: &Path) -> FilesystemSupport {
57            let path = dir.join("tidy-test-file");
58            match fs::File::create(&path) {
59                Ok(file) => {
60                    let exec = is_executable(&path).unwrap_or(false);
61                    drop(file);
62                    fs::remove_file(&path).expect("Deleted temp file");
63                    // If the file is executable, then we assume that this
64                    // filesystem does not track executability, so skip this check.
65                    if exec { Unsupported } else { Supported }
66                }
67                Err(e) => {
68                    // If the directory is read-only or we otherwise don't have rights,
69                    // just don't run this check.
70                    //
71                    // 30 is the "Read-only filesystem" code at least in one CI
72                    //    environment.
73                    if e.raw_os_error() == Some(30) {
74                        eprintln!("tidy: Skipping binary file check, read-only filesystem");
75                        return ReadOnlyFs;
76                    }
77
78                    panic!("unable to create temporary file `{:?}`: {:?}", path, e);
79                }
80            }
81        }
82
83        for &source_dir in sources {
84            match check_dir(source_dir) {
85                Unsupported => return false,
86                ReadOnlyFs => {
87                    return match check_dir(output) {
88                        Supported => true,
89                        _ => false,
90                    };
91                }
92                _ => {}
93            }
94        }
95
96        true
97    }
98
99    // FIXME: check when rust-installer test sh files will be removed,
100    // and then remove them from exclude list
101    const RI_EXCLUSION_LIST: &[&str] = &[
102        "src/tools/rust-installer/test/image1/bin/program",
103        "src/tools/rust-installer/test/image1/bin/program2",
104        "src/tools/rust-installer/test/image1/bin/bad-bin",
105        "src/tools/rust-installer/test/image2/bin/oldprogram",
106        "src/tools/rust-installer/test/image3/bin/cargo",
107    ];
108
109    fn filter_rust_installer_no_so_bins(path: &Path) -> bool {
110        RI_EXCLUSION_LIST.iter().any(|p| path.ends_with(p))
111    }
112
113    #[cfg(unix)]
114    pub fn check(path: &Path, bad: &mut bool) {
115        use std::ffi::OsStr;
116
117        const ALLOWED: &[&str] = &["configure", "x"];
118
119        for p in RI_EXCLUSION_LIST {
120            if !path.join(Path::new(p)).exists() {
121                tidy_error!(bad, "rust-installer test bins missed: {p}");
122            }
123        }
124
125        // FIXME: we don't need to look at all binaries, only files that have been modified in this branch
126        // (e.g. using `git ls-files`).
127        walk_no_read(
128            &[path],
129            |path, _is_dir| {
130                filter_dirs(path)
131                    || path.ends_with("src/etc")
132                    || filter_rust_installer_no_so_bins(path)
133            },
134            &mut |entry| {
135                let file = entry.path();
136                let extension = file.extension();
137                let scripts = ["py", "sh", "ps1", "woff2"];
138                if scripts.into_iter().any(|e| extension == Some(OsStr::new(e))) {
139                    return;
140                }
141
142                if t!(is_executable(&file), file) {
143                    let rel_path = file.strip_prefix(path).unwrap();
144                    let git_friendly_path = rel_path.to_str().unwrap().replace("\\", "/");
145
146                    if ALLOWED.contains(&git_friendly_path.as_str()) {
147                        return;
148                    }
149
150                    let output = Command::new("git")
151                        .arg("ls-files")
152                        .arg(&git_friendly_path)
153                        .current_dir(path)
154                        .stderr(Stdio::null())
155                        .output()
156                        .unwrap_or_else(|e| {
157                            panic!("could not run git ls-files: {e}");
158                        });
159                    let path_bytes = rel_path.as_os_str().as_bytes();
160                    if output.status.success() && output.stdout.starts_with(path_bytes) {
161                        tidy_error!(bad, "binary checked into source: {}", file.display());
162                    }
163                }
164            },
165        )
166    }
167}