paths.rs (22014B)
1 use std::{ 2 env, 3 error::Error, 4 ffi::OsString, 5 fmt, 6 path::{Path, PathBuf}, 7 }; 8 9 pub use radroots_runtime_paths::{ 10 DEFAULT_SHARED_LOCAL_EVENTS_DB_FILE_NAME as SHARED_LOCAL_EVENTS_DB_FILE_NAME, 11 DEFAULT_SHARED_LOCAL_EVENTS_NAMESPACE as SHARED_LOCAL_EVENTS_NAMESPACE, 12 DEFAULT_SHARED_LOCAL_EVENTS_NAMESPACE_KIND as SHARED_LOCAL_EVENTS_NAMESPACE_KIND, 13 DEFAULT_SHARED_LOCAL_EVENTS_NAMESPACE_VALUE as SHARED_LOCAL_EVENTS_NAMESPACE_VALUE, 14 }; 15 use radroots_runtime_paths::{ 16 default_shared_local_events_database_path_from_data_root, 17 default_shared_local_events_database_path_from_shared_accounts_data_root, 18 }; 19 20 pub const APP_RUNTIME_NAMESPACE_KIND: &str = "apps"; 21 pub const APP_RUNTIME_NAMESPACE_VALUE: &str = "app"; 22 pub const APP_RUNTIME_NAMESPACE: &str = "apps/app"; 23 pub const SHARED_ACCOUNTS_NAMESPACE_KIND: &str = "shared"; 24 pub const SHARED_ACCOUNTS_NAMESPACE_VALUE: &str = "accounts"; 25 pub const SHARED_ACCOUNTS_NAMESPACE: &str = "shared/accounts"; 26 pub const SHARED_ACCOUNTS_STORE_FILE_NAME: &str = "store.json"; 27 pub const SHARED_IDENTITIES_NAMESPACE_KIND: &str = "shared"; 28 pub const SHARED_IDENTITIES_NAMESPACE_VALUE: &str = "identities"; 29 pub const SHARED_IDENTITIES_NAMESPACE: &str = "shared/identities"; 30 pub const SHARED_IDENTITY_FILE_NAME: &str = "default.json"; 31 pub const APP_PATHS_PROFILE_ENV: &str = "RADROOTS_APP_PATHS_PROFILE"; 32 pub const APP_PATHS_REPO_LOCAL_ROOT_ENV: &str = "RADROOTS_APP_PATHS_REPO_LOCAL_ROOT"; 33 34 const APP_INTERACTIVE_USER_PROFILE: &str = "interactive_user"; 35 const APP_REPO_LOCAL_PROFILE: &str = "repo_local"; 36 37 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 38 pub enum AppRuntimePlatform { 39 Linux, 40 Macos, 41 Windows, 42 Other(&'static str), 43 } 44 45 impl AppRuntimePlatform { 46 pub fn current() -> Self { 47 match env::consts::OS { 48 "linux" => Self::Linux, 49 "macos" => Self::Macos, 50 "windows" => Self::Windows, 51 other => Self::Other(other), 52 } 53 } 54 55 pub const fn label(self) -> &'static str { 56 match self { 57 Self::Linux => "linux", 58 Self::Macos => "macos", 59 Self::Windows => "windows", 60 Self::Other(other) => other, 61 } 62 } 63 } 64 65 #[derive(Clone, Debug, Default, Eq, PartialEq)] 66 pub struct AppRuntimeHostEnvironment { 67 pub home_dir: Option<PathBuf>, 68 pub appdata_dir: Option<PathBuf>, 69 pub localappdata_dir: Option<PathBuf>, 70 pub paths_profile: Option<String>, 71 pub repo_local_root: Option<PathBuf>, 72 } 73 74 impl AppRuntimeHostEnvironment { 75 pub fn from_current_process() -> Self { 76 Self::from_env_reader(|name| env::var_os(name)) 77 } 78 79 pub fn from_env_reader<F>(mut read_env: F) -> Self 80 where 81 F: FnMut(&str) -> Option<OsString>, 82 { 83 Self { 84 home_dir: read_env("HOME").map(PathBuf::from), 85 appdata_dir: read_env("APPDATA").map(PathBuf::from), 86 localappdata_dir: read_env("LOCALAPPDATA").map(PathBuf::from), 87 paths_profile: read_env(APP_PATHS_PROFILE_ENV) 88 .map(|value| value.to_string_lossy().into_owned()), 89 repo_local_root: read_env(APP_PATHS_REPO_LOCAL_ROOT_ENV).map(PathBuf::from), 90 } 91 } 92 } 93 94 #[derive(Clone, Debug, Eq, PartialEq)] 95 pub struct AppRuntimeRoots { 96 pub config: PathBuf, 97 pub data: PathBuf, 98 pub cache: PathBuf, 99 pub logs: PathBuf, 100 pub run: PathBuf, 101 pub secrets: PathBuf, 102 } 103 104 #[derive(Clone, Debug, Eq, PartialEq)] 105 pub struct AppSharedAccountsPaths { 106 pub data_root: PathBuf, 107 pub secrets_root: PathBuf, 108 pub store_path: PathBuf, 109 } 110 111 #[derive(Clone, Debug, Eq, PartialEq)] 112 pub struct AppSharedIdentityPaths { 113 pub default_identity_path: PathBuf, 114 } 115 116 #[derive(Clone, Debug, Eq, PartialEq)] 117 pub struct AppDesktopRuntimePaths { 118 pub app: AppRuntimeRoots, 119 pub shared_accounts: AppSharedAccountsPaths, 120 pub shared_identity: AppSharedIdentityPaths, 121 } 122 123 impl AppSharedAccountsPaths { 124 pub fn shared_local_events_database_path(&self) -> Option<PathBuf> { 125 shared_local_events_database_path_from_shared_accounts(self) 126 } 127 } 128 129 impl AppRuntimeRoots { 130 pub fn current_desktop() -> Result<Self, AppRuntimePathsError> { 131 AppDesktopRuntimePaths::current_desktop().map(|paths| paths.app) 132 } 133 134 pub fn for_desktop( 135 platform: AppRuntimePlatform, 136 host_environment: AppRuntimeHostEnvironment, 137 ) -> Result<Self, AppRuntimePathsError> { 138 Ok(resolve_desktop_base_roots(platform, host_environment)?.namespaced_app()) 139 } 140 141 pub fn from_base_root(base_root: impl AsRef<Path>) -> Self { 142 let base_root = base_root.as_ref(); 143 Self { 144 config: base_root.join("config"), 145 data: base_root.join("data"), 146 cache: base_root.join("cache"), 147 logs: base_root.join("logs"), 148 run: base_root.join("run"), 149 secrets: base_root.join("secrets"), 150 } 151 } 152 153 pub fn namespaced_app(&self) -> Self { 154 self.namespaced(APP_RUNTIME_NAMESPACE_KIND, APP_RUNTIME_NAMESPACE_VALUE) 155 } 156 157 fn namespaced_shared(&self, value: &str) -> Self { 158 self.namespaced(SHARED_ACCOUNTS_NAMESPACE_KIND, value) 159 } 160 161 fn namespaced(&self, kind: &str, value: &str) -> Self { 162 let namespace = PathBuf::from(kind).join(value); 163 Self { 164 config: self.config.join(&namespace), 165 data: self.data.join(&namespace), 166 cache: self.cache.join(&namespace), 167 logs: self.logs.join(&namespace), 168 run: self.run.join(&namespace), 169 secrets: self.secrets.join(namespace), 170 } 171 } 172 } 173 174 impl AppDesktopRuntimePaths { 175 pub fn current_desktop() -> Result<Self, AppRuntimePathsError> { 176 Self::for_desktop( 177 AppRuntimePlatform::current(), 178 AppRuntimeHostEnvironment::from_current_process(), 179 ) 180 } 181 182 pub fn for_desktop( 183 platform: AppRuntimePlatform, 184 host_environment: AppRuntimeHostEnvironment, 185 ) -> Result<Self, AppRuntimePathsError> { 186 let base_roots = resolve_desktop_base_roots(platform, host_environment)?; 187 let shared_accounts = base_roots.namespaced_shared(SHARED_ACCOUNTS_NAMESPACE_VALUE); 188 let shared_identity = base_roots.namespaced_shared(SHARED_IDENTITIES_NAMESPACE_VALUE); 189 190 Ok(Self { 191 app: base_roots.namespaced_app(), 192 shared_accounts: AppSharedAccountsPaths { 193 data_root: shared_accounts.data.clone(), 194 secrets_root: shared_accounts.secrets.clone(), 195 store_path: shared_accounts.data.join(SHARED_ACCOUNTS_STORE_FILE_NAME), 196 }, 197 shared_identity: AppSharedIdentityPaths { 198 default_identity_path: shared_identity.secrets.join(SHARED_IDENTITY_FILE_NAME), 199 }, 200 }) 201 } 202 203 pub fn shared_local_events_database_path(&self) -> Result<PathBuf, AppRuntimePathsError> { 204 let data_root = self 205 .app 206 .data 207 .parent() 208 .and_then(|apps_root| apps_root.parent()) 209 .ok_or(AppRuntimePathsError::SharedLocalEventsPath)?; 210 211 Ok(shared_local_events_database_path_from_data_root(data_root)) 212 } 213 } 214 215 pub fn shared_local_events_database_path_from_shared_accounts( 216 paths: &AppSharedAccountsPaths, 217 ) -> Option<PathBuf> { 218 default_shared_local_events_database_path_from_shared_accounts_data_root(&paths.data_root).ok() 219 } 220 221 fn shared_local_events_database_path_from_data_root(data_root: &Path) -> PathBuf { 222 default_shared_local_events_database_path_from_data_root(data_root) 223 } 224 225 fn resolve_desktop_base_roots( 226 platform: AppRuntimePlatform, 227 host_environment: AppRuntimeHostEnvironment, 228 ) -> Result<AppRuntimeRoots, AppRuntimePathsError> { 229 let roots = match resolve_desktop_profile(host_environment.paths_profile.as_deref())? { 230 AppDesktopPathProfile::InteractiveUser => resolve_interactive_user_roots( 231 platform, 232 host_environment.home_dir, 233 host_environment.appdata_dir, 234 host_environment.localappdata_dir, 235 )?, 236 AppDesktopPathProfile::RepoLocal => { 237 let repo_local_root = host_environment 238 .repo_local_root 239 .ok_or(AppRuntimePathsError::MissingRepoLocalRoot)?; 240 if repo_local_root.as_os_str().is_empty() { 241 return Err(AppRuntimePathsError::EmptyRepoLocalRoot); 242 } 243 AppRuntimeRoots::from_base_root(repo_local_root) 244 } 245 }; 246 247 Ok(roots) 248 } 249 250 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 251 enum AppDesktopPathProfile { 252 InteractiveUser, 253 RepoLocal, 254 } 255 256 fn resolve_desktop_profile( 257 profile: Option<&str>, 258 ) -> Result<AppDesktopPathProfile, AppRuntimePathsError> { 259 match profile { 260 None => Ok(AppDesktopPathProfile::InteractiveUser), 261 Some(value) => match value.trim().to_ascii_lowercase().as_str() { 262 APP_INTERACTIVE_USER_PROFILE => Ok(AppDesktopPathProfile::InteractiveUser), 263 APP_REPO_LOCAL_PROFILE => Ok(AppDesktopPathProfile::RepoLocal), 264 _ => Err(AppRuntimePathsError::UnsupportedPathProfile { 265 value: value.to_owned(), 266 }), 267 }, 268 } 269 } 270 271 fn resolve_interactive_user_roots( 272 platform: AppRuntimePlatform, 273 home_dir: Option<PathBuf>, 274 appdata_dir: Option<PathBuf>, 275 localappdata_dir: Option<PathBuf>, 276 ) -> Result<AppRuntimeRoots, AppRuntimePathsError> { 277 match platform { 278 AppRuntimePlatform::Linux | AppRuntimePlatform::Macos => { 279 let home_dir = home_dir.ok_or(AppRuntimePathsError::MissingHomeDir { platform })?; 280 Ok(AppRuntimeRoots::from_base_root(home_dir.join(".radroots"))) 281 } 282 AppRuntimePlatform::Windows => { 283 let appdata_dir = appdata_dir.ok_or(AppRuntimePathsError::MissingWindowsUserDirs)?; 284 let localappdata_dir = 285 localappdata_dir.ok_or(AppRuntimePathsError::MissingWindowsUserDirs)?; 286 let config_root = appdata_dir.join("Radroots"); 287 let local_root = localappdata_dir.join("Radroots"); 288 Ok(AppRuntimeRoots { 289 config: config_root.join("config"), 290 data: local_root.join("data"), 291 cache: local_root.join("cache"), 292 logs: local_root.join("logs"), 293 run: local_root.join("run"), 294 secrets: config_root.join("secrets"), 295 }) 296 } 297 AppRuntimePlatform::Other(_) => Err(AppRuntimePathsError::UnsupportedPlatform { platform }), 298 } 299 } 300 301 #[derive(Clone, Debug, Eq, PartialEq)] 302 pub enum AppRuntimePathsError { 303 MissingHomeDir { platform: AppRuntimePlatform }, 304 MissingWindowsUserDirs, 305 MissingRepoLocalRoot, 306 EmptyRepoLocalRoot, 307 UnsupportedPathProfile { value: String }, 308 UnsupportedPlatform { platform: AppRuntimePlatform }, 309 SharedLocalEventsPath, 310 } 311 312 impl fmt::Display for AppRuntimePathsError { 313 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 314 match self { 315 Self::MissingHomeDir { platform } => { 316 write!( 317 formatter, 318 "desktop runtime roots require HOME for {}", 319 platform.label() 320 ) 321 } 322 Self::MissingWindowsUserDirs => formatter 323 .write_str("desktop runtime roots require APPDATA and LOCALAPPDATA on windows"), 324 Self::MissingRepoLocalRoot => write!( 325 formatter, 326 "desktop runtime roots require {APP_PATHS_REPO_LOCAL_ROOT_ENV} when {APP_PATHS_PROFILE_ENV}=repo_local" 327 ), 328 Self::EmptyRepoLocalRoot => write!( 329 formatter, 330 "{APP_PATHS_REPO_LOCAL_ROOT_ENV} must not be empty when {APP_PATHS_PROFILE_ENV}=repo_local" 331 ), 332 Self::UnsupportedPathProfile { value } => write!( 333 formatter, 334 "{APP_PATHS_PROFILE_ENV} must be `interactive_user` or `repo_local`, got `{value}`" 335 ), 336 Self::UnsupportedPlatform { platform } => write!( 337 formatter, 338 "desktop runtime roots are unsupported on {}", 339 platform.label() 340 ), 341 Self::SharedLocalEventsPath => formatter 342 .write_str("desktop app data root must be nested under the Radroots data root"), 343 } 344 } 345 } 346 347 impl Error for AppRuntimePathsError {} 348 349 #[cfg(test)] 350 mod tests { 351 use std::{collections::BTreeMap, ffi::OsString, path::PathBuf}; 352 353 use super::{ 354 APP_PATHS_PROFILE_ENV, APP_PATHS_REPO_LOCAL_ROOT_ENV, APP_RUNTIME_NAMESPACE, 355 AppDesktopRuntimePaths, AppRuntimeHostEnvironment, AppRuntimePathsError, 356 AppRuntimePlatform, AppRuntimeRoots, SHARED_ACCOUNTS_NAMESPACE, 357 SHARED_ACCOUNTS_STORE_FILE_NAME, SHARED_IDENTITIES_NAMESPACE, SHARED_IDENTITY_FILE_NAME, 358 SHARED_LOCAL_EVENTS_DB_FILE_NAME, SHARED_LOCAL_EVENTS_NAMESPACE, 359 }; 360 361 #[test] 362 fn desktop_runtime_roots_use_canonical_macos_namespace() { 363 let paths = AppDesktopRuntimePaths::for_desktop( 364 AppRuntimePlatform::Macos, 365 AppRuntimeHostEnvironment { 366 home_dir: Some(PathBuf::from("/Users/treesap")), 367 ..AppRuntimeHostEnvironment::default() 368 }, 369 ) 370 .expect("macos roots should resolve"); 371 372 assert_eq!( 373 paths.app.data, 374 PathBuf::from("/Users/treesap/.radroots/data").join(APP_RUNTIME_NAMESPACE) 375 ); 376 assert_eq!( 377 paths.app.logs, 378 PathBuf::from("/Users/treesap/.radroots/logs").join(APP_RUNTIME_NAMESPACE) 379 ); 380 assert_eq!( 381 paths.shared_accounts.data_root, 382 PathBuf::from("/Users/treesap/.radroots/data").join(SHARED_ACCOUNTS_NAMESPACE) 383 ); 384 assert_eq!( 385 paths.shared_accounts.secrets_root, 386 PathBuf::from("/Users/treesap/.radroots/secrets").join(SHARED_ACCOUNTS_NAMESPACE) 387 ); 388 assert_eq!( 389 paths.shared_accounts.store_path, 390 PathBuf::from("/Users/treesap/.radroots/data") 391 .join(SHARED_ACCOUNTS_NAMESPACE) 392 .join(SHARED_ACCOUNTS_STORE_FILE_NAME) 393 ); 394 assert_eq!( 395 paths.shared_identity.default_identity_path, 396 PathBuf::from("/Users/treesap/.radroots/secrets") 397 .join(SHARED_IDENTITIES_NAMESPACE) 398 .join(SHARED_IDENTITY_FILE_NAME) 399 ); 400 assert_eq!( 401 paths 402 .shared_local_events_database_path() 403 .expect("shared local events path"), 404 PathBuf::from("/Users/treesap/.radroots/data") 405 .join(SHARED_LOCAL_EVENTS_NAMESPACE) 406 .join(SHARED_LOCAL_EVENTS_DB_FILE_NAME) 407 ); 408 } 409 410 #[test] 411 fn desktop_runtime_roots_use_canonical_linux_namespace() { 412 let roots = AppRuntimeRoots::for_desktop( 413 AppRuntimePlatform::Linux, 414 AppRuntimeHostEnvironment { 415 home_dir: Some(PathBuf::from("/home/treesap")), 416 ..AppRuntimeHostEnvironment::default() 417 }, 418 ) 419 .expect("linux roots should resolve"); 420 421 assert_eq!( 422 roots.data, 423 PathBuf::from("/home/treesap/.radroots/data").join(APP_RUNTIME_NAMESPACE) 424 ); 425 assert_eq!( 426 roots.logs, 427 PathBuf::from("/home/treesap/.radroots/logs").join(APP_RUNTIME_NAMESPACE) 428 ); 429 } 430 431 #[test] 432 fn desktop_runtime_roots_use_native_windows_roots() { 433 let roots = AppRuntimeRoots::for_desktop( 434 AppRuntimePlatform::Windows, 435 AppRuntimeHostEnvironment { 436 appdata_dir: Some(PathBuf::from(r"C:\Users\treesap\AppData\Roaming")), 437 localappdata_dir: Some(PathBuf::from(r"C:\Users\treesap\AppData\Local")), 438 ..AppRuntimeHostEnvironment::default() 439 }, 440 ) 441 .expect("windows roots should resolve"); 442 443 assert_eq!( 444 roots.config, 445 PathBuf::from(r"C:\Users\treesap\AppData\Roaming") 446 .join("Radroots") 447 .join("config") 448 .join(APP_RUNTIME_NAMESPACE) 449 ); 450 assert_eq!( 451 roots.data, 452 PathBuf::from(r"C:\Users\treesap\AppData\Local") 453 .join("Radroots") 454 .join("data") 455 .join(APP_RUNTIME_NAMESPACE) 456 ); 457 } 458 459 #[test] 460 fn desktop_runtime_roots_use_explicit_repo_local_root() { 461 let paths = AppDesktopRuntimePaths::for_desktop( 462 AppRuntimePlatform::Macos, 463 AppRuntimeHostEnvironment { 464 paths_profile: Some("repo_local".to_owned()), 465 repo_local_root: Some(PathBuf::from("/repo/infra/local/runtime/radroots")), 466 ..AppRuntimeHostEnvironment::default() 467 }, 468 ) 469 .expect("repo-local roots should resolve"); 470 471 assert_eq!( 472 paths.app.data, 473 PathBuf::from("/repo/infra/local/runtime/radroots/data/apps/app") 474 ); 475 assert_eq!( 476 paths.app.logs, 477 PathBuf::from("/repo/infra/local/runtime/radroots/logs/apps/app") 478 ); 479 assert_eq!( 480 paths.shared_accounts.data_root, 481 PathBuf::from("/repo/infra/local/runtime/radroots/data/shared/accounts") 482 ); 483 assert_eq!( 484 paths.shared_identity.default_identity_path, 485 PathBuf::from("/repo/infra/local/runtime/radroots/secrets/shared/identities") 486 .join(SHARED_IDENTITY_FILE_NAME) 487 ); 488 assert_eq!( 489 paths 490 .shared_accounts 491 .shared_local_events_database_path() 492 .expect("shared local events path"), 493 PathBuf::from("/repo/infra/local/runtime/radroots/data") 494 .join(SHARED_LOCAL_EVENTS_NAMESPACE) 495 .join(SHARED_LOCAL_EVENTS_DB_FILE_NAME) 496 ); 497 } 498 499 #[test] 500 fn host_environment_can_resolve_from_env_reader() { 501 let env = BTreeMap::from([ 502 (APP_PATHS_PROFILE_ENV, OsString::from("repo_local")), 503 ( 504 APP_PATHS_REPO_LOCAL_ROOT_ENV, 505 OsString::from("/repo/infra/local/runtime/radroots"), 506 ), 507 ]); 508 let paths = AppDesktopRuntimePaths::for_desktop( 509 AppRuntimePlatform::Linux, 510 AppRuntimeHostEnvironment::from_env_reader(|name| env.get(name).cloned()), 511 ) 512 .expect("repo-local env-backed roots should resolve"); 513 514 assert_eq!( 515 paths.app.data, 516 PathBuf::from("/repo/infra/local/runtime/radroots/data/apps/app") 517 ); 518 } 519 520 #[test] 521 fn repo_local_profile_requires_explicit_root() { 522 let err = AppRuntimeRoots::for_desktop( 523 AppRuntimePlatform::Macos, 524 AppRuntimeHostEnvironment { 525 paths_profile: Some("repo_local".to_owned()), 526 ..AppRuntimeHostEnvironment::default() 527 }, 528 ) 529 .expect_err("repo-local root should be required"); 530 531 assert_eq!(err, AppRuntimePathsError::MissingRepoLocalRoot); 532 } 533 534 #[test] 535 fn unsupported_path_profile_is_rejected() { 536 let err = AppRuntimeRoots::for_desktop( 537 AppRuntimePlatform::Macos, 538 AppRuntimeHostEnvironment { 539 paths_profile: Some("dev".to_owned()), 540 ..AppRuntimeHostEnvironment::default() 541 }, 542 ) 543 .expect_err("unsupported profile should fail"); 544 545 assert_eq!( 546 err, 547 AppRuntimePathsError::UnsupportedPathProfile { 548 value: "dev".to_owned(), 549 } 550 ); 551 } 552 553 #[cfg(unix)] 554 #[test] 555 fn malformed_env_profile_fails_closed() { 556 use std::os::unix::ffi::OsStringExt; 557 558 let env = BTreeMap::from([( 559 APP_PATHS_PROFILE_ENV, 560 OsString::from_vec(vec![0xff, b'd', b'e', b'v']), 561 )]); 562 let err = AppRuntimeRoots::for_desktop( 563 AppRuntimePlatform::Macos, 564 AppRuntimeHostEnvironment::from_env_reader(|name| env.get(name).cloned()), 565 ) 566 .expect_err("malformed configured profile should fail closed"); 567 568 match err { 569 AppRuntimePathsError::UnsupportedPathProfile { value } => { 570 assert!(value.contains('\u{fffd}')); 571 assert!(value.ends_with("dev")); 572 } 573 unexpected => panic!("unexpected malformed profile error: {unexpected:?}"), 574 } 575 } 576 577 #[test] 578 fn desktop_runtime_roots_require_home_dir_on_unix() { 579 let err = AppRuntimeRoots::for_desktop( 580 AppRuntimePlatform::Macos, 581 AppRuntimeHostEnvironment::default(), 582 ) 583 .expect_err("missing home dir should fail"); 584 585 assert_eq!( 586 err, 587 AppRuntimePathsError::MissingHomeDir { 588 platform: AppRuntimePlatform::Macos, 589 } 590 ); 591 } 592 593 #[test] 594 fn desktop_runtime_roots_require_windows_user_dirs() { 595 let err = AppRuntimeRoots::for_desktop( 596 AppRuntimePlatform::Windows, 597 AppRuntimeHostEnvironment { 598 appdata_dir: Some(PathBuf::from(r"C:\Users\treesap\AppData\Roaming")), 599 ..AppRuntimeHostEnvironment::default() 600 }, 601 ) 602 .expect_err("missing local appdata should fail"); 603 604 assert_eq!(err, AppRuntimePathsError::MissingWindowsUserDirs); 605 } 606 607 #[test] 608 fn desktop_runtime_roots_reject_unsupported_platforms() { 609 let err = AppRuntimeRoots::for_desktop( 610 AppRuntimePlatform::Other("freebsd"), 611 AppRuntimeHostEnvironment::default(), 612 ) 613 .expect_err("unsupported platform should fail"); 614 615 assert_eq!( 616 err, 617 AppRuntimePathsError::UnsupportedPlatform { 618 platform: AppRuntimePlatform::Other("freebsd"), 619 } 620 ); 621 } 622 }