seher/browser/
detector.rs1use super::types::{BrowserType, Profile};
2use std::path::{Path, PathBuf};
3
4pub struct BrowserDetector {
5 home_dir: PathBuf,
6}
7
8impl BrowserDetector {
9 #[must_use]
10 pub fn new() -> Self {
11 let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
12 Self { home_dir }
13 }
14
15 #[must_use]
16 pub fn detect_browsers(&self) -> Vec<BrowserType> {
17 let mut browsers = Vec::new();
18
19 for browser_type in [
20 BrowserType::Chrome,
21 BrowserType::Edge,
22 BrowserType::Brave,
23 BrowserType::Chromium,
24 BrowserType::Vivaldi,
25 BrowserType::Comet,
26 BrowserType::Dia,
27 BrowserType::Atlas,
28 BrowserType::Firefox,
29 BrowserType::Safari,
30 ] {
31 if self.is_browser_installed(browser_type) {
32 browsers.push(browser_type);
33 }
34 }
35
36 browsers
37 }
38
39 fn is_browser_installed(&self, browser_type: BrowserType) -> bool {
40 self.get_browser_base_path(browser_type)
41 .is_some_and(|p| p.exists())
42 }
43
44 #[must_use]
45 pub fn get_browser_base_path(&self, browser_type: BrowserType) -> Option<PathBuf> {
46 #[cfg(target_os = "macos")]
47 {
48 let path = match browser_type {
49 BrowserType::Chrome => {
50 self.home_dir.join("Library/Application Support/Google/Chrome")
51 }
52 BrowserType::Edge => {
53 self.home_dir.join("Library/Application Support/Microsoft Edge")
54 }
55 BrowserType::Brave => {
56 self.home_dir.join("Library/Application Support/BraveSoftware/Brave-Browser")
57 }
58 BrowserType::Chromium => {
59 self.home_dir.join("Library/Application Support/Chromium")
60 }
61 BrowserType::Vivaldi => {
62 self.home_dir.join("Library/Application Support/Vivaldi")
63 }
64 BrowserType::Comet => {
65 self.home_dir.join("Library/Application Support/Comet")
66 }
67 BrowserType::Dia => {
68 self.home_dir.join("Library/Application Support/Dia")
69 }
70 BrowserType::Atlas => {
71 self.home_dir.join("Library/Application Support/Atlas")
72 }
73 BrowserType::Firefox => {
74 self.home_dir.join("Library/Application Support/Firefox")
75 }
76 BrowserType::Safari => {
77 self.home_dir.join("Library/Containers/com.apple.Safari/Data/Library/Cookies/Cookies.binarycookies")
78 }
79 };
80 Some(path)
81 }
82
83 #[cfg(target_os = "linux")]
84 {
85 let config_dir = self.home_dir.join(".config");
86 let path = match browser_type {
87 BrowserType::Chrome => config_dir.join("google-chrome"),
88 BrowserType::Edge => config_dir.join("microsoft-edge"),
89 BrowserType::Brave => config_dir.join("BraveSoftware/Brave-Browser"),
90 BrowserType::Chromium => config_dir.join("chromium"),
91 BrowserType::Vivaldi => config_dir.join("vivaldi"),
92 BrowserType::Comet => config_dir.join("comet"),
93 BrowserType::Dia => config_dir.join("dia"),
94 BrowserType::Atlas => config_dir.join("atlas"),
95 BrowserType::Firefox => self.home_dir.join(".mozilla/firefox"),
96 BrowserType::Safari => return None,
97 };
98 Some(path)
99 }
100
101 #[cfg(target_os = "windows")]
102 {
103 let local_app_data = std::env::var("LOCALAPPDATA").ok()?;
104 let app_data = std::env::var("APPDATA").ok()?;
105 let base = PathBuf::from(local_app_data);
106 let roaming_base = PathBuf::from(app_data);
107 let path = match browser_type {
108 BrowserType::Chrome => base.join("Google\\Chrome\\User Data"),
109 BrowserType::Edge => base.join("Microsoft\\Edge\\User Data"),
110 BrowserType::Brave => base.join("BraveSoftware\\Brave-Browser\\User Data"),
111 BrowserType::Chromium => base.join("Chromium\\User Data"),
112 BrowserType::Vivaldi => base.join("Vivaldi\\User Data"),
113 BrowserType::Comet => base.join("Comet\\User Data"),
114 BrowserType::Dia => base.join("Dia\\User Data"),
115 BrowserType::Atlas => base.join("Atlas\\User Data"),
116 BrowserType::Firefox => roaming_base.join("Mozilla\\Firefox"),
117 BrowserType::Safari => return None,
118 };
119 Some(path)
120 }
121
122 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
123 None
124 }
125
126 #[must_use]
127 pub fn list_profiles(&self, browser_type: BrowserType) -> Vec<Profile> {
128 let base_path = match self.get_browser_base_path(browser_type) {
129 Some(p) if p.exists() => p,
130 _ => return Vec::new(),
131 };
132
133 if browser_type == BrowserType::Firefox {
134 return Self::list_firefox_profiles(&base_path);
135 }
136
137 if browser_type == BrowserType::Safari {
138 return vec![Profile::new(
139 "Default".to_string(),
140 base_path,
141 BrowserType::Safari,
142 )];
143 }
144
145 let mut profiles = Vec::new();
146
147 if let Ok(entries) = std::fs::read_dir(&base_path) {
148 for entry in entries.flatten() {
149 let path = entry.path();
150 if !path.is_dir() {
151 continue;
152 }
153
154 let file_name = entry.file_name();
155 let name = file_name.to_string_lossy();
156
157 if name == "Default" || name.starts_with("Profile ") {
158 let cookies_path = path.join("Cookies");
159 if cookies_path.exists() {
160 let profile_name = if name == "Default" {
161 "Default".to_string()
162 } else {
163 name.to_string()
164 };
165 profiles.push(Profile::new(profile_name, path, browser_type));
166 }
167 }
168 }
169 }
170
171 profiles.sort_by(|a, b| {
172 if a.name == "Default" {
173 std::cmp::Ordering::Less
174 } else if b.name == "Default" {
175 std::cmp::Ordering::Greater
176 } else {
177 a.name.cmp(&b.name)
178 }
179 });
180
181 profiles
182 }
183
184 fn list_firefox_profiles(base_path: &Path) -> Vec<Profile> {
185 let profiles_ini = base_path.join("profiles.ini");
186 if !profiles_ini.exists() {
187 return Vec::new();
188 }
189
190 let mut profiles = Vec::new();
191
192 if let Ok(content) = std::fs::read_to_string(&profiles_ini) {
193 let mut current_profile_name = None;
194 let mut current_profile_path = None;
195 let mut current_is_relative = true;
196
197 for line in content.lines() {
198 let line = line.trim();
199
200 if line.starts_with("[Profile") {
201 if let (Some(name), Some(path)) =
202 (current_profile_name.take(), current_profile_path.take())
203 {
204 let full_path = if current_is_relative {
205 base_path.join(path)
206 } else {
207 PathBuf::from(path)
208 };
209
210 if full_path.join("cookies.sqlite").exists() {
211 profiles.push(Profile::new(name, full_path, BrowserType::Firefox));
212 }
213 }
214 current_is_relative = true;
215 } else if let Some(stripped) = line.strip_prefix("Name=") {
216 current_profile_name = Some(stripped.to_string());
217 } else if let Some(stripped) = line.strip_prefix("Path=") {
218 current_profile_path = Some(stripped.to_string());
219 } else if let Some(stripped) = line.strip_prefix("IsRelative=") {
220 current_is_relative = stripped.trim() == "1";
221 }
222 }
223
224 if let (Some(name), Some(path)) = (current_profile_name, current_profile_path) {
225 let full_path = if current_is_relative {
226 base_path.join(path)
227 } else {
228 PathBuf::from(path)
229 };
230
231 if full_path.join("cookies.sqlite").exists() {
232 profiles.push(Profile::new(name, full_path, BrowserType::Firefox));
233 }
234 }
235 }
236
237 profiles
238 }
239
240 #[must_use]
241 pub fn get_profile(
242 &self,
243 browser_type: BrowserType,
244 profile_name: Option<&str>,
245 ) -> Option<Profile> {
246 let profiles = self.list_profiles(browser_type);
247
248 match profile_name {
249 Some(name) => profiles.into_iter().find(|p| p.name == name),
250 None => profiles.into_iter().next(),
251 }
252 }
253}
254
255impl Default for BrowserDetector {
256 fn default() -> Self {
257 Self::new()
258 }
259}