1use anyhow::{anyhow, Context, Result};
4use std::path::PathBuf;
5
6use crate::detect::Kind;
7
8const ENV_OVERRIDE: &str = "BROWSER_CONTROL_DATA_DIR";
9const CONFIG_ENV_OVERRIDE: &str = "BROWSER_CONTROL_CONFIG_DIR";
10
11pub fn data_dir() -> Result<PathBuf> {
19 if let Some(v) = std::env::var_os(ENV_OVERRIDE) {
20 let p = PathBuf::from(v);
21 if p.as_os_str().is_empty() {
22 return Err(anyhow!("{} is set but empty", ENV_OVERRIDE));
23 }
24 if !p.is_absolute() {
25 return Err(anyhow!(
26 "{} must be an absolute path (got {})",
27 ENV_OVERRIDE,
28 p.display()
29 ));
30 }
31 return Ok(p);
32 }
33
34 let pd = directories::ProjectDirs::from("", "", "browser-control")
35 .ok_or_else(|| anyhow!("could not determine user data directory (no home dir found)"))?;
36 Ok(pd.data_dir().to_path_buf())
37}
38
39pub fn registry_db_path() -> Result<PathBuf> {
41 let dir = data_dir()?;
42 std::fs::create_dir_all(&dir)
43 .with_context(|| format!("creating data dir {}", dir.display()))?;
44 Ok(dir.join("registry.db"))
45}
46
47pub fn config_dir() -> Result<PathBuf> {
55 if let Some(v) = std::env::var_os(CONFIG_ENV_OVERRIDE) {
56 let p = PathBuf::from(v);
57 if p.as_os_str().is_empty() {
58 return Err(anyhow!("{} is set but empty", CONFIG_ENV_OVERRIDE));
59 }
60 if !p.is_absolute() {
61 return Err(anyhow!(
62 "{} must be an absolute path (got {})",
63 CONFIG_ENV_OVERRIDE,
64 p.display()
65 ));
66 }
67 return Ok(p);
68 }
69
70 let pd = directories::ProjectDirs::from("", "", "browser-control")
71 .ok_or_else(|| anyhow!("could not determine user config directory (no home dir found)"))?;
72 Ok(pd.config_dir().to_path_buf())
73}
74
75pub fn config_file_path() -> Result<PathBuf> {
77 let dir = config_dir()?;
78 std::fs::create_dir_all(&dir)
79 .with_context(|| format!("creating config dir {}", dir.display()))?;
80 Ok(dir.join("config.toml"))
81}
82
83pub fn profiles_dir() -> Result<PathBuf> {
85 let dir = data_dir()?.join("profiles");
86 std::fs::create_dir_all(&dir)
87 .with_context(|| format!("creating profiles dir {}", dir.display()))?;
88 Ok(dir)
89}
90
91pub fn default_profile_dir(kind: Kind) -> Result<PathBuf> {
107 let dir = config_dir()?
108 .join("profiles")
109 .join(kind.as_str())
110 .join("default");
111 std::fs::create_dir_all(&dir)
112 .with_context(|| format!("creating default profile dir {}", dir.display()))?;
113 Ok(dir)
114}
115
116pub fn downloads_staging_dir(kind: Kind) -> Result<PathBuf> {
122 let dir = data_dir()?.join("downloads").join(kind.as_str());
123 std::fs::create_dir_all(&dir)
124 .with_context(|| format!("creating downloads dir {}", dir.display()))?;
125 Ok(dir)
126}
127
128pub fn is_owned_profile(profile_dir: &std::path::Path) -> bool {
133 let Ok(root) = config_dir().map(|d| d.join("profiles")) else {
134 return false;
135 };
136 let canon = |p: &std::path::Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
137 canon(profile_dir).starts_with(canon(&root))
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn paths_work() {
146 let _g = crate::test_support::ENV_LOCK
147 .lock()
148 .unwrap_or_else(|e| e.into_inner());
149 let tmp = tempfile::TempDir::new().unwrap();
151 let tmp_path = tmp.path().to_path_buf();
152 std::env::set_var(ENV_OVERRIDE, &tmp_path);
155
156 assert_eq!(data_dir().unwrap(), tmp_path);
157
158 let db = registry_db_path().unwrap();
159 assert_eq!(db, tmp_path.join("registry.db"));
160 assert!(db.parent().unwrap().exists());
161 assert_eq!(db.file_name().unwrap(), "registry.db");
162
163 let pdir = profiles_dir().unwrap();
164 assert_eq!(pdir, tmp_path.join("profiles"));
165 assert!(pdir.is_dir());
166 assert_eq!(pdir.file_name().unwrap(), "profiles");
167
168 std::env::remove_var(ENV_OVERRIDE);
170 let d = data_dir().unwrap();
171 let expected_suffix = if cfg!(windows) {
172 "browser-control\\data"
173 } else {
174 "browser-control"
175 };
176 assert!(
177 d.ends_with(expected_suffix),
178 "expected default data_dir to end with {expected_suffix:?}, got {}",
179 d.display()
180 );
181 }
182
183 #[test]
184 fn config_paths_work() {
185 let _g = crate::test_support::ENV_LOCK
186 .lock()
187 .unwrap_or_else(|e| e.into_inner());
188 let tmp = tempfile::TempDir::new().unwrap();
189 let tmp_path = tmp.path().to_path_buf();
190 std::env::set_var(CONFIG_ENV_OVERRIDE, &tmp_path);
191
192 assert_eq!(config_dir().unwrap(), tmp_path);
193
194 let cfg = config_file_path().unwrap();
195 assert_eq!(cfg, tmp_path.join("config.toml"));
196 assert!(cfg.parent().unwrap().exists());
197 assert_eq!(cfg.file_name().unwrap(), "config.toml");
198
199 std::env::remove_var(CONFIG_ENV_OVERRIDE);
200 let d = config_dir().unwrap();
201 let expected_suffix = if cfg!(windows) {
202 "browser-control\\config"
203 } else {
204 "browser-control"
205 };
206 assert!(
207 d.ends_with(expected_suffix),
208 "expected default config_dir to end with {expected_suffix:?}, got {}",
209 d.display()
210 );
211 }
212
213 #[test]
214 fn default_profile_dir_honours_config_env_override() {
215 let _g = crate::test_support::ENV_LOCK
216 .lock()
217 .unwrap_or_else(|e| e.into_inner());
218 let tmp = tempfile::TempDir::new().unwrap();
219 let tmp_path = tmp.path().to_path_buf();
220 std::env::set_var(CONFIG_ENV_OVERRIDE, &tmp_path);
221
222 for k in [
223 Kind::Chrome,
224 Kind::Edge,
225 Kind::Chromium,
226 Kind::Brave,
227 Kind::Firefox,
228 ] {
229 let p = default_profile_dir(k).unwrap();
230 assert_eq!(
231 p,
232 tmp_path.join("profiles").join(k.as_str()).join("default")
233 );
234 assert!(p.is_dir(), "expected {} to be created", p.display());
235 }
236
237 std::env::remove_var(CONFIG_ENV_OVERRIDE);
238 }
239}