Skip to main content

asimov_chromium_module/
browsers.rs

1use miette::{IntoDiagnostic, Result, WrapErr, miette};
2use phf::phf_map;
3use serde_json::Value;
4use std::path::{Path, PathBuf};
5use std::string::{String, ToString};
6use std::vec::Vec;
7use std::{format, vec};
8
9/// Configuration for browser-specific user data paths.
10#[derive(Clone, Copy)]
11#[allow(dead_code)]
12pub struct UserDataPath {
13    url_prefix: &'static str,
14    linux: &'static str,
15    macos: &'static str,
16    windows: &'static str,
17}
18
19/// Browser configuration and operations.
20pub struct BrowserConfig {
21    name: &'static str,
22    paths: &'static UserDataPath,
23}
24
25impl BrowserConfig {
26    pub fn name(&self) -> &str {
27        self.name
28    }
29
30    pub fn profile_path(&self, profile_name: Option<&str>) -> Result<PathBuf> {
31        let mut path = self.platform_user_data_path()?;
32        path.push(profile_name.unwrap_or("Default"));
33        if !path.is_dir() {
34            return Err(miette!(
35                "Profile path not found for browser '{}': {}",
36                self.name,
37                path.display()
38            ));
39        }
40        Ok(path)
41    }
42
43    fn platform_user_data_path(&self) -> Result<PathBuf> {
44        let mut path = PathBuf::new();
45
46        #[cfg(target_os = "linux")]
47        {
48            let home = std::env::var("HOME")
49                .into_diagnostic()
50                .wrap_err("HOME environment variable must be set")?;
51            path.push(home);
52            path.push(self.paths.linux);
53        }
54
55        #[cfg(target_os = "macos")]
56        {
57            let home = std::env::var("HOME")
58                .into_diagnostic()
59                .wrap_err("HOME environment variable must be set")?;
60            path.push(home);
61            path.push(self.paths.macos);
62        }
63
64        #[cfg(target_os = "windows")]
65        {
66            let local_app_data = std::env::var("LOCALAPPDATA")
67                .into_diagnostic()
68                .wrap_err("LOCALAPPDATA environment variable must be set")?;
69            path.push(local_app_data);
70            path.push(self.paths.windows);
71        }
72
73        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
74        {
75            return Err(miette!("Unsupported operating system"));
76        }
77
78        Ok(path)
79    }
80
81    pub fn bookmarks_path(&self, profile_name: Option<&str>) -> Result<PathBuf> {
82        self.profile_path(profile_name)
83            .map(|path| path.join("Bookmarks"))
84    }
85
86    pub fn list_profiles(&self) -> Result<Vec<String>> {
87        let profile_path = self.profile_path(None)?;
88        let base_path = profile_path
89            .parent()
90            .ok_or_else(|| miette!("Failed to get parent directory"))?;
91
92        let mut profiles = Vec::new();
93        for entry in std::fs::read_dir(base_path).into_diagnostic()? {
94            let entry = entry.into_diagnostic()?;
95            if entry.file_type().into_diagnostic()?.is_dir() {
96                if let Some(name) = entry.file_name().to_str() {
97                    if matches!(name, "Default") || name.starts_with("Profile ") {
98                        profiles.push(name.to_string());
99                    }
100                }
101            }
102        }
103        Ok(profiles)
104    }
105}
106
107static SUPPORTED_BROWSERS: phf::Map<&'static str, UserDataPath> = phf_map! {
108    "chrome" => UserDataPath {
109        url_prefix: "chrome://bookmarks",
110        linux: ".config/google-chrome",
111        macos: "Library/Application Support/Google/Chrome",
112        windows: "Google/Chrome/User Data",
113    },
114    "brave" => UserDataPath {
115        url_prefix: "brave://bookmarks",
116        linux: ".config/BraveSoftware/Brave-Browser",
117        macos: "Library/Application Support/BraveSoftware/Brave-Browser",
118        windows: "BraveSoftware/Brave-Browser/User Data",
119    },
120    "edge" => UserDataPath {
121        url_prefix: "edge://bookmarks",
122        linux: ".config/microsoft-edge",
123        macos: "Library/Application Support/Microsoft Edge",
124        windows: "Microsoft/Edge/User Data",
125    },
126    "chromium" => UserDataPath {
127        url_prefix: "chromium://bookmarks",
128        linux: ".config/chromium",
129        macos: "Library/Application Support/Chromium",
130        windows: "Chromium/User Data",
131    },
132};
133
134pub fn get_browser_from_url(url: &str) -> Option<BrowserConfig> {
135    SUPPORTED_BROWSERS
136        .entries()
137        .find(|(_, config)| {
138            url == config.url_prefix || url.starts_with(&format!("{}/", config.url_prefix))
139        })
140        .map(|(name, paths)| BrowserConfig { name, paths })
141}
142
143pub fn fetch_bookmarks(url: &str) -> Result<Vec<Value>> {
144    let browser = get_browser_from_url(url).ok_or_else(|| {
145        miette!(
146            "Unsupported URL: {}. Supported prefixes: {:?}",
147            url,
148            SUPPORTED_BROWSERS
149                .entries()
150                .map(|(_, config)| config.url_prefix)
151                .collect::<Vec<_>>()
152        )
153    })?;
154
155    let profiles: Vec<String> = url
156        .strip_prefix(browser.paths.url_prefix)
157        .and_then(|suffix| suffix.strip_prefix('/').filter(|s| !s.is_empty()))
158        .map(|profile| vec![profile.to_string()])
159        .unwrap_or_else(|| browser.list_profiles().unwrap_or_default());
160
161    if profiles.is_empty() {
162        return Err(miette!("No profiles found for browser: {}", browser.name()));
163    }
164
165    let mut outputs = Vec::new();
166
167    for profile in profiles {
168        if let Ok(path) = browser.bookmarks_path(Some(&profile)) {
169            if let Ok(bookmarks) = read_bookmarks_file(&path) {
170                outputs.push(bookmarks);
171            }
172        }
173    }
174
175    if outputs.is_empty() {
176        return Err(miette!(
177            "No valid bookmarks files found for browser: {}",
178            browser.name()
179        ));
180    }
181
182    Ok(outputs)
183}
184
185fn read_bookmarks_file(path: &Path) -> Result<Value> {
186    if !path.is_file() {
187        return Err(miette!("Bookmarks file not found at {}", path.display()));
188    }
189
190    let input = std::fs::read_to_string(path)
191        .into_diagnostic()
192        .wrap_err_with(|| format!("Failed to read bookmarks at {}", path.display()))?;
193
194    serde_json::from_str(&input)
195        .into_diagnostic()
196        .wrap_err_with(|| format!("Failed to parse bookmarks JSON from {}", path.display()))
197}