1use std::collections::HashMap;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7use rusqlite::Connection;
8
9use crate::error::{Error, Result};
10use crate::types::Profile;
11
12fn firefox_dir() -> Result<PathBuf> {
21 let home = dirs::home_dir().ok_or(Error::FirefoxDirNotFound)?;
22 let firefox = home.join("Library/Application Support/Firefox");
23 if firefox.is_dir() {
24 Ok(firefox)
25 } else {
26 Err(Error::FirefoxDirNotFound)
27 }
28}
29
30pub fn list_profiles() -> Result<Vec<Profile>> {
41 let firefox = firefox_dir()?;
42 let groups_dir = firefox.join("Profile Groups");
43
44 if !groups_dir.is_dir() {
45 return Err(Error::NoProfileGroups);
46 }
47
48 let entries = fs::read_dir(&groups_dir)?;
50 let mut sqlite_files: Vec<PathBuf> = Vec::new();
51 for entry in entries {
52 let entry = entry?;
53 let path = entry.path();
54 if path.extension().is_some_and(|ext| ext == "sqlite") {
55 sqlite_files.push(path);
56 }
57 }
58
59 if sqlite_files.is_empty() {
60 return Err(Error::NoProfileGroups);
61 }
62
63 let mut profiles_by_path: HashMap<PathBuf, Profile> = HashMap::new();
65
66 for db_path in &sqlite_files {
67 if let Ok(conn) = Connection::open(db_path)
68 && let Ok(mut stmt) = conn.prepare("SELECT name, path FROM Profiles")
69 {
70 let rows = stmt.query_map([], |row| {
71 let name: String = row.get(0)?;
72 let rel_path: String = row.get(1)?;
73 Ok((name, rel_path))
74 });
75
76 if let Ok(rows) = rows {
77 for row in rows.flatten() {
78 let (name, rel_path) = row;
79 let abs_path = firefox.join(&rel_path);
81
82 if abs_path.join("places.sqlite").exists() {
84 profiles_by_path.insert(
85 abs_path.clone(),
86 Profile {
87 name,
88 path: abs_path,
89 },
90 );
91 }
92 }
93 }
94 }
95 }
96
97 if profiles_by_path.is_empty() {
98 return Err(Error::NoProfileGroups);
99 }
100
101 let mut profiles: Vec<Profile> = profiles_by_path.into_values().collect();
103 profiles.sort_by(|a, b| a.name.cmp(&b.name));
104
105 Ok(profiles)
106}
107
108pub fn find_profile(name: &str) -> Result<Profile> {
115 let profiles = list_profiles()?;
116 let name_lower = name.to_lowercase();
117
118 let matches: Vec<&Profile> = profiles
119 .iter()
120 .filter(|p| p.name.to_lowercase() == name_lower)
121 .collect();
122
123 match matches.len() {
124 0 => Err(Error::ProfileNotFound {
125 name: name.to_string(),
126 }),
127 1 => Ok(matches[0].clone()),
128 _ => Err(Error::AmbiguousProfile {
129 name: name.to_string(),
130 matches: matches.iter().map(|p| p.name.clone()).collect(),
131 }),
132 }
133}
134
135pub fn check_firefox_not_running(profile_path: &Path) -> Result<()> {
144 let parentlock = profile_path.join(".parentlock");
146 if parentlock.exists() {
147 let places = profile_path.join("places.sqlite");
151 if places.exists() {
152 match Connection::open_with_flags(
154 &places,
155 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
156 ) {
157 Ok(conn) => {
158 match conn.execute_batch("SELECT 1 FROM moz_bookmarks LIMIT 1") {
160 Ok(()) => Ok(()),
161 Err(e) => {
162 if is_busy_error(&e) {
163 Err(Error::FirefoxRunning)
164 } else {
165 Err(Error::Sqlite(e))
166 }
167 }
168 }
169 }
170 Err(e) => {
171 if is_busy_error(&e) {
172 Err(Error::FirefoxRunning)
173 } else {
174 Err(Error::Sqlite(e))
175 }
176 }
177 }
178 } else {
179 Ok(())
180 }
181 } else {
182 Ok(())
183 }
184}
185
186fn is_busy_error(e: &rusqlite::Error) -> bool {
188 matches!(
189 e,
190 rusqlite::Error::SqliteFailure(
191 rusqlite::ffi::Error {
192 code: rusqlite::ffi::ErrorCode::DatabaseBusy
193 | rusqlite::ffi::ErrorCode::DatabaseLocked,
194 ..
195 },
196 _
197 )
198 )
199}