lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit cb56b30efec4e2a52b2631f09d2a940e029a1db3
parent cc9a3940269b1b8614089e7f4ef7652b1e84fd8c
Author: triesap <tyson@radroots.org>
Date:   Tue,  7 Apr 2026 23:08:38 +0000

paths: add canonical runtime path contract crate

- add `radroots-runtime-paths` with typed profiles roots and namespace derivation
- cover interactive-user service-host repo-local and mobile-native resolution deterministically
- register the crate in workspace coverage and release metadata
- validate with `nix develop -c cargo test -p radroots-runtime-paths`

Diffstat:
MCargo.lock | 7+++++++
MCargo.toml | 2++
Mcontract/coverage/policy.toml | 1+
Mcontract/release/publish-set.toml | 2++
Acrates/runtime-paths/Cargo.toml | 17+++++++++++++++++
Acrates/runtime-paths/README.md | 15+++++++++++++++
Acrates/runtime-paths/src/error.rs | 30++++++++++++++++++++++++++++++
Acrates/runtime-paths/src/lib.rs | 318+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/runtime-paths/src/namespace.rs | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/runtime-paths/src/platform.rs | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/runtime-paths/src/roots.rs | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 761 insertions(+), 0 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2616,6 +2616,13 @@ dependencies = [ ] [[package]] +name = "radroots-runtime-paths" +version = "0.1.0-alpha.1" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] name = "radroots-secret-vault" version = "0.1.0-alpha.1" dependencies = [ diff --git a/Cargo.toml b/Cargo.toml @@ -35,6 +35,7 @@ members = [ "crates/replica-sync-wasm", "crates/replica-db", "crates/replica-db-wasm", + "crates/runtime-paths", "crates/trade", "crates/types", "crates/protected-store", @@ -64,6 +65,7 @@ radroots-nostr-connect = { path = "crates/nostr-connect", version = "0.1.0-alpha radroots-nostr-signer = { path = "crates/nostr-signer", version = "0.1.0-alpha.1", default-features = false } radroots-nostr-ndb = { path = "crates/nostr-ndb", version = "0.1.0-alpha.1", default-features = false } radroots-runtime = { path = "crates/runtime", version = "0.1.0-alpha.1", default-features = false } +radroots-runtime-paths = { path = "crates/runtime-paths", version = "0.1.0-alpha.1", default-features = false } radroots-log = { path = "crates/log", version = "0.1.0-alpha.1", default-features = false } radroots-net = { path = "crates/net", version = "0.1.0-alpha.1", default-features = false } radroots-net-core = { path = "crates/net-core", version = "0.1.0-alpha.1", default-features = false } diff --git a/contract/coverage/policy.toml b/contract/coverage/policy.toml @@ -29,6 +29,7 @@ crates = [ "radroots-nostr-runtime", "radroots-protected-store", "radroots-runtime", + "radroots-runtime-paths", "radroots-secret-vault", "radroots-simplex-chat-proto", "radroots-simplex-smp-proto", diff --git a/contract/release/publish-set.toml b/contract/release/publish-set.toml @@ -9,6 +9,7 @@ crates = [ "radroots-log", "radroots-protected-store", "radroots-runtime", + "radroots-runtime-paths", "radroots-secret-vault", "radroots-simplex-chat-proto", "radroots-simplex-smp-proto", @@ -47,6 +48,7 @@ crates = [ "radroots-events", "radroots-protected-store", "radroots-runtime", + "radroots-runtime-paths", "radroots-secret-vault", "radroots-simplex-chat-proto", "radroots-simplex-smp-proto", diff --git a/crates/runtime-paths/Cargo.toml b/crates/runtime-paths/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "radroots-runtime-paths" +version = "0.1.0-alpha.1" +edition.workspace = true +authors = [ + "Radroots Authors", +] +rust-version.workspace = true +license.workspace = true +description = "canonical local filesystem path resolution for radroots runtimes" +repository.workspace = true +homepage.workspace = true +documentation = "https://docs.rs/radroots-runtime-paths" +readme = "README.md" + +[dependencies] +thiserror = { workspace = true } diff --git a/crates/runtime-paths/README.md b/crates/runtime-paths/README.md @@ -0,0 +1,15 @@ +# radroots-runtime-paths + +Canonical local filesystem path resolution for Rad Roots runtimes. + +## Goals + +- define the shared interactive-user, service-host, repo-local, and mobile-native path contract +- keep Linux, macOS, and Windows path derivation deterministic and explicit +- keep runtime namespaces explicit across apps, services, workers, and shared storage areas +- give higher-level runtimes one reusable source of truth for config, data, cache, logs, run, and + secrets roots + +## License + +Licensed under AGPL-3.0. See LICENSE. diff --git a/crates/runtime-paths/src/error.rs b/crates/runtime-paths/src/error.rs @@ -0,0 +1,30 @@ +use thiserror::Error; + +use crate::{RadrootsPathProfile, RadrootsPlatform}; + +#[derive(Debug, Error, Clone, PartialEq, Eq)] +pub enum RadrootsRuntimePathsError { + #[error("interactive_user on {platform} requires a home directory")] + MissingHomeDir { platform: RadrootsPlatform }, + + #[error("interactive_user on windows requires APPDATA and LOCALAPPDATA roots")] + MissingWindowsUserDirs, + + #[error("service_host on windows requires a ProgramData root")] + MissingWindowsProgramDataDir, + + #[error("repo_local requires an explicit repo-local base root")] + MissingRepoLocalRoot, + + #[error("mobile_native requires explicit logical roots")] + MissingMobileRoots, + + #[error("{profile} is not supported on {platform}")] + UnsupportedProfilePlatform { + profile: RadrootsPathProfile, + platform: RadrootsPlatform, + }, + + #[error("runtime namespace `{value}` must be one non-empty path component")] + InvalidNamespaceComponent { value: String }, +} diff --git a/crates/runtime-paths/src/lib.rs b/crates/runtime-paths/src/lib.rs @@ -0,0 +1,318 @@ +#![forbid(unsafe_code)] + +pub mod error; +pub mod namespace; +pub mod platform; +pub mod roots; + +pub use error::RadrootsRuntimePathsError; +pub use namespace::{RadrootsRuntimeNamespace, RadrootsRuntimeNamespaceKind}; +pub use platform::{RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform}; +pub use roots::{RadrootsPathOverrides, RadrootsPathResolver, RadrootsPaths}; + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::{ + RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, + RadrootsPaths, RadrootsPlatform, RadrootsRuntimeNamespace, RadrootsRuntimePathsError, + }; + + #[test] + fn interactive_user_linux_uses_home_dotradroots_root() { + let resolver = RadrootsPathResolver::new( + RadrootsPlatform::Linux, + RadrootsHostEnvironment { + home_dir: Some(PathBuf::from("/home/treesap")), + ..RadrootsHostEnvironment::default() + }, + ); + + let roots = resolver + .resolve( + RadrootsPathProfile::InteractiveUser, + &RadrootsPathOverrides::default(), + ) + .expect("resolve linux interactive roots"); + + assert_eq!( + roots, + RadrootsPaths::from_base_root("/home/treesap/.radroots") + ); + } + + #[test] + fn interactive_user_macos_uses_home_dotradroots_root() { + let resolver = RadrootsPathResolver::new( + RadrootsPlatform::Macos, + RadrootsHostEnvironment { + home_dir: Some(PathBuf::from("/Users/treesap")), + ..RadrootsHostEnvironment::default() + }, + ); + + let roots = resolver + .resolve( + RadrootsPathProfile::InteractiveUser, + &RadrootsPathOverrides::default(), + ) + .expect("resolve macos interactive roots"); + + assert_eq!( + roots, + RadrootsPaths::from_base_root("/Users/treesap/.radroots") + ); + } + + #[test] + fn interactive_user_windows_uses_native_user_roots() { + let resolver = RadrootsPathResolver::new( + RadrootsPlatform::Windows, + RadrootsHostEnvironment { + appdata_dir: Some(PathBuf::from(r"C:\Users\treesap\AppData\Roaming")), + localappdata_dir: Some(PathBuf::from(r"C:\Users\treesap\AppData\Local")), + ..RadrootsHostEnvironment::default() + }, + ); + + let roots = resolver + .resolve( + RadrootsPathProfile::InteractiveUser, + &RadrootsPathOverrides::default(), + ) + .expect("resolve windows interactive roots"); + + assert_eq!( + roots, + RadrootsPaths { + config: PathBuf::from(r"C:\Users\treesap\AppData\Roaming") + .join("Radroots") + .join("config"), + data: PathBuf::from(r"C:\Users\treesap\AppData\Local") + .join("Radroots") + .join("data"), + cache: PathBuf::from(r"C:\Users\treesap\AppData\Local") + .join("Radroots") + .join("cache"), + logs: PathBuf::from(r"C:\Users\treesap\AppData\Local") + .join("Radroots") + .join("logs"), + run: PathBuf::from(r"C:\Users\treesap\AppData\Local") + .join("Radroots") + .join("run"), + secrets: PathBuf::from(r"C:\Users\treesap\AppData\Roaming") + .join("Radroots") + .join("secrets"), + } + ); + } + + #[test] + fn service_host_unix_uses_canonical_service_roots() { + let resolver = + RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()); + + let roots = resolver + .resolve( + RadrootsPathProfile::ServiceHost, + &RadrootsPathOverrides::default(), + ) + .expect("resolve service_host roots"); + + assert_eq!( + roots, + RadrootsPaths { + config: PathBuf::from("/etc/radroots"), + data: PathBuf::from("/var/lib/radroots"), + cache: PathBuf::from("/var/cache/radroots"), + logs: PathBuf::from("/var/log/radroots"), + run: PathBuf::from("/run/radroots"), + secrets: PathBuf::from("/etc/radroots/secrets"), + } + ); + } + + #[test] + fn service_host_windows_uses_programdata_roots() { + let resolver = RadrootsPathResolver::new( + RadrootsPlatform::Windows, + RadrootsHostEnvironment { + programdata_dir: Some(PathBuf::from(r"C:\ProgramData")), + ..RadrootsHostEnvironment::default() + }, + ); + + let roots = resolver + .resolve( + RadrootsPathProfile::ServiceHost, + &RadrootsPathOverrides::default(), + ) + .expect("resolve service_host roots"); + + assert_eq!( + roots, + RadrootsPaths { + config: PathBuf::from(r"C:\ProgramData") + .join("Radroots") + .join("config"), + data: PathBuf::from(r"C:\ProgramData") + .join("Radroots") + .join("data"), + cache: PathBuf::from(r"C:\ProgramData") + .join("Radroots") + .join("cache"), + logs: PathBuf::from(r"C:\ProgramData") + .join("Radroots") + .join("logs"), + run: PathBuf::from(r"C:\ProgramData") + .join("Radroots") + .join("run"), + secrets: PathBuf::from(r"C:\ProgramData") + .join("Radroots") + .join("secrets"), + } + ); + } + + #[test] + fn repo_local_requires_explicit_base_root() { + let resolver = + RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()); + + let err = resolver + .resolve( + RadrootsPathProfile::RepoLocal, + &RadrootsPathOverrides::default(), + ) + .expect_err("repo_local should require an explicit base root"); + + assert_eq!(err, RadrootsRuntimePathsError::MissingRepoLocalRoot); + } + + #[test] + fn repo_local_uses_explicit_base_root() { + let resolver = + RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()); + + let roots = resolver + .resolve( + RadrootsPathProfile::RepoLocal, + &RadrootsPathOverrides::repo_local("/repo/.local/radroots"), + ) + .expect("resolve repo_local roots"); + + assert_eq!( + roots, + RadrootsPaths::from_base_root("/repo/.local/radroots") + ); + } + + #[test] + fn mobile_native_requires_explicit_roots() { + let resolver = RadrootsPathResolver::new( + RadrootsPlatform::Android, + RadrootsHostEnvironment::default(), + ); + + let err = resolver + .resolve( + RadrootsPathProfile::MobileNative, + &RadrootsPathOverrides::default(), + ) + .expect_err("mobile_native should require explicit roots"); + + assert_eq!(err, RadrootsRuntimePathsError::MissingMobileRoots); + } + + #[test] + fn mobile_native_returns_explicit_roots() { + let resolver = + RadrootsPathResolver::new(RadrootsPlatform::Ios, RadrootsHostEnvironment::default()); + let mobile_roots = RadrootsPaths { + config: PathBuf::from("/sandbox/config"), + data: PathBuf::from("/sandbox/data"), + cache: PathBuf::from("/sandbox/cache"), + logs: PathBuf::from("/sandbox/logs"), + run: PathBuf::from("/sandbox/run"), + secrets: PathBuf::from("/sandbox/secrets"), + }; + + let roots = resolver + .resolve( + RadrootsPathProfile::MobileNative, + &RadrootsPathOverrides::mobile(mobile_roots.clone()), + ) + .expect("resolve mobile_native roots"); + + assert_eq!(roots, mobile_roots); + } + + #[test] + fn namespace_derivation_keeps_runtime_segments_explicit() { + let namespace = RadrootsRuntimeNamespace::service("myc").expect("namespace"); + let roots = RadrootsPaths::from_base_root("/home/treesap/.radroots"); + let namespaced = roots.namespaced(&namespace); + + assert_eq!( + namespaced.config, + PathBuf::from("/home/treesap/.radroots/config/services/myc") + ); + assert_eq!( + namespaced.data, + PathBuf::from("/home/treesap/.radroots/data/services/myc") + ); + assert_eq!( + namespaced.secrets, + PathBuf::from("/home/treesap/.radroots/secrets/services/myc") + ); + } + + #[test] + fn namespace_validation_rejects_path_escape_values() { + let err = RadrootsRuntimeNamespace::app("../cli").expect_err("invalid namespace"); + assert_eq!( + err, + RadrootsRuntimePathsError::InvalidNamespaceComponent { + value: "../cli".to_owned(), + } + ); + } + + #[test] + fn interactive_user_unix_requires_home_dir() { + let resolver = + RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()); + + let err = resolver + .resolve( + RadrootsPathProfile::InteractiveUser, + &RadrootsPathOverrides::default(), + ) + .expect_err("interactive_user on linux should require a home dir"); + + assert_eq!( + err, + RadrootsRuntimePathsError::MissingHomeDir { + platform: RadrootsPlatform::Linux, + } + ); + } + + #[test] + fn interactive_user_windows_requires_native_dirs() { + let resolver = RadrootsPathResolver::new( + RadrootsPlatform::Windows, + RadrootsHostEnvironment::default(), + ); + + let err = resolver + .resolve( + RadrootsPathProfile::InteractiveUser, + &RadrootsPathOverrides::default(), + ) + .expect_err("interactive_user on windows should require native dirs"); + + assert_eq!(err, RadrootsRuntimePathsError::MissingWindowsUserDirs); + } +} diff --git a/crates/runtime-paths/src/namespace.rs b/crates/runtime-paths/src/namespace.rs @@ -0,0 +1,86 @@ +use std::path::PathBuf; + +use crate::RadrootsRuntimePathsError; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RadrootsRuntimeNamespaceKind { + App, + Service, + Worker, + Shared, +} + +impl RadrootsRuntimeNamespaceKind { + #[must_use] + pub fn path_segment(self) -> &'static str { + match self { + Self::App => "apps", + Self::Service => "services", + Self::Worker => "workers", + Self::Shared => "shared", + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsRuntimeNamespace { + kind: RadrootsRuntimeNamespaceKind, + value: String, +} + +impl RadrootsRuntimeNamespace { + pub fn app(value: impl Into<String>) -> Result<Self, RadrootsRuntimePathsError> { + Self::new(RadrootsRuntimeNamespaceKind::App, value) + } + + pub fn service(value: impl Into<String>) -> Result<Self, RadrootsRuntimePathsError> { + Self::new(RadrootsRuntimeNamespaceKind::Service, value) + } + + pub fn worker(value: impl Into<String>) -> Result<Self, RadrootsRuntimePathsError> { + Self::new(RadrootsRuntimeNamespaceKind::Worker, value) + } + + pub fn shared(value: impl Into<String>) -> Result<Self, RadrootsRuntimePathsError> { + Self::new(RadrootsRuntimeNamespaceKind::Shared, value) + } + + pub fn new( + kind: RadrootsRuntimeNamespaceKind, + value: impl Into<String>, + ) -> Result<Self, RadrootsRuntimePathsError> { + let value = value.into(); + validate_component(&value)?; + Ok(Self { kind, value }) + } + + #[must_use] + pub fn kind(&self) -> RadrootsRuntimeNamespaceKind { + self.kind + } + + #[must_use] + pub fn value(&self) -> &str { + self.value.as_str() + } + + #[must_use] + pub fn relative_path(&self) -> PathBuf { + PathBuf::from(self.kind.path_segment()).join(self.value.as_str()) + } +} + +fn validate_component(value: &str) -> Result<(), RadrootsRuntimePathsError> { + let trimmed = value.trim(); + if trimmed.is_empty() + || trimmed == "." + || trimmed == ".." + || trimmed.contains('/') + || trimmed.contains('\\') + { + return Err(RadrootsRuntimePathsError::InvalidNamespaceComponent { + value: value.to_owned(), + }); + } + Ok(()) +} diff --git a/crates/runtime-paths/src/platform.rs b/crates/runtime-paths/src/platform.rs @@ -0,0 +1,84 @@ +use std::fmt; +use std::path::PathBuf; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RadrootsPlatform { + Linux, + Macos, + Windows, + Android, + Ios, +} + +impl RadrootsPlatform { + #[must_use] + pub fn current() -> Self { + if cfg!(target_os = "android") { + Self::Android + } else if cfg!(target_os = "ios") { + Self::Ios + } else if cfg!(target_os = "macos") { + Self::Macos + } else if cfg!(target_os = "windows") { + Self::Windows + } else { + Self::Linux + } + } + + #[must_use] + pub fn is_unix_like(self) -> bool { + matches!(self, Self::Linux | Self::Macos) + } +} + +impl fmt::Display for RadrootsPlatform { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Linux => "linux", + Self::Macos => "macos", + Self::Windows => "windows", + Self::Android => "android", + Self::Ios => "ios", + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RadrootsPathProfile { + InteractiveUser, + ServiceHost, + RepoLocal, + MobileNative, +} + +impl fmt::Display for RadrootsPathProfile { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::InteractiveUser => "interactive_user", + Self::ServiceHost => "service_host", + Self::RepoLocal => "repo_local", + Self::MobileNative => "mobile_native", + }) + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct RadrootsHostEnvironment { + pub home_dir: Option<PathBuf>, + pub appdata_dir: Option<PathBuf>, + pub localappdata_dir: Option<PathBuf>, + pub programdata_dir: Option<PathBuf>, +} + +impl RadrootsHostEnvironment { + #[must_use] + pub fn from_current_process() -> Self { + Self { + home_dir: std::env::var_os("HOME").map(PathBuf::from), + appdata_dir: std::env::var_os("APPDATA").map(PathBuf::from), + localappdata_dir: std::env::var_os("LOCALAPPDATA").map(PathBuf::from), + programdata_dir: std::env::var_os("ProgramData").map(PathBuf::from), + } + } +} diff --git a/crates/runtime-paths/src/roots.rs b/crates/runtime-paths/src/roots.rs @@ -0,0 +1,199 @@ +use std::path::{Path, PathBuf}; + +use crate::{ + RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform, RadrootsRuntimeNamespace, + RadrootsRuntimePathsError, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsPaths { + pub config: PathBuf, + pub data: PathBuf, + pub cache: PathBuf, + pub logs: PathBuf, + pub run: PathBuf, + pub secrets: PathBuf, +} + +impl RadrootsPaths { + #[must_use] + pub fn from_base_root(base_root: impl AsRef<Path>) -> Self { + let base_root = base_root.as_ref(); + Self { + config: base_root.join("config"), + data: base_root.join("data"), + cache: base_root.join("cache"), + logs: base_root.join("logs"), + run: base_root.join("run"), + secrets: base_root.join("secrets"), + } + } + + #[must_use] + pub fn namespaced(&self, namespace: &RadrootsRuntimeNamespace) -> Self { + let relative = namespace.relative_path(); + Self { + config: self.config.join(&relative), + data: self.data.join(&relative), + cache: self.cache.join(&relative), + logs: self.logs.join(&relative), + run: self.run.join(&relative), + secrets: self.secrets.join(relative), + } + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct RadrootsPathOverrides { + pub repo_local_root: Option<PathBuf>, + pub mobile_roots: Option<RadrootsPaths>, +} + +impl RadrootsPathOverrides { + #[must_use] + pub fn repo_local(base_root: impl Into<PathBuf>) -> Self { + Self { + repo_local_root: Some(base_root.into()), + mobile_roots: None, + } + } + + #[must_use] + pub fn mobile(roots: RadrootsPaths) -> Self { + Self { + repo_local_root: None, + mobile_roots: Some(roots), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsPathResolver { + platform: RadrootsPlatform, + host_environment: RadrootsHostEnvironment, +} + +impl RadrootsPathResolver { + #[must_use] + pub fn new(platform: RadrootsPlatform, host_environment: RadrootsHostEnvironment) -> Self { + Self { + platform, + host_environment, + } + } + + #[must_use] + pub fn current() -> Self { + Self::new( + RadrootsPlatform::current(), + RadrootsHostEnvironment::from_current_process(), + ) + } + + #[must_use] + pub fn platform(&self) -> RadrootsPlatform { + self.platform + } + + pub fn resolve( + &self, + profile: RadrootsPathProfile, + overrides: &RadrootsPathOverrides, + ) -> Result<RadrootsPaths, RadrootsRuntimePathsError> { + match profile { + RadrootsPathProfile::InteractiveUser => self.resolve_interactive_user(), + RadrootsPathProfile::ServiceHost => self.resolve_service_host(), + RadrootsPathProfile::RepoLocal => overrides + .repo_local_root + .as_ref() + .map(RadrootsPaths::from_base_root) + .ok_or(RadrootsRuntimePathsError::MissingRepoLocalRoot), + RadrootsPathProfile::MobileNative => match self.platform { + RadrootsPlatform::Android | RadrootsPlatform::Ios => overrides + .mobile_roots + .clone() + .ok_or(RadrootsRuntimePathsError::MissingMobileRoots), + _ => Err(RadrootsRuntimePathsError::UnsupportedProfilePlatform { + profile, + platform: self.platform, + }), + }, + } + } + + fn resolve_interactive_user(&self) -> Result<RadrootsPaths, RadrootsRuntimePathsError> { + match self.platform { + RadrootsPlatform::Linux | RadrootsPlatform::Macos => self + .host_environment + .home_dir + .as_ref() + .map(|home| RadrootsPaths::from_base_root(home.join(".radroots"))) + .ok_or(RadrootsRuntimePathsError::MissingHomeDir { + platform: self.platform, + }), + RadrootsPlatform::Windows => { + let appdata = self + .host_environment + .appdata_dir + .as_ref() + .ok_or(RadrootsRuntimePathsError::MissingWindowsUserDirs)?; + let localappdata = self + .host_environment + .localappdata_dir + .as_ref() + .ok_or(RadrootsRuntimePathsError::MissingWindowsUserDirs)?; + let config_root = appdata.join("Radroots"); + let local_root = localappdata.join("Radroots"); + Ok(RadrootsPaths { + config: config_root.join("config"), + data: local_root.join("data"), + cache: local_root.join("cache"), + logs: local_root.join("logs"), + run: local_root.join("run"), + secrets: config_root.join("secrets"), + }) + } + RadrootsPlatform::Android | RadrootsPlatform::Ios => { + Err(RadrootsRuntimePathsError::UnsupportedProfilePlatform { + profile: RadrootsPathProfile::InteractiveUser, + platform: self.platform, + }) + } + } + } + + fn resolve_service_host(&self) -> Result<RadrootsPaths, RadrootsRuntimePathsError> { + match self.platform { + RadrootsPlatform::Windows => { + let programdata = self + .host_environment + .programdata_dir + .as_ref() + .ok_or(RadrootsRuntimePathsError::MissingWindowsProgramDataDir)?; + let base = programdata.join("Radroots"); + Ok(RadrootsPaths { + config: base.join("config"), + data: base.join("data"), + cache: base.join("cache"), + logs: base.join("logs"), + run: base.join("run"), + secrets: base.join("secrets"), + }) + } + RadrootsPlatform::Linux | RadrootsPlatform::Macos => Ok(RadrootsPaths { + config: PathBuf::from("/etc/radroots"), + data: PathBuf::from("/var/lib/radroots"), + cache: PathBuf::from("/var/cache/radroots"), + logs: PathBuf::from("/var/log/radroots"), + run: PathBuf::from("/run/radroots"), + secrets: PathBuf::from("/etc/radroots/secrets"), + }), + RadrootsPlatform::Android | RadrootsPlatform::Ios => { + Err(RadrootsRuntimePathsError::UnsupportedProfilePlatform { + profile: RadrootsPathProfile::ServiceHost, + platform: self.platform, + }) + } + } + } +}