Skip to main content

seher/browser/
detector.rs

1use 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}