commit 6091f15ce2b48e4146d17ebc465d253937db9569
parent 9701a78ac02d186d736cbe5414120a2192298fd7
Author: triesap <tyson@radroots.org>
Date: Thu, 9 Apr 2026 04:14:12 +0000
app: add shared runtime path adapter
Diffstat:
7 files changed, 143 insertions(+), 71 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -3083,6 +3083,7 @@ dependencies = [
"eframe",
"egui",
"radroots-app-test-support",
+ "radroots-runtime-paths",
"zeroize",
]
diff --git a/crates/android/src/storage.rs b/crates/android/src/storage.rs
@@ -2,36 +2,18 @@
use crate::security::{ANDROID_NOSTR_SERVICE, resolve_radroots_base_root};
#[cfg(target_os = "android")]
use crate::vault::RadrootsAndroidKeystoreVault;
+use radroots_app_core::mobile_native_app_storage_layout;
#[cfg(target_os = "android")]
use radroots_nostr_accounts::prelude::{
RadrootsNostrAccountsManager, RadrootsNostrFileAccountStore,
};
-use radroots_runtime_paths::{
- RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
- RadrootsPaths, RadrootsPlatform, RadrootsRuntimeNamespace,
-};
+use radroots_runtime_paths::{RadrootsPaths, RadrootsPlatform};
use std::path::{Path, PathBuf};
#[cfg(target_os = "android")]
use std::sync::Arc;
-fn app_namespace() -> Result<RadrootsRuntimeNamespace, String> {
- RadrootsRuntimeNamespace::app("app")
- .map_err(|source| format!("failed to resolve android app namespace: {source}"))
-}
-
fn app_paths_from_base_root(base_root: &Path) -> Result<RadrootsPaths, String> {
- let resolver = RadrootsPathResolver::new(
- RadrootsPlatform::Android,
- RadrootsHostEnvironment::default(),
- );
- let namespace = app_namespace()?;
- resolver
- .resolve(
- RadrootsPathProfile::MobileNative,
- &RadrootsPathOverrides::mobile(RadrootsPaths::from_base_root(base_root)),
- )
- .map(|roots| roots.namespaced(&namespace))
- .map_err(|source| format!("failed to resolve android mobile-native roots: {source}"))
+ Ok(mobile_native_app_storage_layout(RadrootsPlatform::Android, base_root)?.app_paths)
}
#[cfg(target_os = "android")]
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
@@ -16,6 +16,7 @@ workspace = true
[dependencies]
eframe.workspace = true
egui.workspace = true
+radroots-runtime-paths.workspace = true
zeroize.workspace = true
[dev-dependencies]
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs
@@ -10,6 +10,7 @@ mod location_resolver;
mod offline_geocoder;
mod remote_signer;
mod secret_keys;
+mod storage_paths;
pub const APP_NAME: &str = "Rad Roots";
@@ -28,6 +29,10 @@ pub use remote_signer::{
RadrootsRemoteSignerSignedNote,
};
pub use secret_keys::{RadrootsSecretImportMode, RadrootsSecretImportRequest};
+pub use storage_paths::{
+ RadrootsAppStorageLayout, interactive_user_app_storage_layout_with_resolver,
+ mobile_native_app_storage_layout,
+};
use home_location_tools::HomeLocationTools;
diff --git a/crates/core/src/storage_paths.rs b/crates/core/src/storage_paths.rs
@@ -0,0 +1,122 @@
+use std::path::{Path, PathBuf};
+
+use radroots_runtime_paths::{
+ RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
+ RadrootsPaths, RadrootsPlatform, RadrootsRuntimeNamespace,
+};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsAppStorageLayout {
+ pub runtime_root: PathBuf,
+ pub app_paths: RadrootsPaths,
+}
+
+fn app_namespace() -> Result<RadrootsRuntimeNamespace, String> {
+ RadrootsRuntimeNamespace::app("app")
+ .map_err(|source| format!("failed to resolve app runtime namespace: {source}"))
+}
+
+fn runtime_root_from_paths(roots: &RadrootsPaths) -> Result<PathBuf, String> {
+ roots
+ .config
+ .parent()
+ .map(Path::to_path_buf)
+ .ok_or_else(|| "resolved app config root had no parent".to_owned())
+}
+
+pub fn interactive_user_app_storage_layout_with_resolver(
+ resolver: &RadrootsPathResolver,
+) -> Result<RadrootsAppStorageLayout, String> {
+ let roots = resolver
+ .resolve(
+ RadrootsPathProfile::InteractiveUser,
+ &RadrootsPathOverrides::default(),
+ )
+ .map_err(|source| format!("failed to resolve app interactive-user roots: {source}"))?;
+ let namespace = app_namespace()?;
+ Ok(RadrootsAppStorageLayout {
+ runtime_root: runtime_root_from_paths(&roots)?,
+ app_paths: roots.namespaced(&namespace),
+ })
+}
+
+pub fn mobile_native_app_storage_layout(
+ platform: RadrootsPlatform,
+ base_root: &Path,
+) -> Result<RadrootsAppStorageLayout, String> {
+ let resolver = RadrootsPathResolver::new(platform, RadrootsHostEnvironment::default());
+ let roots = resolver
+ .resolve(
+ RadrootsPathProfile::MobileNative,
+ &RadrootsPathOverrides::mobile(RadrootsPaths::from_base_root(base_root)),
+ )
+ .map_err(|source| format!("failed to resolve app mobile-native roots: {source}"))?;
+ let namespace = app_namespace()?;
+ Ok(RadrootsAppStorageLayout {
+ runtime_root: runtime_root_from_paths(&roots)?,
+ app_paths: roots.namespaced(&namespace),
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{
+ interactive_user_app_storage_layout_with_resolver, mobile_native_app_storage_layout,
+ };
+ use radroots_runtime_paths::{RadrootsHostEnvironment, RadrootsPathResolver, RadrootsPlatform};
+ use std::path::PathBuf;
+
+ #[test]
+ fn interactive_user_layout_keeps_runtime_root_and_namespaced_paths() {
+ let resolver = RadrootsPathResolver::new(
+ RadrootsPlatform::Linux,
+ RadrootsHostEnvironment {
+ home_dir: Some(PathBuf::from("/home/treesap")),
+ ..RadrootsHostEnvironment::default()
+ },
+ );
+
+ let layout =
+ interactive_user_app_storage_layout_with_resolver(&resolver).expect("app layout");
+
+ assert_eq!(
+ layout.runtime_root,
+ PathBuf::from("/home/treesap/.radroots")
+ );
+ assert_eq!(
+ layout.app_paths.data,
+ PathBuf::from("/home/treesap/.radroots/data/apps/app")
+ );
+ assert_eq!(
+ layout.app_paths.logs,
+ PathBuf::from("/home/treesap/.radroots/logs/apps/app")
+ );
+ }
+
+ #[test]
+ fn mobile_native_layout_keeps_explicit_runtime_root_and_namespaced_paths() {
+ let base_root = PathBuf::from("/data/user/0/org.radroots.app.android/no_backup/RadRoots");
+
+ let layout =
+ mobile_native_app_storage_layout(RadrootsPlatform::Android, base_root.as_path())
+ .expect("mobile layout");
+
+ assert_eq!(layout.runtime_root, base_root);
+ assert_eq!(
+ layout.app_paths.config,
+ PathBuf::from(
+ "/data/user/0/org.radroots.app.android/no_backup/RadRoots/config/apps/app"
+ )
+ );
+ assert_eq!(
+ layout.app_paths.data,
+ PathBuf::from("/data/user/0/org.radroots.app.android/no_backup/RadRoots/data/apps/app")
+ );
+ assert_eq!(
+ layout.app_paths.secrets,
+ PathBuf::from(
+ "/data/user/0/org.radroots.app.android/no_backup/RadRoots/secrets/apps/app"
+ )
+ );
+ }
+}
diff --git a/crates/desktop/src/main.rs b/crates/desktop/src/main.rs
@@ -15,7 +15,7 @@ use radroots_app_core::{
RadrootsLocationReverseOptions, RadrootsOfflineGeocoderPlatform, RadrootsOfflineGeocoderState,
RadrootsOfflineGeocoderUnavailableKind, RadrootsResolvedLocation,
RadrootsReverseLocationLookupResult, RadrootsSecretImportMode, RadrootsSecretImportRequest,
- SetupActionState,
+ SetupActionState, interactive_user_app_storage_layout_with_resolver,
};
#[cfg(target_os = "macos")]
use radroots_identity::RadrootsIdentity;
@@ -23,10 +23,7 @@ use radroots_identity::RadrootsIdentity;
use radroots_nostr_accounts::prelude::{
RadrootsNostrAccountsManager, RadrootsNostrFileAccountStore, RadrootsNostrSelectedAccountStatus,
};
-use radroots_runtime_paths::{
- RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsPaths,
- RadrootsRuntimeNamespace,
-};
+use radroots_runtime_paths::{RadrootsPathResolver, RadrootsPaths};
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[cfg(target_os = "macos")]
@@ -79,25 +76,8 @@ struct DesktopBackend {
}
impl DesktopBackend {
- fn app_namespace() -> Result<RadrootsRuntimeNamespace, String> {
- RadrootsRuntimeNamespace::app("app")
- .map_err(|source| format!("failed to resolve desktop app namespace: {source}"))
- }
-
- fn interactive_user_roots_with_resolver(
- resolver: &RadrootsPathResolver,
- ) -> Result<RadrootsPaths, String> {
- resolver
- .resolve(
- RadrootsPathProfile::InteractiveUser,
- &RadrootsPathOverrides::default(),
- )
- .map_err(|source| format!("failed to resolve desktop interactive-user roots: {source}"))
- }
-
fn app_paths_with_resolver(resolver: &RadrootsPathResolver) -> Result<RadrootsPaths, String> {
- let namespace = Self::app_namespace()?;
- Ok(Self::interactive_user_roots_with_resolver(resolver)?.namespaced(&namespace))
+ Ok(interactive_user_app_storage_layout_with_resolver(resolver)?.app_paths)
}
fn new() -> Self {
@@ -131,12 +111,10 @@ impl DesktopBackend {
}
fn radroots_root() -> Result<PathBuf, String> {
- let roots = Self::interactive_user_roots_with_resolver(&RadrootsPathResolver::current())?;
- roots
- .config
- .parent()
- .map(Path::to_path_buf)
- .ok_or_else(|| "desktop interactive-user config root had no parent".to_owned())
+ Ok(
+ interactive_user_app_storage_layout_with_resolver(&RadrootsPathResolver::current())?
+ .runtime_root,
+ )
}
fn app_data_root() -> Result<PathBuf, String> {
diff --git a/crates/ios/src/storage.rs b/crates/ios/src/storage.rs
@@ -1,22 +1,15 @@
#[cfg(target_os = "ios")]
use radroots_app_apple_security::{APPLE_NOSTR_SERVICE, RadrootsAppleKeychainVault};
+use radroots_app_core::mobile_native_app_storage_layout;
#[cfg(target_os = "ios")]
use radroots_nostr_accounts::prelude::{
RadrootsNostrAccountsManager, RadrootsNostrFileAccountStore,
};
-use radroots_runtime_paths::{
- RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
- RadrootsPaths, RadrootsPlatform, RadrootsRuntimeNamespace,
-};
+use radroots_runtime_paths::{RadrootsPaths, RadrootsPlatform};
use std::path::{Path, PathBuf};
#[cfg(target_os = "ios")]
use std::sync::Arc;
-fn app_namespace() -> Result<RadrootsRuntimeNamespace, String> {
- RadrootsRuntimeNamespace::app("app")
- .map_err(|source| format!("failed to resolve ios app namespace: {source}"))
-}
-
fn mobile_base_root_from_home(home: &Path) -> PathBuf {
home.join("Library")
.join("Application Support")
@@ -24,18 +17,8 @@ fn mobile_base_root_from_home(home: &Path) -> PathBuf {
}
fn app_paths_from_home(home: &Path) -> Result<RadrootsPaths, String> {
- let resolver =
- RadrootsPathResolver::new(RadrootsPlatform::Ios, RadrootsHostEnvironment::default());
- let namespace = app_namespace()?;
- resolver
- .resolve(
- RadrootsPathProfile::MobileNative,
- &RadrootsPathOverrides::mobile(RadrootsPaths::from_base_root(
- mobile_base_root_from_home(home),
- )),
- )
- .map(|roots| roots.namespaced(&namespace))
- .map_err(|source| format!("failed to resolve ios mobile-native roots: {source}"))
+ let base_root = mobile_base_root_from_home(home);
+ Ok(mobile_native_app_storage_layout(RadrootsPlatform::Ios, base_root.as_path())?.app_paths)
}
#[cfg(target_os = "ios")]