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:
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,
+ })
+ }
+ }
+ }
+}