context.rs (27803B)
1 //! Immutable resolved bootstrap context for one service instance. 2 3 use core::fmt; 4 use std::path::{Path, PathBuf}; 5 6 use serde::{Serialize, Serializer, ser::SerializeStruct}; 7 use thiserror::Error; 8 9 use crate::{ 10 InstanceId, RadrootsPathProfile, RadrootsPathResolver, RadrootsServiceInstancePaths, 11 RuntimeStateDirectoryPlan, ServiceId, StateDirectoryProvisionError, 12 }; 13 14 /// Closed provenance vocabulary for effective runtime configuration. 15 /// 16 /// Arbitrary strings, secrets, and high-cardinality labels cannot be converted 17 /// into this vocabulary: 18 /// 19 /// ```compile_fail 20 /// use radroots_runtime_paths::RuntimeContextSource; 21 /// 22 /// let _ = RuntimeContextSource::from("secret:caller-controlled-value"); 23 /// ``` 24 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] 25 #[serde(rename_all = "snake_case")] 26 pub enum RuntimeContextSource { 27 BootstrapCli, 28 Toml, 29 SafeDefault, 30 DerivedPath, 31 } 32 33 /// Sealed validated bootstrap input for one runtime context. 34 #[derive(Clone, PartialEq, Eq)] 35 pub struct RuntimeContextBootstrap { 36 profile: RadrootsPathProfile, 37 repo_local_root: Option<PathBuf>, 38 profile_source: RuntimeContextSource, 39 instance_source: RuntimeContextSource, 40 } 41 42 impl fmt::Debug for RuntimeContextBootstrap { 43 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 44 formatter 45 .debug_struct("RuntimeContextBootstrap") 46 .field("profile", &self.profile) 47 .field( 48 "repo_local_root", 49 &self.repo_local_root.as_ref().map(|_| "[redacted]"), 50 ) 51 .field("profile_source", &self.profile_source) 52 .field("instance_source", &self.instance_source) 53 .finish() 54 } 55 } 56 57 impl RuntimeContextBootstrap { 58 pub fn new( 59 profile: RadrootsPathProfile, 60 repo_local_root: Option<PathBuf>, 61 profile_source: RuntimeContextSource, 62 instance_source: RuntimeContextSource, 63 ) -> Result<Self, RuntimeContextError> { 64 if matches!(profile, RadrootsPathProfile::MobileNative) 65 || (matches!(profile, RadrootsPathProfile::RepoLocal) 66 && !matches!(profile_source, RuntimeContextSource::BootstrapCli)) 67 || !matches!( 68 profile_source, 69 RuntimeContextSource::BootstrapCli | RuntimeContextSource::SafeDefault 70 ) 71 || !matches!( 72 instance_source, 73 RuntimeContextSource::BootstrapCli | RuntimeContextSource::SafeDefault 74 ) 75 || (matches!(profile, RadrootsPathProfile::RepoLocal) != repo_local_root.is_some()) 76 { 77 return Err(RuntimeContextError::InvalidBootstrapBinding); 78 } 79 Ok(Self { 80 profile, 81 repo_local_root, 82 profile_source, 83 instance_source, 84 }) 85 } 86 87 #[must_use] 88 pub fn profile(&self) -> RadrootsPathProfile { 89 self.profile 90 } 91 } 92 93 /// Closed provenance bound to every runtime-context field class. 94 #[derive(Clone, Debug, PartialEq, Eq, Serialize)] 95 pub struct RuntimeContextSources { 96 service: RuntimeContextSource, 97 instance: RuntimeContextSource, 98 profile: RuntimeContextSource, 99 repo_local_root: Option<RuntimeContextSource>, 100 paths: RuntimeContextSource, 101 } 102 103 impl RuntimeContextSources { 104 #[must_use] 105 pub fn service(&self) -> RuntimeContextSource { 106 self.service 107 } 108 109 #[must_use] 110 pub fn instance(&self) -> RuntimeContextSource { 111 self.instance 112 } 113 114 #[must_use] 115 pub fn profile(&self) -> RuntimeContextSource { 116 self.profile 117 } 118 119 #[must_use] 120 pub fn repo_local_root(&self) -> Option<RuntimeContextSource> { 121 self.repo_local_root 122 } 123 124 #[must_use] 125 pub fn paths(&self) -> RuntimeContextSource { 126 self.paths 127 } 128 } 129 130 /// Immutable resolved bootstrap identity and canonical paths. 131 /// 132 /// External callers cannot forge or mutate a context: 133 /// 134 /// ```compile_fail 135 /// use radroots_runtime_paths::RuntimeContext; 136 /// 137 /// let _ = RuntimeContext { 138 /// service: todo!(), 139 /// instance: todo!(), 140 /// profile: todo!(), 141 /// repo_local_root: todo!(), 142 /// paths: todo!(), 143 /// sources: todo!(), 144 /// }; 145 /// ``` 146 #[derive(Clone, PartialEq, Eq)] 147 pub struct RuntimeContext { 148 service: ServiceId, 149 instance: InstanceId, 150 profile: RadrootsPathProfile, 151 repo_local_root: Option<PathBuf>, 152 paths: RadrootsServiceInstancePaths, 153 sources: RuntimeContextSources, 154 } 155 156 impl RuntimeContext { 157 pub fn resolve( 158 resolver: &RadrootsPathResolver, 159 bootstrap: RuntimeContextBootstrap, 160 service: ServiceId, 161 instance: InstanceId, 162 ) -> Result<Self, RuntimeContextError> { 163 let RuntimeContextBootstrap { 164 profile, 165 repo_local_root, 166 profile_source, 167 instance_source, 168 } = bootstrap; 169 let roots = resolver 170 .resolve(profile, repo_local_root.as_deref()) 171 .map_err(|_| RuntimeContextError::PathSelection)?; 172 let paths = RadrootsServiceInstancePaths::from_resolved_roots(&roots, &service, &instance); 173 let sources = RuntimeContextSources { 174 service: RuntimeContextSource::SafeDefault, 175 instance: instance_source, 176 profile: profile_source, 177 repo_local_root: repo_local_root 178 .as_ref() 179 .map(|_| RuntimeContextSource::BootstrapCli), 180 paths: RuntimeContextSource::DerivedPath, 181 }; 182 183 Ok(Self { 184 service, 185 instance, 186 profile, 187 repo_local_root, 188 paths, 189 sources, 190 }) 191 } 192 193 #[must_use] 194 pub fn service(&self) -> &ServiceId { 195 &self.service 196 } 197 198 #[must_use] 199 pub fn instance(&self) -> &InstanceId { 200 &self.instance 201 } 202 203 #[must_use] 204 pub fn profile(&self) -> RadrootsPathProfile { 205 self.profile 206 } 207 208 /// Returns the validated explicit repo-local base when that profile is active. 209 #[must_use] 210 pub fn repo_local_root(&self) -> Option<&Path> { 211 self.repo_local_root.as_deref() 212 } 213 214 #[must_use] 215 pub fn paths(&self) -> &RadrootsServiceInstancePaths { 216 &self.paths 217 } 218 219 #[must_use] 220 pub fn sources(&self) -> &RuntimeContextSources { 221 &self.sources 222 } 223 224 /// Returns a sealed, filesystem-I/O-free plan for this instance's state directory. 225 /// 226 /// Calling this method does not inspect or mutate the filesystem. Callers must 227 /// explicitly invoke [`RuntimeStateDirectoryPlan::provision`] to validate or 228 /// create the governed state-directory suffix. 229 pub fn state_directory_plan( 230 &self, 231 ) -> Result<RuntimeStateDirectoryPlan, StateDirectoryProvisionError> { 232 RuntimeStateDirectoryPlan::from_context(self) 233 } 234 } 235 236 impl fmt::Debug for RuntimeContext { 237 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 238 formatter 239 .debug_struct("RuntimeContext") 240 .field("service", &self.service) 241 .field("instance", &self.instance) 242 .field("profile", &self.profile) 243 .field( 244 "repo_local_root", 245 &self.repo_local_root.as_ref().map(|_| "[redacted]"), 246 ) 247 .field("paths", &"[redacted]") 248 .field("sources", &self.sources) 249 .finish() 250 } 251 } 252 253 impl Serialize for RuntimeContext { 254 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 255 where 256 S: Serializer, 257 { 258 let mut state = serializer.serialize_struct("RuntimeContext", 5)?; 259 state.serialize_field("service", &self.service)?; 260 state.serialize_field("instance", &self.instance)?; 261 state.serialize_field("profile", &self.profile.to_string())?; 262 state.serialize_field("paths", "[redacted]")?; 263 state.serialize_field("sources", &self.sources)?; 264 state.end() 265 } 266 } 267 268 /// Safe construction failures for [`RuntimeContext`]. 269 #[derive(Clone, Copy, Debug, Error, PartialEq, Eq)] 270 pub enum RuntimeContextError { 271 #[error("runtime context bootstrap provenance does not match its selectors")] 272 InvalidBootstrapBinding, 273 #[error("runtime context path selection failed")] 274 PathSelection, 275 } 276 277 #[cfg(test)] 278 mod tests { 279 use std::{path::PathBuf, time::SystemTime}; 280 281 use serde_json::json; 282 283 use super::{ 284 RuntimeContext, RuntimeContextBootstrap, RuntimeContextError, RuntimeContextSource, 285 }; 286 use crate::{ 287 InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, 288 RadrootsPlatform, ServiceId, 289 }; 290 291 fn repo_local_context(base: PathBuf) -> RuntimeContext { 292 repo_local_context_for(base, "myc", "primary") 293 } 294 295 fn repo_local_context_for(base: PathBuf, service: &str, instance: &str) -> RuntimeContext { 296 RuntimeContext::resolve( 297 &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), 298 RuntimeContextBootstrap::new( 299 RadrootsPathProfile::RepoLocal, 300 Some(base), 301 RuntimeContextSource::BootstrapCli, 302 RuntimeContextSource::BootstrapCli, 303 ) 304 .expect("bootstrap"), 305 ServiceId::new(service).expect("service"), 306 InstanceId::new(instance).expect("instance"), 307 ) 308 .expect("runtime context") 309 } 310 311 #[test] 312 fn one_repo_local_base_derives_exact_noncolliding_service_instances() { 313 let base = PathBuf::from("/repo/.local/radroots"); 314 let mut configs = Vec::new(); 315 316 for (service, instance) in [("myc", "primary"), ("myc", "secondary"), ("rhi", "default")] { 317 let context = repo_local_context_for(base.clone(), service, instance); 318 assert_eq!(context.repo_local_root(), Some(base.as_path())); 319 let suffix = PathBuf::from("services").join(service).join(instance); 320 assert_eq!(context.paths().config(), base.join("config").join(&suffix)); 321 assert_eq!(context.paths().state(), base.join("data").join(&suffix)); 322 assert_eq!(context.paths().cache(), base.join("cache").join(&suffix)); 323 assert_eq!(context.paths().logs(), base.join("logs").join(&suffix)); 324 assert_eq!(context.paths().run(), base.join("run").join(&suffix)); 325 assert_eq!( 326 context.paths().secrets(), 327 base.join("secrets").join(&suffix) 328 ); 329 configs.push(context.paths().config().to_path_buf()); 330 } 331 332 configs.sort(); 333 configs.dedup(); 334 assert_eq!(configs.len(), 3); 335 } 336 337 #[test] 338 fn retained_macos_and_windows_profiles_derive_exact_context_paths() { 339 let macos = RuntimeContext::resolve( 340 &RadrootsPathResolver::new( 341 RadrootsPlatform::Macos, 342 RadrootsHostEnvironment { 343 home_dir: Some(PathBuf::from("/Users/treesap")), 344 ..RadrootsHostEnvironment::default() 345 }, 346 ), 347 RuntimeContextBootstrap::new( 348 RadrootsPathProfile::InteractiveUser, 349 None, 350 RuntimeContextSource::SafeDefault, 351 RuntimeContextSource::BootstrapCli, 352 ) 353 .expect("macOS bootstrap"), 354 ServiceId::new("myc").expect("service"), 355 InstanceId::new("primary").expect("instance"), 356 ) 357 .expect("macOS context"); 358 assert_eq!( 359 macos.paths().config(), 360 PathBuf::from( 361 "/Users/treesap/Library/Application Support/Radroots/config/services/myc/primary" 362 ) 363 ); 364 assert_eq!( 365 macos.paths().state(), 366 PathBuf::from( 367 "/Users/treesap/Library/Application Support/Radroots/data/services/myc/primary" 368 ) 369 ); 370 assert_eq!( 371 macos.paths().cache(), 372 PathBuf::from("/Users/treesap/Library/Caches/Radroots/services/myc/primary") 373 ); 374 assert_eq!( 375 macos.paths().logs(), 376 PathBuf::from("/Users/treesap/Library/Logs/Radroots/services/myc/primary") 377 ); 378 assert_eq!( 379 macos.paths().run(), 380 PathBuf::from( 381 "/Users/treesap/Library/Application Support/Radroots/run/services/myc/primary" 382 ) 383 ); 384 assert_eq!( 385 macos.paths().secrets(), 386 PathBuf::from( 387 "/Users/treesap/Library/Application Support/Radroots/secrets/services/myc/primary" 388 ) 389 ); 390 391 let appdata = PathBuf::from(r"C:\Users\treesap\AppData\Roaming"); 392 let localappdata = PathBuf::from(r"C:\Users\treesap\AppData\Local"); 393 let windows = RuntimeContext::resolve( 394 &RadrootsPathResolver::new( 395 RadrootsPlatform::Windows, 396 RadrootsHostEnvironment { 397 appdata_dir: Some(appdata.clone()), 398 localappdata_dir: Some(localappdata.clone()), 399 ..RadrootsHostEnvironment::default() 400 }, 401 ), 402 RuntimeContextBootstrap::new( 403 RadrootsPathProfile::InteractiveUser, 404 None, 405 RuntimeContextSource::SafeDefault, 406 RuntimeContextSource::BootstrapCli, 407 ) 408 .expect("Windows bootstrap"), 409 ServiceId::new("rhi").expect("service"), 410 InstanceId::new("default").expect("instance"), 411 ) 412 .expect("Windows context"); 413 let suffix = PathBuf::from("services/rhi/default"); 414 assert_eq!( 415 windows.paths().config(), 416 appdata.join("Radroots/config").join(&suffix) 417 ); 418 assert_eq!( 419 windows.paths().state(), 420 localappdata.join("Radroots/data").join(&suffix) 421 ); 422 assert_eq!( 423 windows.paths().cache(), 424 localappdata.join("Radroots/cache").join(&suffix) 425 ); 426 assert_eq!( 427 windows.paths().logs(), 428 localappdata.join("Radroots/logs").join(&suffix) 429 ); 430 assert_eq!( 431 windows.paths().run(), 432 localappdata.join("Radroots/run").join(&suffix) 433 ); 434 assert_eq!( 435 windows.paths().secrets(), 436 appdata.join("Radroots/secrets").join(&suffix) 437 ); 438 439 assert_eq!( 440 RuntimeContext::resolve( 441 &RadrootsPathResolver::new( 442 RadrootsPlatform::Windows, 443 RadrootsHostEnvironment::default(), 444 ), 445 RuntimeContextBootstrap::new( 446 RadrootsPathProfile::InteractiveUser, 447 None, 448 RuntimeContextSource::SafeDefault, 449 RuntimeContextSource::BootstrapCli, 450 ) 451 .expect("missing-directory bootstrap"), 452 ServiceId::new("myc").expect("service"), 453 InstanceId::new("primary").expect("instance"), 454 ), 455 Err(RuntimeContextError::PathSelection) 456 ); 457 } 458 459 #[test] 460 fn retained_linux_xdg_vectors_are_exact_and_ignore_invalid_optional_values() { 461 fn resolve( 462 environment: RadrootsHostEnvironment, 463 ) -> Result<RuntimeContext, RuntimeContextError> { 464 RuntimeContext::resolve( 465 &RadrootsPathResolver::new(RadrootsPlatform::Linux, environment), 466 RuntimeContextBootstrap::new( 467 RadrootsPathProfile::InteractiveUser, 468 None, 469 RuntimeContextSource::SafeDefault, 470 RuntimeContextSource::BootstrapCli, 471 ) 472 .expect("Linux bootstrap"), 473 ServiceId::new("myc").expect("service"), 474 InstanceId::new("primary").expect("instance"), 475 ) 476 } 477 478 let configured = resolve(RadrootsHostEnvironment { 479 home_dir: Some(PathBuf::from("/home/treesap")), 480 xdg_config_home: Some(PathBuf::from("/xdg/config")), 481 xdg_data_home: Some(PathBuf::from("/xdg/data")), 482 xdg_state_home: Some(PathBuf::from("/xdg/state")), 483 xdg_cache_home: Some(PathBuf::from("/xdg/cache")), 484 xdg_runtime_dir: Some(PathBuf::from("/xdg/run")), 485 ..RadrootsHostEnvironment::default() 486 }) 487 .expect("configured XDG context"); 488 assert_eq!( 489 configured.paths().config(), 490 PathBuf::from("/xdg/config/radroots/services/myc/primary") 491 ); 492 assert_eq!( 493 configured.paths().state(), 494 PathBuf::from("/xdg/data/radroots/services/myc/primary") 495 ); 496 assert_eq!( 497 configured.paths().cache(), 498 PathBuf::from("/xdg/cache/radroots/services/myc/primary") 499 ); 500 assert_eq!( 501 configured.paths().logs(), 502 PathBuf::from("/xdg/state/radroots/logs/services/myc/primary") 503 ); 504 assert_eq!( 505 configured.paths().run(), 506 PathBuf::from("/xdg/run/radroots/services/myc/primary") 507 ); 508 assert_eq!( 509 configured.paths().secrets(), 510 PathBuf::from("/xdg/config/radroots/secrets/services/myc/primary") 511 ); 512 513 for invalid in ["", "relative"] { 514 let config = resolve(RadrootsHostEnvironment { 515 home_dir: Some(PathBuf::from("/home/treesap")), 516 xdg_config_home: Some(PathBuf::from(invalid)), 517 xdg_runtime_dir: Some(PathBuf::from("/run/user/1000")), 518 ..RadrootsHostEnvironment::default() 519 }) 520 .expect("invalid config override is ignored"); 521 assert_eq!( 522 config.paths().config(), 523 PathBuf::from("/home/treesap/.config/radroots/services/myc/primary") 524 ); 525 526 let data = resolve(RadrootsHostEnvironment { 527 home_dir: Some(PathBuf::from("/home/treesap")), 528 xdg_data_home: Some(PathBuf::from(invalid)), 529 xdg_runtime_dir: Some(PathBuf::from("/run/user/1000")), 530 ..RadrootsHostEnvironment::default() 531 }) 532 .expect("invalid data override is ignored"); 533 assert_eq!( 534 data.paths().state(), 535 PathBuf::from("/home/treesap/.local/share/radroots/services/myc/primary") 536 ); 537 538 let state = resolve(RadrootsHostEnvironment { 539 home_dir: Some(PathBuf::from("/home/treesap")), 540 xdg_state_home: Some(PathBuf::from(invalid)), 541 xdg_runtime_dir: Some(PathBuf::from("/run/user/1000")), 542 ..RadrootsHostEnvironment::default() 543 }) 544 .expect("invalid state override is ignored"); 545 assert_eq!( 546 state.paths().logs(), 547 PathBuf::from("/home/treesap/.local/state/radroots/logs/services/myc/primary") 548 ); 549 550 let cache = resolve(RadrootsHostEnvironment { 551 home_dir: Some(PathBuf::from("/home/treesap")), 552 xdg_cache_home: Some(PathBuf::from(invalid)), 553 xdg_runtime_dir: Some(PathBuf::from("/run/user/1000")), 554 ..RadrootsHostEnvironment::default() 555 }) 556 .expect("invalid cache override is ignored"); 557 assert_eq!( 558 cache.paths().cache(), 559 PathBuf::from("/home/treesap/.cache/radroots/services/myc/primary") 560 ); 561 562 assert_eq!( 563 resolve(RadrootsHostEnvironment { 564 home_dir: Some(PathBuf::from("/home/treesap")), 565 xdg_runtime_dir: Some(PathBuf::from(invalid)), 566 ..RadrootsHostEnvironment::default() 567 }), 568 Err(RuntimeContextError::PathSelection) 569 ); 570 } 571 } 572 573 #[test] 574 fn context_is_equal_immutable_and_preserves_exact_typed_sources() { 575 let first = repo_local_context(PathBuf::from("/repo/.local/radroots")); 576 let second = repo_local_context(PathBuf::from("/repo/.local/radroots")); 577 assert_eq!(first, second); 578 assert_eq!(first.service().as_str(), "myc"); 579 assert_eq!(first.instance().as_str(), "primary"); 580 assert_eq!(first.profile(), RadrootsPathProfile::RepoLocal); 581 assert_eq!(first.sources().service(), RuntimeContextSource::SafeDefault); 582 assert_eq!( 583 first.sources().instance(), 584 RuntimeContextSource::BootstrapCli 585 ); 586 assert_eq!( 587 first.sources().profile(), 588 RuntimeContextSource::BootstrapCli 589 ); 590 assert_eq!( 591 first.sources().repo_local_root(), 592 Some(RuntimeContextSource::BootstrapCli) 593 ); 594 assert_eq!(first.sources().paths(), RuntimeContextSource::DerivedPath); 595 assert_eq!( 596 first.paths().config(), 597 PathBuf::from("/repo/.local/radroots/config/services/myc/primary") 598 ); 599 600 let defaulted = RuntimeContext::resolve( 601 &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()), 602 RuntimeContextBootstrap::new( 603 RadrootsPathProfile::ServiceHost, 604 None, 605 RuntimeContextSource::SafeDefault, 606 RuntimeContextSource::SafeDefault, 607 ) 608 .expect("default bootstrap"), 609 ServiceId::new("rhi").expect("service"), 610 InstanceId::new("default").expect("instance"), 611 ) 612 .expect("default runtime context"); 613 assert_eq!( 614 defaulted.sources().profile(), 615 RuntimeContextSource::SafeDefault 616 ); 617 assert_eq!( 618 defaulted.sources().instance(), 619 RuntimeContextSource::SafeDefault 620 ); 621 assert_eq!(defaulted.sources().repo_local_root(), None); 622 assert_eq!(defaulted.repo_local_root(), None); 623 } 624 625 #[test] 626 fn serialization_and_debug_redact_paths_and_use_only_closed_sources() { 627 let bootstrap = RuntimeContextBootstrap::new( 628 RadrootsPathProfile::RepoLocal, 629 Some(PathBuf::from("/sensitive/project-root")), 630 RuntimeContextSource::BootstrapCli, 631 RuntimeContextSource::BootstrapCli, 632 ) 633 .expect("bootstrap"); 634 assert_eq!(bootstrap.profile(), RadrootsPathProfile::RepoLocal); 635 let bootstrap_debug = format!("{bootstrap:?}"); 636 assert!(bootstrap_debug.contains("repo_local_root: Some(\"[redacted]\")")); 637 let context = repo_local_context(PathBuf::from("/sensitive/project-root")); 638 let serialized = serde_json::to_value(&context).expect("serialize context"); 639 assert_eq!( 640 serialized, 641 json!({ 642 "service": "myc", 643 "instance": "primary", 644 "profile": "repo_local", 645 "paths": "[redacted]", 646 "sources": { 647 "service": "safe_default", 648 "instance": "bootstrap_cli", 649 "profile": "bootstrap_cli", 650 "repo_local_root": "bootstrap_cli", 651 "paths": "derived_path" 652 } 653 }) 654 ); 655 assert_eq!( 656 serde_json::to_value([ 657 RuntimeContextSource::BootstrapCli, 658 RuntimeContextSource::Toml, 659 RuntimeContextSource::SafeDefault, 660 RuntimeContextSource::DerivedPath, 661 ]) 662 .expect("source inventory"), 663 json!(["bootstrap_cli", "toml", "safe_default", "derived_path"]) 664 ); 665 let debug = format!("{context:?}"); 666 assert!(debug.contains("paths: \"[redacted]\"")); 667 for forbidden in [ 668 "/sensitive", 669 "project-root", 670 "/config/", 671 "/run/", 672 "secret:caller-controlled-value", 673 "0123456789abcdef0123456789abcdef", 674 ] { 675 assert!(!serialized.to_string().contains(forbidden)); 676 assert!(!debug.contains(forbidden)); 677 assert!(!bootstrap_debug.contains(forbidden)); 678 } 679 } 680 681 #[test] 682 fn construction_performs_no_directory_file_or_ambient_bootstrap_io() { 683 let unique = SystemTime::now() 684 .duration_since(SystemTime::UNIX_EPOCH) 685 .expect("clock after epoch") 686 .as_nanos(); 687 let base = std::env::temp_dir().join(format!( 688 "radroots-runtime-context-no-io-{}-{unique}", 689 std::process::id() 690 )); 691 assert!(!base.exists(), "unique test base unexpectedly exists"); 692 let context = repo_local_context(base.clone()); 693 assert_eq!( 694 context.paths().state(), 695 base.join("data/services/myc/primary") 696 ); 697 assert!(!base.exists(), "context construction created the base"); 698 699 let production = include_str!("context.rs") 700 .split("#[cfg(test)]") 701 .next() 702 .expect("production source"); 703 for forbidden in [ 704 "std::fs", 705 "create_dir", 706 "create_file", 707 "OpenOptions", 708 "std::env", 709 ] { 710 assert!( 711 !production.contains(forbidden), 712 "context production source contains forbidden I/O `{forbidden}`" 713 ); 714 } 715 } 716 717 #[test] 718 fn typed_bootstrap_rejects_every_mismatched_provenance_combination() { 719 for source in [ 720 RuntimeContextSource::Toml, 721 RuntimeContextSource::DerivedPath, 722 ] { 723 assert_eq!( 724 RuntimeContextBootstrap::new( 725 RadrootsPathProfile::ServiceHost, 726 None, 727 source, 728 RuntimeContextSource::BootstrapCli, 729 ), 730 Err(RuntimeContextError::InvalidBootstrapBinding) 731 ); 732 assert_eq!( 733 RuntimeContextBootstrap::new( 734 RadrootsPathProfile::ServiceHost, 735 None, 736 RuntimeContextSource::SafeDefault, 737 source, 738 ), 739 Err(RuntimeContextError::InvalidBootstrapBinding) 740 ); 741 } 742 assert_eq!( 743 RuntimeContextBootstrap::new( 744 RadrootsPathProfile::RepoLocal, 745 None, 746 RuntimeContextSource::BootstrapCli, 747 RuntimeContextSource::BootstrapCli, 748 ), 749 Err(RuntimeContextError::InvalidBootstrapBinding) 750 ); 751 assert_eq!( 752 RuntimeContextBootstrap::new( 753 RadrootsPathProfile::RepoLocal, 754 Some(PathBuf::from("/repo/.local/radroots")), 755 RuntimeContextSource::SafeDefault, 756 RuntimeContextSource::SafeDefault, 757 ), 758 Err(RuntimeContextError::InvalidBootstrapBinding) 759 ); 760 assert_eq!( 761 RuntimeContextBootstrap::new( 762 RadrootsPathProfile::ServiceHost, 763 Some(PathBuf::from("/repo/.local/radroots")), 764 RuntimeContextSource::BootstrapCli, 765 RuntimeContextSource::BootstrapCli, 766 ), 767 Err(RuntimeContextError::InvalidBootstrapBinding) 768 ); 769 assert_eq!( 770 RuntimeContextBootstrap::new( 771 RadrootsPathProfile::MobileNative, 772 None, 773 RuntimeContextSource::SafeDefault, 774 RuntimeContextSource::SafeDefault, 775 ), 776 Err(RuntimeContextError::InvalidBootstrapBinding) 777 ); 778 } 779 }