1pub mod migration;
20
21use std::ffi::OsString;
22use std::path::PathBuf;
23
24pub const TEST_HOME_VAR: &str = "SCOPE_TEST_HOME";
28
29pub const HOME_VAR: &str = "SCOPE_BCA_HOME";
31
32pub const HOME_DIR_NAME: &str = ".scope-bca";
34
35pub fn home() -> PathBuf {
37 #[cfg(any(test, feature = "test-support"))]
38 return sandbox().join(HOME_DIR_NAME);
39 #[cfg(not(any(test, feature = "test-support")))]
40 home_from(std::env::var_os(HOME_VAR), user_home())
41}
42
43pub fn home_from(override_root: Option<OsString>, user_home: Option<PathBuf>) -> PathBuf {
46 match override_root.filter(|r| !r.is_empty()) {
47 Some(root) => PathBuf::from(root),
48 None => user_home
49 .map(|h| h.join(HOME_DIR_NAME))
50 .unwrap_or_else(|| PathBuf::from(HOME_DIR_NAME)),
51 }
52}
53
54pub fn config_file() -> PathBuf {
56 home().join("config.yaml")
57}
58
59pub fn venues_dir() -> PathBuf {
61 home().join("venues")
62}
63
64pub fn watchlists_dir() -> PathBuf {
66 home().join("watchlists")
67}
68
69pub fn data_dir() -> PathBuf {
71 home().join("data")
72}
73
74pub fn state_dir() -> PathBuf {
76 home().join("state")
77}
78
79#[allow(clippy::disallowed_methods)]
81pub fn user_home() -> Option<PathBuf> {
82 #[cfg(any(test, feature = "test-support"))]
83 return Some(sandbox());
84 #[cfg(not(any(test, feature = "test-support")))]
85 dirs::home_dir()
86}
87
88#[allow(clippy::disallowed_methods)]
91pub(crate) fn legacy_config_dir() -> Option<PathBuf> {
92 #[cfg(any(test, feature = "test-support"))]
93 return Some(sandbox().join("legacy-config"));
94 #[cfg(not(any(test, feature = "test-support")))]
95 dirs::config_dir()
96}
97
98#[allow(clippy::disallowed_methods)]
101pub(crate) fn legacy_data_dir() -> Option<PathBuf> {
102 #[cfg(any(test, feature = "test-support"))]
103 return Some(sandbox().join("legacy-data"));
104 #[cfg(not(any(test, feature = "test-support")))]
105 dirs::data_dir()
106}
107
108#[cfg(any(test, feature = "test-support"))]
111fn sandbox() -> PathBuf {
112 static FRESH: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
113 if let Some(dir) = std::env::var_os(TEST_HOME_VAR) {
114 return PathBuf::from(dir);
115 }
116 FRESH
117 .get_or_init(|| {
118 let dir = std::env::temp_dir().join(format!("scope-test-home-{}", std::process::id()));
119 let _ = std::fs::remove_dir_all(&dir);
123 seed_catalog(&dir.join(HOME_DIR_NAME).join("venues").join("catalog"));
124 dir
125 })
126 .clone()
127}
128
129#[cfg(any(test, feature = "test-support"))]
133fn seed_catalog(dest: &std::path::Path) {
134 let repo = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("venues");
135 let Ok(entries) = std::fs::read_dir(&repo) else {
136 return;
137 };
138 if std::fs::create_dir_all(dest).is_err() {
139 return;
140 }
141 for entry in entries.flatten() {
142 let path = entry.path();
143 if path.is_file() {
144 let _ = std::fs::copy(&path, dest.join(entry.file_name()));
145 }
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use std::ffi::OsString;
153
154 #[test]
155 fn test_test_builds_never_resolve_the_real_home() {
156 let temp = std::env::temp_dir();
159 let mut all = vec![home(), config_file(), venues_dir(), data_dir(), state_dir()];
160 all.extend(user_home());
161 all.extend(legacy_config_dir());
162 all.extend(legacy_data_dir());
163 for dir in all {
164 assert!(
165 dir.starts_with(&temp),
166 "{} is outside the sandbox",
167 dir.display()
168 );
169 }
170 }
171
172 #[test]
173 fn test_the_layout_is_under_one_home() {
174 let root = home();
175 assert_eq!(config_file(), root.join("config.yaml"));
176 assert_eq!(venues_dir(), root.join("venues"));
177 assert_eq!(data_dir(), root.join("data"));
178 assert_eq!(state_dir(), root.join("state"));
179 }
180
181 #[test]
182 fn test_home_from_uses_the_override_else_the_user_home() {
183 let user = Some(PathBuf::from("/u/jon"));
184 assert_eq!(
185 home_from(None, user.clone()),
186 PathBuf::from("/u/jon/.scope-bca")
187 );
188 assert_eq!(
189 home_from(Some(OsString::from("/profiles/two")), user),
190 PathBuf::from("/profiles/two")
191 );
192 }
193
194 #[test]
195 fn test_home_from_ignores_an_empty_override() {
196 let user = Some(PathBuf::from("/u/jon"));
198 assert_eq!(
199 home_from(Some(OsString::new()), user),
200 PathBuf::from("/u/jon/.scope-bca")
201 );
202 }
203
204 #[test]
205 fn test_home_from_without_a_user_home_is_relative() {
206 assert_eq!(home_from(None, None), PathBuf::from(HOME_DIR_NAME));
207 }
208}