// file: crates/ksp-app-wallet-desk/src/wallet_config.rs // version: 2 //! Wallet Desk composition adapter for the standard Wallet Config and its application-owned directory preparation. /// Resolved Wallet Config and directory preparation status captured during application bootstrap. pub(crate) struct WalletConfigStartup { composite_profile_id: String, effective_directory_created_on_startup: bool, resolved: ksp_config_lib::ResolvedWalletConfig, root_directory_created_on_startup: bool, } impl WalletConfigStartup { /// Returns the selected Wallet Desk composite profile. pub(crate) fn composite_profile_id(&self) -> &str { return self.composite_profile_id.as_str(); } /// Reports whether bootstrap created at least one missing component of the effective Wallet profile directory. pub(crate) const fn effective_directory_created_on_startup(&self) -> bool { return self.effective_directory_created_on_startup; } /// Returns the resolved standard Wallet configuration. pub(crate) const fn resolved(&self) -> &ksp_config_lib::ResolvedWalletConfig { return &self.resolved; } /// Reports whether bootstrap created the configured global Wallet root. pub(crate) const fn root_directory_created_on_startup(&self) -> bool { return self.root_directory_created_on_startup; } } /// Resolves the Wallet component selected by the Wallet Desk composite and prepares its global/effective directory tree. pub(crate) fn initialize_wallet_config(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result { let environment = ksp_config_lib::ConfigEnvironment::load(); let environment = match environment { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let composite = crate::load_wallet_desk_composite(management); let composite = match composite { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let logging_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_LOGGING, ksp_config_lib::FILE_ID_STD_LOGGING); if let std::result::Result::Err(error) = logging_profile { return std::result::Result::Err(error); } let transport_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_TRANSPORT, ksp_config_lib::FILE_ID_STD_TRANSPORT); if let std::result::Result::Err(error) = transport_profile { return std::result::Result::Err(error); } let wallet_profile = crate::required_composite_component_profile(&composite, crate::COMPOSITE_COMPONENT_ID_WALLET, ksp_config_lib::FILE_ID_STD_WALLET); let wallet_profile = match wallet_profile { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let resolved = management.engine().resolve_wallet_config_profile(&wallet_profile, &environment); let resolved = match resolved { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let preparation = prepare_wallet_directories(&resolved); let preparation = match preparation { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let root_path = resolved.wallets_directory().to_string_lossy().into_owned(); let effective_path = resolved.effective_wallets_directory().to_string_lossy().into_owned(); ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_CONFIG, composite_profile = composite.profile_id(), wallet_profile = resolved.profile_id(), root_path = root_path.as_str(), effective_path = effective_path.as_str(), root_created = preparation.root_created, effective_created = preparation.effective_created, "Wallet Desk Wallet directories are ready"); return std::result::Result::Ok(WalletConfigStartup { composite_profile_id: composite.profile_id().to_owned(), effective_directory_created_on_startup: preparation.effective_created, resolved, root_directory_created_on_startup: preparation.root_created, }); } struct WalletDirectoryPreparation { effective_created: bool, root_created: bool, } fn prepare_wallet_directories(resolved: &ksp_config_lib::ResolvedWalletConfig) -> ksp_core_lib::Result { let root_created = ensure_global_wallet_root(resolved.wallets_directory()); let root_created = match root_created { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let effective_created = match resolved.wallets_subdirectory() { std::option::Option::Some(subdirectory) => { let created = ensure_profile_wallet_directory(resolved.wallets_directory(), subdirectory); match created { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), } }, std::option::Option::None => root_created, }; return std::result::Result::Ok(WalletDirectoryPreparation { effective_created, root_created }); } fn ensure_global_wallet_root(path: &std::path::Path) -> ksp_core_lib::Result { let metadata = std::fs::symlink_metadata(path); return match metadata { std::result::Result::Ok(metadata) if metadata.file_type().is_symlink() => directory_invalid(path, "configured Wallet root cannot be a symbolic link"), std::result::Result::Ok(metadata) if metadata.is_dir() => std::result::Result::Ok(false), std::result::Result::Ok(_) => directory_invalid(path, "configured Wallet root exists but is not a directory"), std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => { let creation = std::fs::create_dir_all(path); match creation { std::result::Result::Ok(()) => std::result::Result::Ok(true), std::result::Result::Err(error) => directory_prepare_error(path, "configured Wallet root cannot be created", error), } }, std::result::Result::Err(error) => directory_prepare_error(path, "configured Wallet root cannot be inspected", error), }; } fn ensure_profile_wallet_directory(root: &std::path::Path, subdirectory: &std::path::Path) -> ksp_core_lib::Result { let mut current = root.to_path_buf(); let mut created = false; for component in subdirectory.components() { let normal = match component { std::path::Component::Normal(value) => value, _ => return directory_invalid(subdirectory, "Wallet profile subdirectory contains an unsafe path component"), }; current.push(normal); let metadata = std::fs::symlink_metadata(current.as_path()); match metadata { std::result::Result::Ok(metadata) if metadata.file_type().is_symlink() => { return directory_invalid(current.as_path(), "Wallet profile directory path traverses a symbolic link"); }, std::result::Result::Ok(metadata) if metadata.is_dir() => {}, std::result::Result::Ok(_) => return directory_invalid(current.as_path(), "Wallet profile directory path contains a non-directory entry"), std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => { let creation = std::fs::create_dir(current.as_path()); if let std::result::Result::Err(error) = creation { return directory_prepare_error(current.as_path(), "Wallet profile directory component cannot be created", error); } created = true; }, std::result::Result::Err(error) => { return directory_prepare_error(current.as_path(), "Wallet profile directory component cannot be inspected", error); }, } } return std::result::Result::Ok(created); } fn directory_invalid(path: &std::path::Path, reason: &'static str) -> ksp_core_lib::Result { let path_text = path.to_string_lossy().into_owned(); ksp_logging_lib::error!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_CONFIG, path = path_text.as_str(), reason, "Wallet directory preparation rejected an invalid filesystem object"); return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_DIRECTORY_INVALID, "Configured Wallet directory is invalid") .with_context("path", path_text) .with_context("reason", reason), ); } fn directory_prepare_error(path: &std::path::Path, reason: &'static str, source: std::io::Error) -> ksp_core_lib::Result { let path_text = path.to_string_lossy().into_owned(); let source_kind = std::format!("{:?}", source.kind()); ksp_logging_lib::error!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WALLET_CONFIG, path = path_text.as_str(), reason, source_kind = source_kind.as_str(), "Wallet directory preparation failed"); return std::result::Result::Err( ksp_core_lib::Error::new(crate::ERROR_CODE_WALLET_DIRECTORY_PREPARE_FAILED, "Configured Wallet directory cannot be prepared") .with_context("path", path_text) .with_context("reason", reason) .with_source(source), ); } #[cfg(test)] #[path = "../unit_tests/wallet_config.rs"] mod tests;