use crate::cancellation::RequestLease; use spotiflac_extensions::environment::{ EnvironmentError, ExtensionEnvironment as CoreEnvironment, }; use spotiflac_extensions::{ExtensionError, ExtensionRuntime, RuntimeLimits}; use std::path::Path; use std::sync::{Arc, Mutex}; /// Keeps operation directories accessible until native finalization finishes. /// Explicit close and object destruction both release only these grants. #[derive(uniffi::Object)] pub struct DownloadDirectoryScope { grants: Mutex>>, } #[uniffi::export] impl DownloadDirectoryScope { pub fn release(&self) { self.grants .lock() .expect("download directory scope lock") .take(); } } #[derive(Debug, thiserror::Error, uniffi::Error)] #[uniffi(flat_error)] pub enum JsExtensionError { #[error("extension runtime closed")] Closed, #[error("extension runtime busy")] Busy, #[error("execution timeout exceeded")] Timeout, #[error("{0}")] Cancelled(String), #[error("extension did not call registerExtension()")] NotRegistered, #[error("extension function not found: {0}")] MissingFunction(String), #[error("invalid extension input: {0}")] InvalidInput(String), #[error("{0}")] Script(String), #[error("extension environment closed")] EnvironmentClosed, #[error("{0}")] Storage(String), #[error("{0}")] Manifest(String), #[error("{0}")] Gate(String), #[error("{0}")] Network(String), #[error("{0}")] Auth(String), #[error("{0}")] Index(String), } impl From for JsExtensionError { fn from(error: EnvironmentError) -> Self { match error { EnvironmentError::Closed => Self::EnvironmentClosed, EnvironmentError::Runtime(error) => error.into(), EnvironmentError::Storage(error) => Self::Storage(error.to_string()), EnvironmentError::Manifest(error) => Self::Manifest(error.to_string()), EnvironmentError::Gate(message) => Self::Gate(message), EnvironmentError::Network(error) => Self::Network(error.to_string()), EnvironmentError::Auth(error) => Self::Auth(error), EnvironmentError::Index(error) => Self::Index(error), } } } impl From for JsExtensionError { fn from(error: ExtensionError) -> Self { match error { ExtensionError::Closed => Self::Closed, ExtensionError::Busy => Self::Busy, ExtensionError::Timeout => Self::Timeout, ExtensionError::Cancelled(error) => Self::Cancelled(error.to_string()), ExtensionError::NotRegistered => Self::NotRegistered, ExtensionError::MissingFunction(method) => Self::MissingFunction(method), ExtensionError::InvalidInput(message) => Self::InvalidInput(message), ExtensionError::Script(message) => Self::Script(message), } } } /// Migration runtime. Calls must run off the UI thread. The complete extension /// manager and network/file/auth hosts are not yet wired into the mobile app. #[derive(uniffi::Object)] pub struct JsExtension { inner: Arc, } #[uniffi::export] impl JsExtension { pub fn download_state(&self) -> Arc { Arc::new(crate::progress::DownloadState { inner: self.inner.download_state(), }) } #[uniffi::constructor] pub fn new( source: String, settings_json: String, timeout_ms: u64, ) -> Result { let limits = RuntimeLimits { timeout_ms: if timeout_ms == 0 { 30_000 } else { timeout_ms }, ..RuntimeLimits::default() }; Ok(Self { inner: Arc::new(ExtensionRuntime::load(&source, &settings_json, limits)?), }) } pub fn call( &self, method: String, arguments_json: String, lease: Option>, timeout_ms: u64, ) -> Result { let lease = lease.map(|lease| Arc::clone(&lease.inner)); self.inner .call(&method, &arguments_json, lease, timeout_ms) .map_err(Into::into) } pub fn shutdown(&self) { self.inner.shutdown(); } } #[derive(uniffi::Object)] pub struct ExtensionEnvironment { pub(crate) inner: Arc, } #[uniffi::export] impl ExtensionEnvironment { pub fn log_buffer(&self) -> Arc { Arc::new(crate::logging::LogBuffer { inner: self.inner.log_buffer(), }) } pub fn ffmpeg_commands(&self) -> Arc { Arc::new(crate::ffmpeg::FfmpegCommands { inner: self.inner.ffmpeg_commands(), }) } pub fn download_state(&self) -> Arc { Arc::new(crate::progress::DownloadState { inner: self.inner.download_state(), }) } #[uniffi::constructor] pub fn new( data_directory: String, master_key: String, app_version: String, ) -> Result { let master_key = zeroize::Zeroizing::new(master_key); Ok(Self { inner: Arc::new(CoreEnvironment::new( Path::new(&data_directory), &master_key, &app_version, )?), }) } pub fn settings(&self, extension_id: String) -> Result { Ok(serde_json::Value::Object(self.inner.settings(&extension_id)?).to_string()) } pub fn set_allow_private_network(&self, allow: bool) -> Result<(), JsExtensionError> { self.inner .set_allow_private_network(allow) .map_err(Into::into) } pub fn set_network_compatibility_options( &self, allow_http: bool, insecure_tls: bool, ) -> Result<(), JsExtensionError> { self.inner .set_network_compatibility_options(allow_http, insecure_tls) .map_err(Into::into) } pub fn cleanup_connections(&self) -> Result<(), JsExtensionError> { self.inner.cleanup_connections().map_err(Into::into) } pub fn set_allowed_download_directories( &self, directories: Vec, ) -> Result<(), JsExtensionError> { self.inner .set_allowed_download_directories( &directories .into_iter() .map(std::path::PathBuf::from) .collect::>(), ) .map_err(Into::into) } pub fn grant_download_directories( &self, directories: Vec, ) -> Result, JsExtensionError> { let grants = directories .iter() .map(|path| { self.inner .grant_temporary_download_directory(Path::new(path)) }) .collect::, _>>() .map_err(JsExtensionError::Storage)?; Ok(Arc::new(DownloadDirectoryScope { grants: Mutex::new(Some(grants)), })) } pub fn set_runtime_state(&self, raw: String) -> Result<(), JsExtensionError> { self.inner.set_runtime_state(&raw).map_err(Into::into) } pub fn set_session_grant( &self, extension_id: String, grant: String, ) -> Result<(), JsExtensionError> { let grant = zeroize::Zeroizing::new(grant); self.inner .set_session_grant(&extension_id, &grant) .map_err(Into::into) } pub fn resolve_callback_state(&self, state: String) -> Result { self.inner .resolve_callback_state(&state, false) .map_err(Into::into) } pub fn consume_callback_state(&self, state: String) -> Result { self.inner .resolve_callback_state(&state, true) .map_err(Into::into) } pub fn set_auth_code( &self, extension_id: String, code: String, ) -> Result<(), JsExtensionError> { let code = zeroize::Zeroizing::new(code); self.inner .set_auth_code(&extension_id, &code) .map_err(Into::into) } pub fn shutdown(&self) { self.inner.shutdown(); } }