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