use anyhow::{anyhow, Result};
use dirs;
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use std::str::FromStr;
#[derive(Debug, Clone)]
pub enum Browser {
Chrome,
Firefox,
Safari,
Edge,
}
impl fmt::Display for Browser {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Browser::Chrome => write!(f, "Chrome"),
Browser::Firefox => write!(f, "Firefox"),
Browser::Safari => write!(f, "Safari"),
Browser::Edge => write!(f, "Edge"),
}
}
}
impl FromStr for Browser {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"chrome" => Ok(Browser::Chrome),
"firefox" => Ok(Browser::Firefox),
"safari" => Ok(Browser::Safari),
"edge" => Ok(Browser::Edge),
_ => Err(anyhow!("Unsupported browser: {}", s)),
}
}
}
impl Browser {
pub fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"chrome" => Ok(Browser::Chrome),
"firefox" => Ok(Browser::Firefox),
"safari" => Ok(Browser::Safari),
"edge" => Ok(Browser::Edge),
_ => Err(anyhow!("Unsupported browser: {}", s)),
}
}
pub fn get_default_data_dir(&self) -> Result<PathBuf> {
let home = dirs::home_dir().ok_or_else(|| anyhow!("Could not find home directory"))?;
match self {
Browser::Chrome => {
if cfg!(target_os = "macos") {
Ok(home.join("Library/Application Support/Google/Chrome"))
} else if cfg!(target_os = "windows") {
let app_data = dirs::data_dir()
.ok_or_else(|| anyhow!("Could not find AppData directory"))?;
Ok(app_data.join("Google/Chrome/User Data"))
} else {
Ok(home.join(".config/google-chrome"))
}
}
Browser::Firefox => {
if cfg!(target_os = "macos") {
Ok(home.join("Library/Application Support/Firefox/Profiles"))
} else if cfg!(target_os = "windows") {
let app_data = dirs::data_dir()
.ok_or_else(|| anyhow!("Could not find AppData directory"))?;
Ok(app_data.join("Mozilla/Firefox/Profiles"))
} else {
Ok(home.join(".mozilla/firefox"))
}
}
Browser::Safari => {
if cfg!(target_os = "macos") {
Ok(home.join("Library/Safari"))
} else {
Err(anyhow!("Safari is only available on macOS"))
}
}
Browser::Edge => {
if cfg!(target_os = "macos") {
Ok(home.join("Library/Application Support/Microsoft Edge"))
} else if cfg!(target_os = "windows") {
let app_data = dirs::data_dir()
.ok_or_else(|| anyhow!("Could not find AppData directory"))?;
Ok(app_data.join("Microsoft/Edge/User Data"))
} else {
Ok(home.join(".config/microsoft-edge"))
}
}
}
}
pub fn find_profiles(&self, custom_dir: Option<&Path>) -> Result<Vec<PathBuf>> {
let base_dir = match custom_dir {
Some(dir) => dir.to_path_buf(),
None => self.get_default_data_dir()?,
};
let mut profiles = Vec::new();
match self {
Browser::Chrome | Browser::Edge => {
for entry in fs::read_dir(&base_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let profile_name = path.file_name().unwrap().to_string_lossy();
if profile_name.contains("Profile") || profile_name == "Default" {
if path.join("Bookmarks").exists() {
profiles.push(path);
}
}
}
}
if profiles.is_empty() {
let default_profile = base_dir.join("Default");
if default_profile.join("Bookmarks").exists() {
profiles.push(default_profile);
}
}
}
Browser::Firefox => {
for entry in fs::read_dir(&base_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
if path.join("places.sqlite").exists() {
profiles.push(path);
}
}
}
}
Browser::Safari => {
if base_dir.join("Bookmarks.plist").exists() {
profiles.push(base_dir);
} else if base_dir.extension().and_then(|s| s.to_str()) == Some("plist") {
profiles.push(base_dir.parent().unwrap_or(&base_dir).to_path_buf());
}
}
}
Ok(profiles)
}
pub fn find_profiles_with_lock_check(&self, custom_dir: Option<&Path>) -> Result<Vec<PathBuf>> {
let base_dir = match custom_dir {
Some(dir) => dir.to_path_buf(),
None => self.get_default_data_dir()?,
};
let mut profiles = Vec::new();
match self {
Browser::Chrome | Browser::Edge => {
for entry in fs::read_dir(&base_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let profile_name = path.file_name().unwrap().to_string_lossy();
if profile_name.contains("Profile") || profile_name == "Default" {
if path.join("Bookmarks").exists() {
profiles.push(path);
}
}
}
}
if profiles.is_empty() {
let default_profile = base_dir.join("Default");
if default_profile.join("Bookmarks").exists() {
profiles.push(default_profile);
}
}
}
Browser::Firefox => {
for entry in fs::read_dir(base_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
if path.join("places.sqlite").exists() {
profiles.push(path);
}
}
}
}
Browser::Safari => {
if base_dir.join("Bookmarks.plist").exists() {
profiles.push(base_dir);
}
}
}
Ok(profiles)
}
}
pub fn list_all_browsers() -> Result<()> {
let browsers = ["Chrome", "Firefox", "Safari", "Edge"];
println!("Available browsers:");
for browser_name in browsers.iter() {
if let Ok(browser) = Browser::from_str(browser_name) {
if let Ok(profiles) = browser.find_profiles(None) {
if !profiles.is_empty() {
println!(" {} ({} profiles)", browser_name, profiles.len());
} else {
println!(" {} (no profiles found)", browser_name);
}
} else {
println!(" {} (not available)", browser_name);
}
}
}
Ok(())
}
pub fn list_profiles(browser_name: &str) -> Result<()> {
let browser = Browser::from_str(browser_name)?;
let profiles = browser.find_profiles(None)?;
if profiles.is_empty() {
println!("No profiles found for {}", browser_name);
} else {
println!("Profiles for {}:", browser_name);
for (i, profile) in profiles.iter().enumerate() {
println!(" {}: {}", i + 1, profile.display());
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_browser_from_str() {
assert!(matches!(Browser::from_str("chrome"), Ok(Browser::Chrome)));
assert!(matches!(Browser::from_str("Chrome"), Ok(Browser::Chrome)));
assert!(matches!(Browser::from_str("CHROME"), Ok(Browser::Chrome)));
assert!(matches!(Browser::from_str("firefox"), Ok(Browser::Firefox)));
assert!(matches!(Browser::from_str("Firefox"), Ok(Browser::Firefox)));
assert!(matches!(Browser::from_str("safari"), Ok(Browser::Safari)));
assert!(matches!(Browser::from_str("Safari"), Ok(Browser::Safari)));
assert!(matches!(Browser::from_str("edge"), Ok(Browser::Edge)));
assert!(matches!(Browser::from_str("Edge"), Ok(Browser::Edge)));
assert!(Browser::from_str("invalid").is_err());
assert!(Browser::from_str("chrome2").is_err());
}
#[test]
fn test_browser_display() {
assert_eq!(format!("{}", Browser::Chrome), "Chrome");
assert_eq!(format!("{}", Browser::Firefox), "Firefox");
assert_eq!(format!("{}", Browser::Safari), "Safari");
assert_eq!(format!("{}", Browser::Edge), "Edge");
}
#[test]
fn test_all_browser_variants() {
let browsers = ["Chrome", "Firefox", "Safari", "Edge"];
for browser_name in browsers.iter() {
let browser = Browser::from_str(browser_name);
assert!(browser.is_ok(), "Failed to parse {}", browser_name);
let browser = browser.unwrap();
let display = format!("{}", browser);
assert_eq!(display, *browser_name);
}
}
}