roots.rs (11020B)
1 use std::path::{Path, PathBuf}; 2 3 use crate::{ 4 RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform, RadrootsRuntimeNamespace, 5 RadrootsRuntimePathsError, 6 }; 7 8 #[derive(Debug, Clone, PartialEq, Eq)] 9 pub struct RadrootsPaths { 10 pub config: PathBuf, 11 pub data: PathBuf, 12 pub cache: PathBuf, 13 pub logs: PathBuf, 14 pub run: PathBuf, 15 pub secrets: PathBuf, 16 } 17 18 impl RadrootsPaths { 19 #[must_use] 20 pub fn from_base_root(base_root: impl AsRef<Path>) -> Self { 21 let base_root = base_root.as_ref(); 22 Self { 23 config: base_root.join("config"), 24 data: base_root.join("data"), 25 cache: base_root.join("cache"), 26 logs: base_root.join("logs"), 27 run: base_root.join("run"), 28 secrets: base_root.join("secrets"), 29 } 30 } 31 32 #[must_use] 33 pub fn namespaced(&self, namespace: &RadrootsRuntimeNamespace) -> Self { 34 let relative = namespace.relative_path(); 35 Self { 36 config: self.config.join(&relative), 37 data: self.data.join(&relative), 38 cache: self.cache.join(&relative), 39 logs: self.logs.join(&relative), 40 run: self.run.join(&relative), 41 secrets: self.secrets.join(relative), 42 } 43 } 44 } 45 46 #[derive(Debug, Clone, Default, PartialEq, Eq)] 47 pub struct RadrootsPathOverrides { 48 pub repo_local_root: Option<PathBuf>, 49 pub mobile_roots: Option<RadrootsPaths>, 50 } 51 52 impl RadrootsPathOverrides { 53 #[must_use] 54 pub fn repo_local(base_root: impl Into<PathBuf>) -> Self { 55 Self { 56 repo_local_root: Some(base_root.into()), 57 mobile_roots: None, 58 } 59 } 60 61 #[must_use] 62 pub fn mobile(roots: RadrootsPaths) -> Self { 63 Self { 64 repo_local_root: None, 65 mobile_roots: Some(roots), 66 } 67 } 68 } 69 70 #[derive(Debug, Clone, PartialEq, Eq)] 71 pub struct RadrootsPathResolver { 72 platform: RadrootsPlatform, 73 host_environment: RadrootsHostEnvironment, 74 } 75 76 impl RadrootsPathResolver { 77 #[must_use] 78 pub fn new(platform: RadrootsPlatform, host_environment: RadrootsHostEnvironment) -> Self { 79 Self { 80 platform, 81 host_environment, 82 } 83 } 84 85 #[must_use] 86 pub fn current() -> Self { 87 Self::new( 88 RadrootsPlatform::current(), 89 RadrootsHostEnvironment::from_current_process(), 90 ) 91 } 92 93 #[must_use] 94 pub fn platform(&self) -> RadrootsPlatform { 95 self.platform 96 } 97 98 pub fn resolve( 99 &self, 100 profile: RadrootsPathProfile, 101 overrides: &RadrootsPathOverrides, 102 ) -> Result<RadrootsPaths, RadrootsRuntimePathsError> { 103 match profile { 104 RadrootsPathProfile::InteractiveUser => self.resolve_interactive_user(), 105 RadrootsPathProfile::ServiceHost => self.resolve_service_host(), 106 RadrootsPathProfile::RepoLocal => overrides 107 .repo_local_root 108 .as_ref() 109 .map(RadrootsPaths::from_base_root) 110 .ok_or(RadrootsRuntimePathsError::MissingRepoLocalRoot), 111 RadrootsPathProfile::MobileNative => match self.platform { 112 RadrootsPlatform::Android | RadrootsPlatform::Ios => overrides 113 .mobile_roots 114 .clone() 115 .ok_or(RadrootsRuntimePathsError::MissingMobileRoots), 116 _ => Err(RadrootsRuntimePathsError::UnsupportedProfilePlatform { 117 profile, 118 platform: self.platform, 119 }), 120 }, 121 } 122 } 123 124 fn resolve_interactive_user(&self) -> Result<RadrootsPaths, RadrootsRuntimePathsError> { 125 match self.platform { 126 RadrootsPlatform::Linux | RadrootsPlatform::Macos => self 127 .host_environment 128 .home_dir 129 .as_ref() 130 .map(|home| RadrootsPaths::from_base_root(home.join(".radroots"))) 131 .ok_or(RadrootsRuntimePathsError::MissingHomeDir { 132 platform: self.platform, 133 }), 134 RadrootsPlatform::Windows => { 135 let appdata = self 136 .host_environment 137 .appdata_dir 138 .as_ref() 139 .ok_or(RadrootsRuntimePathsError::MissingWindowsUserDirs)?; 140 let localappdata = self 141 .host_environment 142 .localappdata_dir 143 .as_ref() 144 .ok_or(RadrootsRuntimePathsError::MissingWindowsUserDirs)?; 145 let config_root = appdata.join("Radroots"); 146 let local_root = localappdata.join("Radroots"); 147 Ok(RadrootsPaths { 148 config: config_root.join("config"), 149 data: local_root.join("data"), 150 cache: local_root.join("cache"), 151 logs: local_root.join("logs"), 152 run: local_root.join("run"), 153 secrets: config_root.join("secrets"), 154 }) 155 } 156 RadrootsPlatform::Android | RadrootsPlatform::Ios => { 157 Err(RadrootsRuntimePathsError::UnsupportedProfilePlatform { 158 profile: RadrootsPathProfile::InteractiveUser, 159 platform: self.platform, 160 }) 161 } 162 } 163 } 164 165 fn resolve_service_host(&self) -> Result<RadrootsPaths, RadrootsRuntimePathsError> { 166 match self.platform { 167 RadrootsPlatform::Windows => { 168 let programdata = self 169 .host_environment 170 .programdata_dir 171 .as_ref() 172 .ok_or(RadrootsRuntimePathsError::MissingWindowsProgramDataDir)?; 173 let base = programdata.join("Radroots"); 174 Ok(RadrootsPaths { 175 config: base.join("config"), 176 data: base.join("data"), 177 cache: base.join("cache"), 178 logs: base.join("logs"), 179 run: base.join("run"), 180 secrets: base.join("secrets"), 181 }) 182 } 183 RadrootsPlatform::Linux | RadrootsPlatform::Macos => Ok(RadrootsPaths { 184 config: PathBuf::from("/etc/radroots"), 185 data: PathBuf::from("/var/lib/radroots"), 186 cache: PathBuf::from("/var/cache/radroots"), 187 logs: PathBuf::from("/var/log/radroots"), 188 run: PathBuf::from("/run/radroots"), 189 secrets: PathBuf::from("/etc/radroots/secrets"), 190 }), 191 RadrootsPlatform::Android | RadrootsPlatform::Ios => { 192 Err(RadrootsRuntimePathsError::UnsupportedProfilePlatform { 193 profile: RadrootsPathProfile::ServiceHost, 194 platform: self.platform, 195 }) 196 } 197 } 198 } 199 } 200 201 #[cfg(test)] 202 mod tests { 203 use std::path::PathBuf; 204 205 use super::{RadrootsPathOverrides, RadrootsPathResolver, RadrootsPaths}; 206 use crate::{ 207 RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform, RadrootsRuntimePathsError, 208 }; 209 210 #[test] 211 fn path_override_helpers_only_populate_their_owned_slot() { 212 let repo_local = RadrootsPathOverrides::repo_local("/repo/.local/radroots"); 213 assert_eq!( 214 repo_local.repo_local_root, 215 Some(PathBuf::from("/repo/.local/radroots")) 216 ); 217 assert!(repo_local.mobile_roots.is_none()); 218 219 let mobile_roots = RadrootsPaths::from_base_root("/sandbox"); 220 let mobile = RadrootsPathOverrides::mobile(mobile_roots.clone()); 221 assert!(mobile.repo_local_root.is_none()); 222 assert_eq!(mobile.mobile_roots, Some(mobile_roots)); 223 } 224 225 #[test] 226 fn resolver_current_uses_process_platform_and_environment() { 227 let resolver = RadrootsPathResolver::current(); 228 assert_eq!(resolver.platform(), RadrootsPlatform::current()); 229 assert_eq!( 230 resolver, 231 RadrootsPathResolver::new( 232 RadrootsPlatform::current(), 233 RadrootsHostEnvironment::from_current_process() 234 ) 235 ); 236 } 237 238 #[test] 239 fn mobile_profile_is_rejected_on_non_mobile_platforms() { 240 let resolver = 241 RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()); 242 243 let err = resolver 244 .resolve( 245 RadrootsPathProfile::MobileNative, 246 &RadrootsPathOverrides::default(), 247 ) 248 .expect_err("mobile profile should be rejected on linux"); 249 250 assert_eq!( 251 err, 252 RadrootsRuntimePathsError::UnsupportedProfilePlatform { 253 profile: RadrootsPathProfile::MobileNative, 254 platform: RadrootsPlatform::Linux, 255 } 256 ); 257 } 258 259 #[test] 260 fn interactive_user_is_rejected_on_mobile_platforms() { 261 for platform in [RadrootsPlatform::Android, RadrootsPlatform::Ios] { 262 let resolver = RadrootsPathResolver::new(platform, RadrootsHostEnvironment::default()); 263 let err = resolver 264 .resolve( 265 RadrootsPathProfile::InteractiveUser, 266 &RadrootsPathOverrides::default(), 267 ) 268 .expect_err("interactive_user should be unsupported on mobile"); 269 assert_eq!( 270 err, 271 RadrootsRuntimePathsError::UnsupportedProfilePlatform { 272 profile: RadrootsPathProfile::InteractiveUser, 273 platform, 274 } 275 ); 276 } 277 } 278 279 #[test] 280 fn service_host_windows_requires_programdata() { 281 let resolver = RadrootsPathResolver::new( 282 RadrootsPlatform::Windows, 283 RadrootsHostEnvironment::default(), 284 ); 285 286 let err = resolver 287 .resolve( 288 RadrootsPathProfile::ServiceHost, 289 &RadrootsPathOverrides::default(), 290 ) 291 .expect_err("service_host on windows should require programdata"); 292 293 assert_eq!(err, RadrootsRuntimePathsError::MissingWindowsProgramDataDir); 294 } 295 296 #[test] 297 fn service_host_is_rejected_on_mobile_platforms() { 298 for platform in [RadrootsPlatform::Android, RadrootsPlatform::Ios] { 299 let resolver = RadrootsPathResolver::new(platform, RadrootsHostEnvironment::default()); 300 let err = resolver 301 .resolve( 302 RadrootsPathProfile::ServiceHost, 303 &RadrootsPathOverrides::default(), 304 ) 305 .expect_err("service_host should be unsupported on mobile"); 306 assert_eq!( 307 err, 308 RadrootsRuntimePathsError::UnsupportedProfilePlatform { 309 profile: RadrootsPathProfile::ServiceHost, 310 platform, 311 } 312 ); 313 } 314 } 315 }