1use std::env;
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, Clone)]
5pub struct DetectionOptions {
6 pub msedge: bool,
8 pub unstable: bool,
10 pub prefer_msedge: bool,
18}
19
20impl Default for DetectionOptions {
21 fn default() -> Self {
22 Self {
23 msedge: true,
24 unstable: false,
25 prefer_msedge: false,
26 }
27 }
28}
29
30pub fn default_executable(options: DetectionOptions) -> Result<std::path::PathBuf, String> {
45 if let Some(path) = get_by_env_var(&options) {
46 return Ok(path);
47 }
48
49 if options.msedge && options.prefer_msedge {
50 if let Some(path) = get_edge(&options) {
51 return Ok(path);
52 }
53 }
54
55 if let Some(path) = get_by_name(&options) {
56 return Ok(path);
57 }
58
59 #[cfg(windows)]
60 if let Some(path) = get_by_registry(&options) {
61 return Ok(path);
62 }
63
64 if let Some(path) = get_by_path(&options) {
65 return Ok(path);
66 }
67
68 Err("Could not auto detect a chrome executable".to_string())
69}
70
71fn get_edge(options: &DetectionOptions) -> Option<PathBuf> {
75 if let Some(path) = get_edge_by_name(options) {
76 return Some(path);
77 }
78
79 #[cfg(windows)]
80 if let Some(path) = get_edge_by_registry() {
81 return Some(path);
82 }
83
84 search_paths(&edge_path_candidates(options))
85}
86
87fn env_file(var: &str) -> Option<PathBuf> {
90 let path = env::var(var).ok()?;
91 if Path::new(&path).is_file() {
92 Some(path.into())
93 } else {
94 None
95 }
96}
97
98fn get_by_env_var(options: &DetectionOptions) -> Option<PathBuf> {
99 let chrome = || {
101 let path = env::var("CHROME").ok()?;
102 if Path::new(&path).exists() {
103 Some(PathBuf::from(path))
104 } else {
105 None
106 }
107 };
108 let edge = || {
109 if options.msedge {
110 env_file("MSEDGE").or_else(|| env_file("EDGE"))
111 } else {
112 None
113 }
114 };
115
116 if options.msedge && options.prefer_msedge {
117 edge().or_else(chrome)
118 } else {
119 chrome().or_else(edge)
120 }
121}
122
123#[cfg(feature = "auto-detect-executable")]
124fn chrome_name_candidates(options: &DetectionOptions) -> [(&'static str, bool); 9] {
125 [
126 ("chrome", true),
127 ("chrome-browser", true),
128 ("google-chrome-stable", true),
129 ("google-chrome-beta", options.unstable),
130 ("google-chrome-dev", options.unstable),
131 ("google-chrome-unstable", options.unstable),
132 ("chromium", true),
133 ("chromium-browser", true),
134 ("brave", true),
135 ]
136}
137
138#[cfg(feature = "auto-detect-executable")]
139fn edge_name_candidates(options: &DetectionOptions) -> [(&'static str, bool); 5] {
140 [
141 ("msedge", options.msedge),
142 ("microsoft-edge", options.msedge),
143 ("microsoft-edge-stable", options.msedge),
144 ("microsoft-edge-beta", options.msedge && options.unstable),
145 ("microsoft-edge-dev", options.msedge && options.unstable),
146 ]
147}
148
149#[cfg(feature = "auto-detect-executable")]
156fn name_candidates(options: &DetectionOptions) -> Vec<(&'static str, bool)> {
157 let chrome_apps = chrome_name_candidates(options);
158 let edge_apps = edge_name_candidates(options);
159
160 if options.prefer_msedge {
161 edge_apps.into_iter().chain(chrome_apps).collect()
162 } else {
163 chrome_apps.into_iter().chain(edge_apps).collect()
164 }
165}
166
167#[cfg(feature = "auto-detect-executable")]
168fn which_first(candidates: impl IntoIterator<Item = (&'static str, bool)>) -> Option<PathBuf> {
169 for (app, allowed) in candidates {
170 if !allowed {
171 continue;
172 }
173 if let Ok(path) = which::which(app) {
174 return Some(path);
175 }
176 }
177
178 None
179}
180
181#[cfg(feature = "auto-detect-executable")]
182fn get_by_name(options: &DetectionOptions) -> Option<PathBuf> {
183 which_first(name_candidates(options))
184}
185
186#[cfg(not(feature = "auto-detect-executable"))]
187fn get_by_name(_options: &DetectionOptions) -> Option<PathBuf> {
188 None
189}
190
191#[cfg(feature = "auto-detect-executable")]
192fn get_edge_by_name(options: &DetectionOptions) -> Option<PathBuf> {
193 which_first(edge_name_candidates(options))
194}
195
196#[cfg(not(feature = "auto-detect-executable"))]
197fn get_edge_by_name(_options: &DetectionOptions) -> Option<PathBuf> {
198 None
199}
200
201#[allow(unused_variables)]
202fn chrome_path_candidates(options: &DetectionOptions) -> Vec<(&'static str, bool)> {
203 #[cfg(all(unix, not(target_os = "macos")))]
204 return vec![
205 ("/opt/chromium.org/chromium", true),
206 ("/opt/google/chrome", true),
207 ("/tmp/aws/lib", true),
209 ];
210
211 #[cfg(windows)]
212 return Vec::new();
213
214 #[cfg(target_os = "macos")]
215 return vec![
216 (
217 "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
218 true,
219 ),
220 (
221 "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
222 options.unstable,
223 ),
224 (
225 "/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev",
226 options.unstable,
227 ),
228 (
229 "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
230 options.unstable,
231 ),
232 ("/Applications/Chromium.app/Contents/MacOS/Chromium", true),
233 ];
234}
235
236#[allow(unused_variables)]
237fn edge_path_candidates(options: &DetectionOptions) -> Vec<(&'static str, bool)> {
238 #[cfg(all(unix, not(target_os = "macos")))]
239 return Vec::new();
240
241 #[cfg(windows)]
242 return vec![
243 (
245 r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
246 options.msedge,
247 ),
248 (
249 r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
250 options.msedge,
251 ),
252 ];
253
254 #[cfg(target_os = "macos")]
255 return vec![
256 (
257 "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
258 options.msedge,
259 ),
260 (
261 "/Applications/Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta",
262 options.msedge && options.unstable,
263 ),
264 (
265 "/Applications/Microsoft Edge Dev.app/Contents/MacOS/Microsoft Edge Dev",
266 options.msedge && options.unstable,
267 ),
268 (
269 "/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary",
270 options.msedge && options.unstable,
271 ),
272 ];
273}
274
275fn search_paths(paths: &[(&str, bool)]) -> Option<PathBuf> {
276 for &(path, allowed) in paths {
277 if !allowed {
278 continue;
279 }
280 if Path::new(path).exists() {
281 return Some(path.into());
282 }
283 }
284
285 None
286}
287
288fn get_by_path(options: &DetectionOptions) -> Option<PathBuf> {
289 let chrome_paths = chrome_path_candidates(options);
290 let edge_paths = edge_path_candidates(options);
291
292 if options.prefer_msedge {
293 search_paths(&edge_paths).or_else(|| search_paths(&chrome_paths))
294 } else {
295 search_paths(&chrome_paths).or_else(|| search_paths(&edge_paths))
296 }
297}
298
299#[cfg(windows)]
302fn registry_app_path(exe: &str) -> Option<PathBuf> {
303 let subkey = format!("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\{exe}");
304
305 winreg::RegKey::predef(winreg::enums::HKEY_LOCAL_MACHINE)
306 .open_subkey(&subkey)
307 .or_else(|_| winreg::RegKey::predef(winreg::enums::HKEY_CURRENT_USER).open_subkey(&subkey))
308 .and_then(|key| key.get_value::<String, _>(""))
309 .map(PathBuf::from)
310 .ok()
311}
312
313#[cfg(windows)]
316fn get_edge_by_registry() -> Option<PathBuf> {
317 registry_app_path("msedge.exe").filter(|path| path.exists())
318}
319
320#[cfg(windows)]
321fn get_by_registry(options: &DetectionOptions) -> Option<PathBuf> {
322 let chrome = || registry_app_path("chrome.exe");
323 let edge = || {
324 if options.msedge {
325 get_edge_by_registry()
326 } else {
327 None
328 }
329 };
330
331 if options.prefer_msedge {
332 edge().or_else(chrome)
333 } else {
334 chrome().or_else(edge)
335 }
336}
337
338#[cfg(all(test, feature = "auto-detect-executable"))]
339mod tests {
340 use super::*;
341
342 fn names(options: &DetectionOptions) -> Vec<&'static str> {
343 name_candidates(options)
344 .into_iter()
345 .map(|(name, _)| name)
346 .collect()
347 }
348
349 #[test]
352 fn default_order_checks_chrome_before_edge() {
353 let order = names(&DetectionOptions::default());
354 let chrome = order.iter().position(|n| *n == "chrome").unwrap();
355 let edge = order.iter().position(|n| *n == "msedge").unwrap();
356 assert!(chrome < edge);
357 assert_eq!(order.first(), Some(&"chrome"));
358 }
359
360 #[test]
362 fn prefer_msedge_checks_edge_before_chrome() {
363 let options = DetectionOptions {
364 msedge: true,
365 unstable: false,
366 prefer_msedge: true,
367 };
368 let order = names(&options);
369 let chrome = order.iter().position(|n| *n == "chrome").unwrap();
370 let edge = order.iter().position(|n| *n == "msedge").unwrap();
371 assert!(edge < chrome);
372 assert_eq!(order.first(), Some(&"msedge"));
373 }
374
375 #[test]
378 fn prefer_msedge_keeps_all_candidates() {
379 let base = DetectionOptions {
380 msedge: true,
381 unstable: true,
382 prefer_msedge: false,
383 };
384 let preferred = DetectionOptions {
385 prefer_msedge: true,
386 ..base.clone()
387 };
388
389 let mut base_sorted = names(&base);
390 let mut preferred_sorted = names(&preferred);
391 base_sorted.sort_unstable();
392 preferred_sorted.sort_unstable();
393
394 assert_eq!(base_sorted, preferred_sorted);
395 assert!(preferred_sorted.contains(&"chrome"));
396 assert!(preferred_sorted.contains(&"brave"));
397 }
398
399 #[test]
402 fn edge_candidates_respect_msedge_flag() {
403 let enabled = DetectionOptions {
404 msedge: true,
405 unstable: false,
406 prefer_msedge: true,
407 };
408 assert!(edge_name_candidates(&enabled)
409 .iter()
410 .all(|(name, _)| name.contains("edge")));
411
412 let disabled = DetectionOptions {
413 msedge: false,
414 unstable: true,
415 prefer_msedge: false,
416 };
417 assert!(edge_name_candidates(&disabled)
418 .iter()
419 .all(|(_, allowed)| !allowed));
420 assert!(edge_path_candidates(&disabled)
421 .iter()
422 .all(|(_, allowed)| !allowed));
423 }
424
425 #[test]
428 #[cfg(windows)]
429 fn windows_edge_paths_include_64_bit_install() {
430 let paths: Vec<&str> = edge_path_candidates(&DetectionOptions::default())
431 .into_iter()
432 .map(|(path, _)| path)
433 .collect();
434 assert_eq!(
435 paths.first(),
436 Some(&r"C:\Program Files\Microsoft\Edge\Application\msedge.exe")
437 );
438 assert!(paths.contains(&r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"));
439 }
440}