Skip to main content

upstream_rs/services/integration/
permission_handler.rs

1#[cfg(unix)]
2use anyhow::Context;
3use anyhow::Result;
4
5#[cfg(unix)]
6use std::os::unix::fs::PermissionsExt;
7
8use std::path::Path;
9use std::{fs, path::PathBuf};
10
11/// Sets executable permissions on a file for user, group, and others.
12#[cfg(unix)]
13pub fn make_executable(exec_path: &Path) -> Result<()> {
14    if !exec_path.exists() {
15        anyhow::bail!("Invalid executable path: {}", exec_path.to_string_lossy());
16    }
17
18    match fs::metadata(exec_path) {
19        Ok(metadata) => {
20            let mut permissions = metadata.permissions();
21            let mode = permissions.mode();
22
23            permissions.set_mode(mode | 0o111);
24
25            fs::set_permissions(exec_path, permissions)
26                .context("Failed to set executable permissions")?;
27        }
28        Err(e) => {
29            return Err(e).context("Failed to read metadata");
30        }
31    }
32
33    Ok(())
34}
35
36#[cfg(windows)]
37pub fn make_executable(_exec_path: &Path) -> Result<()> {
38    Ok(())
39}
40
41/// Finds any potential executables in a directory.
42pub fn find_executable(directory_path: &Path, name: &str) -> Option<PathBuf> {
43    #[cfg(windows)]
44    let name = &format!("{}.exe", name);
45
46    // 1. bin/<name>
47    let bin_path = directory_path.join("bin").join(name);
48    if bin_path.is_file() {
49        return Some(bin_path);
50    }
51
52    // 2. directoryPath/<name>
53    let direct_path = directory_path.join(name);
54    if direct_path.is_file() {
55        return Some(direct_path);
56    }
57
58    // 3. directory name is the executable name
59    //    e.g. cool-app-x86_64/cool-app-x86_64
60    if let Some(dir_name) = directory_path.file_name() {
61        let derived_path = directory_path.join(dir_name);
62        if derived_path.is_file() {
63            return Some(derived_path);
64        }
65    }
66
67    // 4. As a fallback, search for any file starting with name
68    //    e.g. "cool-app" -> "cool-app-x86_64", "cool-app-v1"
69    if let Ok(entries) = fs::read_dir(directory_path) {
70        for entry in entries.flatten() {
71            if let Ok(file_type) = entry.file_type()
72                && file_type.is_file()
73                && let Some(file_name) = entry.file_name().to_str()
74                && file_name.to_lowercase().starts_with(&name.to_lowercase())
75            {
76                return Some(entry.path());
77            }
78        }
79    }
80
81    // 5. Handle nested layouts such as "<tool>-linux/<arch>/<tool>".
82    if let Some(path) = find_nested_executable(directory_path, name, true) {
83        return Some(path);
84    }
85
86    // 6. Final fallback: any nested exact-name match up to limited depth.
87    if let Some(path) = find_nested_executable(directory_path, name, false) {
88        return Some(path);
89    }
90
91    None
92}
93
94fn find_nested_executable(root: &Path, name: &str, prefer_arch_paths: bool) -> Option<PathBuf> {
95    let mut stack: Vec<(PathBuf, usize)> = vec![(root.to_path_buf(), 0)];
96    let arch_markers = current_arch_markers();
97    let target_name = name.to_ascii_lowercase();
98
99    while let Some((dir, depth)) = stack.pop() {
100        if depth > 3 {
101            continue;
102        }
103
104        let Ok(entries) = fs::read_dir(&dir) else {
105            continue;
106        };
107
108        for entry in entries.flatten() {
109            let path = entry.path();
110            let Ok(file_type) = entry.file_type() else {
111                continue;
112            };
113
114            if file_type.is_dir() {
115                stack.push((path, depth + 1));
116                continue;
117            }
118
119            if !file_type.is_file() {
120                continue;
121            }
122
123            let Some(file_name) = path.file_name().and_then(|n| n.to_str()) else {
124                continue;
125            };
126
127            if file_name.to_ascii_lowercase() != target_name {
128                continue;
129            }
130
131            if !prefer_arch_paths {
132                return Some(path);
133            }
134
135            let contains_arch_marker = path.components().any(|component| {
136                let s = component.as_os_str().to_string_lossy().to_ascii_lowercase();
137                arch_markers.iter().any(|marker| s == *marker)
138            });
139
140            if contains_arch_marker {
141                return Some(path);
142            }
143        }
144    }
145
146    None
147}
148
149fn current_arch_markers() -> &'static [&'static str] {
150    #[cfg(target_arch = "x86_64")]
151    return &["x86_64", "amd64", "x64"];
152    #[cfg(target_arch = "x86")]
153    return &["x86", "i386", "i686", "x86_32"];
154    #[cfg(target_arch = "aarch64")]
155    return &["aarch64", "arm64"];
156    #[cfg(target_arch = "arm")]
157    return &["arm", "armv7", "armv6"];
158    #[cfg(target_arch = "riscv64")]
159    return &["riscv64"];
160    #[cfg(target_arch = "powerpc")]
161    return &["powerpc", "ppc"];
162    #[cfg(target_arch = "powerpc64")]
163    return &["powerpc64", "ppc64"];
164    #[cfg(target_arch = "s390x")]
165    return &["s390x"];
166    #[cfg(not(any(
167        target_arch = "x86_64",
168        target_arch = "x86",
169        target_arch = "aarch64",
170        target_arch = "arm",
171        target_arch = "riscv64",
172        target_arch = "powerpc",
173        target_arch = "powerpc64",
174        target_arch = "s390x"
175    )))]
176    return &[];
177}
178
179#[cfg(test)]
180mod tests {
181    use super::find_executable;
182    use std::path::{Path, PathBuf};
183    use std::time::{SystemTime, UNIX_EPOCH};
184    use std::{fs, io};
185
186    fn temp_root(name: &str) -> PathBuf {
187        let nanos = SystemTime::now()
188            .duration_since(UNIX_EPOCH)
189            .map(|d| d.as_nanos())
190            .unwrap_or(0);
191        std::env::temp_dir().join(format!("upstream-perm-test-{name}-{nanos}"))
192    }
193
194    fn cleanup(path: &Path) -> io::Result<()> {
195        fs::remove_dir_all(path)
196    }
197
198    fn executable_name(base: &str) -> String {
199        #[cfg(windows)]
200        {
201            format!("{base}.exe")
202        }
203        #[cfg(not(windows))]
204        {
205            base.to_string()
206        }
207    }
208
209    #[test]
210    fn finds_nested_arch_layout_executable() {
211        let root = temp_root("nested-arch");
212        let install_root = root.join("minisign-0.12-linux");
213        let arch_dir = install_root.join("minisign-linux").join("x86_64");
214        fs::create_dir_all(&arch_dir).expect("create nested dirs");
215        let executable = executable_name("minisign");
216        fs::write(arch_dir.join(&executable), b"#!/bin/sh\n").expect("write executable");
217
218        let found = find_executable(&install_root, "minisign").expect("find executable");
219        assert!(found.ends_with(Path::new("minisign-linux").join("x86_64").join(executable)));
220
221        cleanup(&root).expect("cleanup");
222    }
223
224    #[test]
225    fn still_finds_direct_binary_first() {
226        let root = temp_root("direct");
227        fs::create_dir_all(&root).expect("create root");
228        let executable = executable_name("tool");
229        fs::write(root.join(&executable), b"#!/bin/sh\n").expect("write executable");
230
231        let found = find_executable(&root, "tool").expect("find executable");
232        assert!(found.ends_with(Path::new(&executable)));
233
234        cleanup(&root).expect("cleanup");
235    }
236}