chrome_cli/chrome/
platform.rs1use std::path::PathBuf;
2
3use super::ChromeError;
4
5#[derive(Debug, Clone, Copy)]
7pub enum Channel {
8 Stable,
9 Canary,
10 Beta,
11 Dev,
12}
13
14pub fn find_chrome_executable(channel: Channel) -> Result<PathBuf, ChromeError> {
23 let env_override = std::env::var("CHROME_PATH").ok().map(PathBuf::from);
24 find_chrome_from(channel, env_override.as_deref())
25}
26
27fn find_chrome_from(
32 channel: Channel,
33 env_override: Option<&std::path::Path>,
34) -> Result<PathBuf, ChromeError> {
35 if let Some(p) = env_override {
36 if p.exists() {
37 return Ok(p.to_path_buf());
38 }
39 }
40
41 for candidate in chrome_candidates(channel) {
42 if candidate.exists() {
43 return Ok(candidate);
44 }
45 }
46
47 Err(ChromeError::NotFound(format!(
48 "could not find Chrome ({channel:?} channel). Use --chrome-path to specify the executable"
49 )))
50}
51
52#[must_use]
54pub fn default_user_data_dir() -> Option<PathBuf> {
55 #[cfg(target_os = "macos")]
56 {
57 home_dir().map(|h| h.join("Library/Application Support/Google/Chrome"))
58 }
59
60 #[cfg(target_os = "linux")]
61 {
62 home_dir().map(|h| h.join(".config/google-chrome"))
63 }
64
65 #[cfg(target_os = "windows")]
66 {
67 std::env::var("LOCALAPPDATA").ok().map(|d| {
68 PathBuf::from(d)
69 .join("Google")
70 .join("Chrome")
71 .join("User Data")
72 })
73 }
74
75 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
76 {
77 None
78 }
79}
80
81#[cfg(any(target_os = "macos", target_os = "linux"))]
82fn home_dir() -> Option<PathBuf> {
83 std::env::var("HOME").ok().map(PathBuf::from)
84}
85
86fn chrome_candidates(channel: Channel) -> Vec<PathBuf> {
88 #[cfg(target_os = "macos")]
89 {
90 macos_candidates(channel)
91 }
92
93 #[cfg(target_os = "linux")]
94 {
95 linux_candidates(channel)
96 }
97
98 #[cfg(target_os = "windows")]
99 {
100 windows_candidates(channel)
101 }
102
103 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
104 {
105 let _ = channel;
106 vec![]
107 }
108}
109
110#[cfg(target_os = "macos")]
111fn macos_candidates(channel: Channel) -> Vec<PathBuf> {
112 match channel {
113 Channel::Stable => vec![
114 PathBuf::from("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
115 PathBuf::from("/Applications/Chromium.app/Contents/MacOS/Chromium"),
116 ],
117 Channel::Canary => vec![PathBuf::from(
118 "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
119 )],
120 Channel::Beta => vec![PathBuf::from(
121 "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
122 )],
123 Channel::Dev => vec![PathBuf::from(
124 "/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev",
125 )],
126 }
127}
128
129#[cfg(target_os = "linux")]
130fn linux_candidates(channel: Channel) -> Vec<PathBuf> {
131 let path_dirs: Vec<PathBuf> = std::env::var("PATH")
132 .unwrap_or_default()
133 .split(':')
134 .map(PathBuf::from)
135 .collect();
136
137 let names: &[&str] = match channel {
138 Channel::Stable => &[
139 "google-chrome",
140 "google-chrome-stable",
141 "chromium-browser",
142 "chromium",
143 ],
144 Channel::Canary => &["google-chrome-canary"],
145 Channel::Beta => &["google-chrome-beta"],
146 Channel::Dev => &["google-chrome-unstable"],
147 };
148
149 let mut candidates = Vec::new();
150 for name in names {
151 for dir in &path_dirs {
152 candidates.push(dir.join(name));
153 }
154 }
155 candidates
156}
157
158#[cfg(target_os = "windows")]
159fn windows_candidates(channel: Channel) -> Vec<PathBuf> {
160 let program_files = std::env::var("ProgramFiles").unwrap_or_default();
161 let program_files_x86 = std::env::var("ProgramFiles(x86)").unwrap_or_default();
162 let local_app_data = std::env::var("LOCALAPPDATA").unwrap_or_default();
163
164 match channel {
165 Channel::Stable => vec![
166 PathBuf::from(&program_files).join("Google/Chrome/Application/chrome.exe"),
167 PathBuf::from(&program_files_x86).join("Google/Chrome/Application/chrome.exe"),
168 ],
169 Channel::Canary => {
170 vec![PathBuf::from(&local_app_data).join("Google/Chrome SxS/Application/chrome.exe")]
171 }
172 Channel::Beta => vec![
173 PathBuf::from(&program_files).join("Google/Chrome Beta/Application/chrome.exe"),
174 PathBuf::from(&program_files_x86).join("Google/Chrome Beta/Application/chrome.exe"),
175 ],
176 Channel::Dev => vec![
177 PathBuf::from(&program_files).join("Google/Chrome Dev/Application/chrome.exe"),
178 PathBuf::from(&program_files_x86).join("Google/Chrome Dev/Application/chrome.exe"),
179 ],
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn default_user_data_dir_returns_some() {
189 let dir = default_user_data_dir();
191 assert!(dir.is_some(), "Expected a default user data directory");
192 }
193
194 #[test]
195 fn chrome_candidates_is_not_empty() {
196 let candidates = chrome_candidates(Channel::Stable);
197 assert!(
198 !candidates.is_empty(),
199 "Expected at least one candidate path"
200 );
201 }
202
203 #[test]
204 fn chrome_path_override_existing_file() {
205 let exe = std::env::current_exe().unwrap();
207 let result = find_chrome_from(Channel::Stable, Some(&exe));
208 assert_eq!(result.unwrap(), exe);
209 }
210
211 #[test]
212 fn chrome_path_override_nonexistent_is_skipped() {
213 let fake = std::path::Path::new("/nonexistent/chrome-test-binary");
214 let result = find_chrome_from(Channel::Stable, Some(fake));
215 if let Ok(path) = &result {
218 assert_ne!(path.as_path(), fake);
219 }
220 }
221}