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