upstream_rs/services/integration/
permission_handler.rs1#[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#[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
41pub fn find_executable(directory_path: &Path, name: &str) -> Option<PathBuf> {
43 #[cfg(windows)]
44 let name = &format!("{}.exe", name);
45
46 let bin_path = directory_path.join("bin").join(name);
48 if bin_path.is_file() {
49 return Some(bin_path);
50 }
51
52 let direct_path = directory_path.join(name);
54 if direct_path.is_file() {
55 return Some(direct_path);
56 }
57
58 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 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 if let Some(path) = find_nested_executable(directory_path, name, true) {
83 return Some(path);
84 }
85
86 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}