asimov_chromium_module/
browsers.rs1use 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#[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
19pub 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}