mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-04 15:16:58 +02:00
730 lines
25 KiB
Rust
730 lines
25 KiB
Rust
//! Platform-owned storage and lifecycle context for managed extension runtimes.
|
|
|
|
mod index;
|
|
|
|
use crate::auth::{AuthRegistry, ExtensionAuth, PendingAuthRequest};
|
|
use crate::manifest::{ExtensionManifest, ManifestError};
|
|
use crate::runtime::LoadMode;
|
|
use crate::signed_session::SignedRegistry;
|
|
use crate::storage::{
|
|
ExtensionStore, StorageError, StorageMasterKey, StoreKind, valid_extension_id,
|
|
};
|
|
use crate::{ExtensionError, ExtensionRuntime, ExtensionServices, RuntimeLimits};
|
|
use serde_json::{Map, Value};
|
|
use spotiflac_core::app_version::AppVersion;
|
|
use spotiflac_core::cancellation::{CancellationDomain, CancellationRegistry};
|
|
use spotiflac_core::downloads::DownloadState;
|
|
use spotiflac_core::progress::ProgressRegistry;
|
|
use spotiflac_network::{NetworkService, policy::NetworkPermissions};
|
|
use spotiflac_providers::lyrics::{CallGraph, CallNode, LyricsService};
|
|
use std::cmp::Ordering as Comparison;
|
|
use std::collections::BTreeMap;
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::sync::{Arc, Condvar, LazyLock, Mutex, OnceLock, Weak};
|
|
use std::time::Duration;
|
|
|
|
// Keep these contract versions aligned with Go's supportedRuntimeFeatures.
|
|
pub(crate) static SUPPORTED_RUNTIME_FEATURES: LazyLock<BTreeMap<String, isize>> =
|
|
LazyLock::new(|| {
|
|
[
|
|
("signedSession", 3),
|
|
("sessionRefresh", 1),
|
|
("sessionGrant", 1),
|
|
("globalAction", 1),
|
|
("webviewAuth", 1),
|
|
("downloadSegments", 1),
|
|
("patternedFileTransform", 1),
|
|
("preparedContext", 1),
|
|
]
|
|
.into_iter()
|
|
.map(|(name, version)| (name.to_owned(), version))
|
|
.collect()
|
|
});
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum EnvironmentError {
|
|
#[error("extension environment closed")]
|
|
Closed,
|
|
#[error("{0}")]
|
|
Storage(#[from] StorageError),
|
|
#[error("{0}")]
|
|
Manifest(#[from] ManifestError),
|
|
#[error("{0}")]
|
|
Runtime(#[from] ExtensionError),
|
|
#[error("{0}")]
|
|
Gate(String),
|
|
#[error("network initialization failed: {0}")]
|
|
Network(#[from] std::io::Error),
|
|
#[error("{0}")]
|
|
Auth(String),
|
|
#[error("{0}")]
|
|
Index(String),
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct State {
|
|
pending: usize,
|
|
runtimes: Vec<Weak<ExtensionRuntime>>,
|
|
stores: BTreeMap<String, Arc<ExtensionStore>>,
|
|
lyrics_nodes: BTreeMap<String, CallNode>,
|
|
}
|
|
|
|
/// The master key comes from the native Keychain/Keystore adapter. The directory
|
|
/// must belong to this backend; Go and Rust must not concurrently own live data.
|
|
pub struct ExtensionEnvironment {
|
|
logs: Arc<crate::logging::LogBuffer>,
|
|
ffmpeg: Arc<crate::ffmpeg::CommandRegistry>,
|
|
downloads: Arc<spotiflac_core::downloads::DownloadState>,
|
|
data_directory: PathBuf,
|
|
data_directory_alias: PathBuf,
|
|
master_key: Arc<StorageMasterKey>,
|
|
app_version: AppVersion,
|
|
network: Arc<NetworkService>,
|
|
auth: Arc<AuthRegistry>,
|
|
sessions: Arc<SignedRegistry>,
|
|
files: Arc<crate::files::FileRegistry>,
|
|
isrc: Arc<spotiflac_core::isrc::IndexCache>,
|
|
closed: Arc<AtomicBool>,
|
|
state: Mutex<State>,
|
|
idle: Condvar,
|
|
shutdown_lock: Mutex<()>,
|
|
manager_owned: AtomicBool,
|
|
calls: CallGraph,
|
|
lyrics: OnceLock<Weak<LyricsService>>,
|
|
}
|
|
|
|
impl ExtensionEnvironment {
|
|
pub fn new(
|
|
data_directory: &Path,
|
|
master_key: &str,
|
|
app_version: &str,
|
|
) -> Result<Self, EnvironmentError> {
|
|
// Validate the key before initializing any platform services or files.
|
|
StorageMasterKey::from_base64(master_key)?;
|
|
Self::with_network(
|
|
data_directory,
|
|
master_key,
|
|
app_version,
|
|
NetworkService::new()?,
|
|
)
|
|
}
|
|
|
|
pub fn with_network(
|
|
data_directory: &Path,
|
|
master_key: &str,
|
|
app_version: &str,
|
|
network: Arc<NetworkService>,
|
|
) -> Result<Self, EnvironmentError> {
|
|
let master_key = Arc::new(StorageMasterKey::from_base64(master_key)?);
|
|
if data_directory.as_os_str().is_empty() {
|
|
return Err(StorageError::InvalidPath.into());
|
|
}
|
|
let mut builder = fs::DirBuilder::new();
|
|
builder.recursive(true);
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::DirBuilderExt;
|
|
builder.mode(0o700);
|
|
}
|
|
builder.create(data_directory).map_err(StorageError::from)?;
|
|
let data_directory_alias =
|
|
crate::files::clean(&std::path::absolute(data_directory).map_err(StorageError::from)?);
|
|
let data_directory = fs::canonicalize(data_directory).map_err(StorageError::from)?;
|
|
let auth = Arc::new(AuthRegistry::default());
|
|
let sessions = SignedRegistry::new(&data_directory, Arc::clone(&auth));
|
|
Ok(Self {
|
|
logs: Arc::new(crate::logging::LogBuffer::default()),
|
|
ffmpeg: Arc::new(crate::ffmpeg::CommandRegistry::default()),
|
|
downloads: Arc::new(DownloadState {
|
|
progress: Arc::new(ProgressRegistry::new()),
|
|
cancellation: Arc::new(CancellationRegistry::with_waker(
|
|
CancellationDomain::Download,
|
|
network.cancellation_waker(),
|
|
)),
|
|
}),
|
|
data_directory,
|
|
data_directory_alias,
|
|
master_key,
|
|
app_version: app_version.into(),
|
|
network,
|
|
auth,
|
|
sessions,
|
|
files: Arc::new(crate::files::FileRegistry::default()),
|
|
isrc: Arc::new(spotiflac_core::isrc::IndexCache::default()),
|
|
closed: Arc::new(AtomicBool::new(false)),
|
|
state: Mutex::default(),
|
|
idle: Condvar::new(),
|
|
shutdown_lock: Mutex::new(()),
|
|
manager_owned: AtomicBool::new(false),
|
|
calls: CallGraph::default(),
|
|
lyrics: OnceLock::new(),
|
|
})
|
|
}
|
|
|
|
fn enter(&self) -> Result<Operation<'_>, EnvironmentError> {
|
|
let mut state = self.state.lock().expect("extension environment lock");
|
|
if self.closed.load(Ordering::Acquire) {
|
|
return Err(EnvironmentError::Closed);
|
|
}
|
|
state.pending += 1;
|
|
Ok(Operation(self))
|
|
}
|
|
|
|
fn store(&self, id: &str) -> Result<Arc<ExtensionStore>, EnvironmentError> {
|
|
if !valid_extension_id(id) {
|
|
return Err(StorageError::InvalidExtensionId.into());
|
|
}
|
|
let path = self.data_directory.join(id);
|
|
let mut state = self.state.lock().expect("extension environment lock");
|
|
match fs::symlink_metadata(&path) {
|
|
Ok(metadata) if !metadata.is_dir() => return Err(StorageError::InvalidPath.into()),
|
|
Err(error) if error.kind() != std::io::ErrorKind::NotFound => {
|
|
return Err(StorageError::from(error).into());
|
|
}
|
|
_ => {}
|
|
}
|
|
if let Some(store) = state.stores.get(id) {
|
|
return Ok(Arc::clone(store));
|
|
}
|
|
let store = Arc::new(ExtensionStore::open(
|
|
&path,
|
|
id,
|
|
Some(Arc::clone(&self.master_key)),
|
|
)?);
|
|
state.stores.insert(id.to_owned(), Arc::clone(&store));
|
|
Ok(store)
|
|
}
|
|
|
|
pub fn settings(&self, id: &str) -> Result<Map<String, Value>, EnvironmentError> {
|
|
let _operation = self.enter()?;
|
|
Ok(self.store(id)?.all(StoreKind::Settings)?)
|
|
}
|
|
|
|
pub fn set_setting(&self, id: &str, key: &str, value: Value) -> Result<(), EnvironmentError> {
|
|
let _operation = self.enter()?;
|
|
Ok(self.store(id)?.set(StoreKind::Settings, key, value)?)
|
|
}
|
|
|
|
pub fn set_allow_private_network(&self, allow: bool) -> Result<(), EnvironmentError> {
|
|
let _operation = self.enter()?;
|
|
self.network.set_allow_private_network(allow);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_network_compatibility_options(
|
|
&self,
|
|
allow_http: bool,
|
|
insecure_tls: bool,
|
|
) -> Result<(), EnvironmentError> {
|
|
let _operation = self.enter()?;
|
|
self.network
|
|
.set_network_compatibility_options(allow_http, insecure_tls);
|
|
Ok(())
|
|
}
|
|
|
|
/// Recycle connections without cancelling active requests or changing policy.
|
|
pub fn cleanup_connections(&self) -> Result<(), EnvironmentError> {
|
|
let _operation = self.enter()?;
|
|
self.network.reset_connections();
|
|
Ok(())
|
|
}
|
|
|
|
/// Trusted native composition reuses the same transport and network policy.
|
|
pub fn network_service(&self) -> Result<Arc<NetworkService>, EnvironmentError> {
|
|
let _operation = self.enter()?;
|
|
Ok(Arc::clone(&self.network))
|
|
}
|
|
|
|
pub fn get_app_version(&self) -> Result<String, EnvironmentError> {
|
|
let _operation = self.enter()?;
|
|
Ok(self.app_version.get())
|
|
}
|
|
|
|
pub(crate) fn shared_app_version(&self) -> AppVersion {
|
|
self.app_version.clone()
|
|
}
|
|
|
|
pub fn set_allowed_download_directories(
|
|
&self,
|
|
directories: &[PathBuf],
|
|
) -> Result<(), EnvironmentError> {
|
|
let _operation = self.enter()?;
|
|
self.files
|
|
.set_allowed_directories(directories)
|
|
.map_err(StorageError::from)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn replace_settings(
|
|
&self,
|
|
id: &str,
|
|
values: Map<String, Value>,
|
|
) -> Result<(), EnvironmentError> {
|
|
let _operation = self.enter()?;
|
|
Ok(self.store(id)?.replace(StoreKind::Settings, values)?)
|
|
}
|
|
|
|
pub fn load(
|
|
&self,
|
|
manifest_json: &str,
|
|
source: &str,
|
|
limits: RuntimeLimits,
|
|
) -> Result<Arc<ExtensionRuntime>, EnvironmentError> {
|
|
if self.closed.load(Ordering::Acquire) {
|
|
return Err(EnvironmentError::Closed);
|
|
}
|
|
if self.manager_owned.load(Ordering::Acquire) {
|
|
return Err(EnvironmentError::Gate(
|
|
"installed runtimes must be loaded through their extension manager".into(),
|
|
));
|
|
}
|
|
self.load_internal(
|
|
manifest_json,
|
|
source,
|
|
limits,
|
|
LoadMode::Initialize,
|
|
None,
|
|
None,
|
|
)
|
|
}
|
|
|
|
pub(crate) fn load_registered_with_lease(
|
|
&self,
|
|
manifest_json: &str,
|
|
source: &str,
|
|
limits: RuntimeLimits,
|
|
mode: LoadMode,
|
|
lease: Option<Arc<spotiflac_core::cancellation::RequestLease>>,
|
|
) -> Result<Arc<ExtensionRuntime>, EnvironmentError> {
|
|
self.load_internal(manifest_json, source, limits, mode, None, lease)
|
|
}
|
|
|
|
pub(crate) fn load_isolated(
|
|
&self,
|
|
manifest_json: &str,
|
|
source: &str,
|
|
limits: RuntimeLimits,
|
|
primary: &ExtensionRuntime,
|
|
) -> Result<Arc<ExtensionRuntime>, EnvironmentError> {
|
|
self.load_internal(
|
|
manifest_json,
|
|
source,
|
|
limits,
|
|
LoadMode::Register,
|
|
Some(primary),
|
|
None,
|
|
)
|
|
}
|
|
|
|
fn load_internal(
|
|
&self,
|
|
manifest_json: &str,
|
|
source: &str,
|
|
limits: RuntimeLimits,
|
|
load_mode: LoadMode,
|
|
primary: Option<&ExtensionRuntime>,
|
|
startup_lease: Option<Arc<spotiflac_core::cancellation::RequestLease>>,
|
|
) -> Result<Arc<ExtensionRuntime>, EnvironmentError> {
|
|
let _operation = self.enter()?;
|
|
if manifest_json.len() > 1024 * 1024 {
|
|
return Err(EnvironmentError::Gate(
|
|
"invalid extension package: manifest.json is too large".to_owned(),
|
|
));
|
|
}
|
|
let manifest = ExtensionManifest::parse(manifest_json)?;
|
|
validate_gates(
|
|
&manifest,
|
|
&self.app_version.get(),
|
|
&SUPPORTED_RUNTIME_FEATURES,
|
|
)
|
|
.map_err(EnvironmentError::Gate)?;
|
|
let store = self.store(&manifest.name)?;
|
|
let mut settings = store.all(StoreKind::Settings)?;
|
|
settings.retain(|key, _| !key.starts_with('_'));
|
|
let isolated = primary.is_some();
|
|
let compiled_source = primary.map(|runtime| Arc::clone(&runtime.compiled_source));
|
|
let network = primary
|
|
.and_then(ExtensionRuntime::network_session)
|
|
.unwrap_or_else(|| {
|
|
self.network.session(
|
|
NetworkPermissions {
|
|
domains: manifest.permissions.network.clone().unwrap_or_default(),
|
|
allow_http: manifest.permissions.allow_http,
|
|
},
|
|
network_timeout(manifest.capabilities.get("networkTimeoutSeconds")),
|
|
)
|
|
});
|
|
// Legacy metadata helpers do not require the general file permission.
|
|
// They receive read access to this sandbox and native-granted roots.
|
|
let legacy_files = self
|
|
.files
|
|
.extension_with_alias(
|
|
&self.data_directory.join(&manifest.name),
|
|
&self.data_directory_alias.join(&manifest.name),
|
|
)
|
|
.map_err(StorageError::from)?;
|
|
let services = ExtensionServices {
|
|
legacy_backend: true,
|
|
lyrics: self.lyrics.get().cloned().unwrap_or_default(),
|
|
lyrics_node: self.lyrics_node(&manifest.name, isolated),
|
|
lyrics_parent: isolated.then(|| self.lyrics_node(&manifest.name, false)),
|
|
legacy_files: Some(Arc::clone(&legacy_files)),
|
|
isrc: Arc::clone(&self.isrc),
|
|
logs: Arc::clone(&self.logs),
|
|
extension_id: manifest.name.clone(),
|
|
ffmpeg: Arc::clone(&self.ffmpeg),
|
|
raw_ffmpeg_stub: manifest
|
|
.capabilities
|
|
.get("rawFfmpeg")
|
|
.and_then(Value::as_bool)
|
|
.unwrap_or(false),
|
|
downloads: Arc::clone(&self.downloads),
|
|
transfer_policy: crate::transfer_policy::DownloadTransferPolicy::from_capabilities(
|
|
&manifest.capabilities,
|
|
),
|
|
storage: manifest.permissions.storage.then_some(store),
|
|
files: manifest.permissions.file.then_some(legacy_files),
|
|
auth: manifest.permissions.storage.then(|| {
|
|
Arc::new(ExtensionAuth::new(
|
|
&manifest.name,
|
|
Arc::clone(&self.auth),
|
|
Arc::clone(&network),
|
|
self.app_version.clone(),
|
|
))
|
|
}),
|
|
auth_registry: Some(Arc::clone(&self.auth)),
|
|
session: manifest
|
|
.signed_session
|
|
.clone()
|
|
.map(|config| {
|
|
self.sessions
|
|
.session(&manifest.name, config, Arc::clone(&network))
|
|
})
|
|
.transpose()
|
|
.map_err(EnvironmentError::Auth)?,
|
|
network: Some(network),
|
|
app_version: self.app_version.clone(),
|
|
parent_closed: Some(Arc::clone(&self.closed)),
|
|
startup_lease,
|
|
initialize_empty_settings: false,
|
|
load_mode,
|
|
compiled_source,
|
|
};
|
|
let runtime = Arc::new(ExtensionRuntime::load_with_services(
|
|
source,
|
|
&Value::Object(settings).to_string(),
|
|
limits,
|
|
services,
|
|
)?);
|
|
let mut state = self.state.lock().expect("extension environment lock");
|
|
if self.closed.load(Ordering::Acquire) {
|
|
drop(state);
|
|
runtime.shutdown();
|
|
return Err(EnvironmentError::Closed);
|
|
}
|
|
state.runtimes.retain(|runtime| runtime.strong_count() > 0);
|
|
state.runtimes.push(Arc::downgrade(&runtime));
|
|
Ok(runtime)
|
|
}
|
|
|
|
/// Interrupt every child and in-progress initialization, then join workers.
|
|
/// Manager-driven normal unloading should run cleanup before this operation.
|
|
pub fn shutdown(&self) {
|
|
self.closed.store(true, Ordering::Release);
|
|
self.app_version.close();
|
|
self.logs.shutdown();
|
|
self.ffmpeg.shutdown();
|
|
self.downloads.shutdown();
|
|
self.auth.shutdown();
|
|
self.sessions.shutdown();
|
|
let _shutdown = self.shutdown_lock.lock().expect("extension shutdown lock");
|
|
let runtimes = {
|
|
let mut state = self.state.lock().expect("extension environment lock");
|
|
std::mem::take(&mut state.runtimes)
|
|
};
|
|
for runtime in runtimes.into_iter().filter_map(|runtime| runtime.upgrade()) {
|
|
runtime.shutdown();
|
|
}
|
|
let mut state = self.state.lock().expect("extension environment lock");
|
|
while state.pending > 0 {
|
|
state = self.idle.wait(state).expect("extension environment idle");
|
|
}
|
|
state.lyrics_nodes.clear();
|
|
self.isrc.clear();
|
|
}
|
|
}
|
|
|
|
impl ExtensionEnvironment {
|
|
pub(crate) fn attach_lyrics(&self, lyrics: &Arc<LyricsService>) {
|
|
self.lyrics
|
|
.set(Arc::downgrade(lyrics))
|
|
.expect("lyrics owner already configured");
|
|
}
|
|
|
|
pub(crate) fn lyrics_calls(&self) -> CallGraph {
|
|
self.calls.clone()
|
|
}
|
|
|
|
pub(crate) fn lyrics_node(&self, id: &str, isolated: bool) -> CallNode {
|
|
if isolated {
|
|
return self.calls.node();
|
|
}
|
|
self.state
|
|
.lock()
|
|
.expect("extension environment lock")
|
|
.lyrics_nodes
|
|
.entry(id.to_owned())
|
|
.or_insert_with(|| self.calls.node())
|
|
.clone()
|
|
}
|
|
|
|
pub(crate) fn attach_manager(&self) {
|
|
self.manager_owned.store(true, Ordering::Release);
|
|
}
|
|
|
|
pub(crate) fn is_closed(&self) -> bool {
|
|
self.closed.load(Ordering::Acquire)
|
|
}
|
|
|
|
pub(crate) fn closed_flag(&self) -> Arc<AtomicBool> {
|
|
Arc::clone(&self.closed)
|
|
}
|
|
pub(crate) fn data_directory(&self) -> &Path {
|
|
&self.data_directory
|
|
}
|
|
|
|
/// Trusted root operations share download grants without widening any VM's
|
|
/// sandbox. Relative native paths resolve inside the application's data root.
|
|
pub(crate) fn native_files(&self) -> Result<Arc<crate::files::ExtensionFiles>, String> {
|
|
if self.is_closed() {
|
|
return Err("extension environment closed".into());
|
|
}
|
|
self.files
|
|
.extension_with_alias(&self.data_directory, &self.data_directory_alias)
|
|
.map_err(|error| error.to_string())
|
|
}
|
|
|
|
/// Trusted native callers retain this grant until finalization completes.
|
|
pub fn grant_temporary_download_directory(
|
|
&self,
|
|
path: &Path,
|
|
) -> Result<crate::files::TemporaryGrant, String> {
|
|
let _operation = self.enter().map_err(|error| error.to_string())?;
|
|
self.files
|
|
.grant_temporary_directory(path)
|
|
.map_err(|error| error.to_string())
|
|
}
|
|
|
|
pub(crate) fn validate_post_process_path(
|
|
&self,
|
|
id: &str,
|
|
input: &str,
|
|
path: &str,
|
|
) -> Result<(), String> {
|
|
let mut bases = vec![self.data_directory.join(id)];
|
|
// Native platforms may supply a trusted alias such as /var/... while
|
|
// the store owns /private/var/.... Accept that spelling only while it
|
|
// still resolves to this extension's actual data directory.
|
|
let alias = self.data_directory_alias.join(id);
|
|
if alias != bases[0] && fs::canonicalize(&alias).is_ok_and(|path| path == bases[0]) {
|
|
bases.push(alias);
|
|
}
|
|
if !input.is_empty() {
|
|
let input = crate::files::clean(Path::new(input));
|
|
bases.insert(
|
|
0,
|
|
input
|
|
.parent()
|
|
.filter(|path| !path.as_os_str().is_empty())
|
|
.unwrap_or(Path::new("."))
|
|
.into(),
|
|
);
|
|
}
|
|
self.files.validate_native_replacement(path, &bases)
|
|
}
|
|
|
|
pub(crate) fn remove_data(&self, id: &str) -> Result<(), EnvironmentError> {
|
|
let _operation = self.enter()?;
|
|
if !valid_extension_id(id) {
|
|
return Err(StorageError::InvalidExtensionId.into());
|
|
}
|
|
let mut state = self.state.lock().expect("extension environment lock");
|
|
if let Some(store) = state.stores.remove(id) {
|
|
store.close();
|
|
}
|
|
self.auth.clear(id);
|
|
self.sessions.forget_extension(id);
|
|
let path = self.data_directory.join(id);
|
|
match fs::symlink_metadata(&path) {
|
|
Ok(metadata) if metadata.is_dir() => {
|
|
fs::remove_dir_all(path).map_err(StorageError::from)?
|
|
}
|
|
Ok(_) => fs::remove_file(path).map_err(StorageError::from)?,
|
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
|
Err(error) => return Err(StorageError::from(error).into()),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn log_buffer(&self) -> Arc<crate::logging::LogBuffer> {
|
|
Arc::clone(&self.logs)
|
|
}
|
|
|
|
pub fn repository(
|
|
&self,
|
|
directory: &Path,
|
|
) -> Result<crate::repository::ExtensionRepository, crate::repository::RepositoryError> {
|
|
let _operation = self
|
|
.enter()
|
|
.map_err(|e| crate::repository::RepositoryError(e.to_string()))?;
|
|
crate::repository::ExtensionRepository::with_network(directory, Arc::clone(&self.network))
|
|
.map(|repository| repository.attach_environment(Arc::clone(&self.closed)))
|
|
}
|
|
|
|
pub fn ffmpeg_commands(&self) -> Arc<crate::ffmpeg::CommandRegistry> {
|
|
Arc::clone(&self.ffmpeg)
|
|
}
|
|
|
|
pub fn download_state(&self) -> Arc<spotiflac_core::downloads::DownloadState> {
|
|
Arc::clone(&self.downloads)
|
|
}
|
|
|
|
pub fn set_runtime_state(&self, raw: &str) -> Result<(), EnvironmentError> {
|
|
let _operation = self.enter()?;
|
|
self.sessions
|
|
.set_runtime_state(raw)
|
|
.map_err(EnvironmentError::Auth)
|
|
}
|
|
|
|
pub fn set_session_grant(&self, id: &str, grant: &str) -> Result<(), EnvironmentError> {
|
|
let _operation = self.enter()?;
|
|
self.sessions
|
|
.set_grant(id, grant)
|
|
.map_err(EnvironmentError::Auth)
|
|
}
|
|
|
|
pub fn pending_auth(&self, id: &str) -> Result<Option<PendingAuthRequest>, EnvironmentError> {
|
|
let _operation = self.enter()?;
|
|
Ok(self.auth.pending(id.trim()))
|
|
}
|
|
|
|
pub fn resolve_callback_state(
|
|
&self,
|
|
state: &str,
|
|
consume: bool,
|
|
) -> Result<String, EnvironmentError> {
|
|
let _operation = self.enter()?;
|
|
self.auth
|
|
.resolve_callback(state, consume)
|
|
.map_err(EnvironmentError::Auth)
|
|
}
|
|
|
|
pub fn set_auth_code(&self, id: &str, code: &str) -> Result<(), EnvironmentError> {
|
|
let _operation = self.enter()?;
|
|
self.auth.set_code(id, code).map_err(EnvironmentError::Auth)
|
|
}
|
|
}
|
|
|
|
fn network_timeout(value: Option<&Value>) -> Duration {
|
|
let seconds = match value {
|
|
Some(Value::String(value)) => value.trim().parse::<i64>().unwrap_or(0),
|
|
Some(Value::Number(value)) => value.as_f64().unwrap_or(0.0) as i64,
|
|
_ => 0,
|
|
};
|
|
Duration::from_secs(if seconds <= 0 {
|
|
30
|
|
} else {
|
|
seconds.clamp(5, 300) as u64
|
|
})
|
|
}
|
|
|
|
impl Drop for ExtensionEnvironment {
|
|
fn drop(&mut self) {
|
|
self.shutdown();
|
|
}
|
|
}
|
|
|
|
struct Operation<'a>(&'a ExtensionEnvironment);
|
|
impl Drop for Operation<'_> {
|
|
fn drop(&mut self) {
|
|
let mut state = self.0.state.lock().expect("extension environment lock");
|
|
state.pending -= 1;
|
|
self.0.idle.notify_all();
|
|
}
|
|
}
|
|
|
|
pub fn validate_gates(
|
|
manifest: &ExtensionManifest,
|
|
app_version: &str,
|
|
features: &BTreeMap<String, isize>,
|
|
) -> Result<(), String> {
|
|
let minimum = manifest.min_app_version.trim();
|
|
let installed = app_version.trim();
|
|
if !minimum.is_empty() && !installed.is_empty() && compare_versions(installed, minimum).is_lt()
|
|
{
|
|
return Err(format!(
|
|
"requires app {minimum} or later (installed: {installed})"
|
|
));
|
|
}
|
|
for raw in &manifest.required_runtime_features {
|
|
let mut name = raw.trim();
|
|
if name.is_empty() {
|
|
continue;
|
|
}
|
|
let mut wanted = 1;
|
|
if let Some((feature, version)) = name.rsplit_once('@')
|
|
&& !feature.is_empty()
|
|
{
|
|
wanted = version
|
|
.parse::<isize>()
|
|
.ok()
|
|
.filter(|version| *version > 0)
|
|
.unwrap_or(1);
|
|
name = feature;
|
|
}
|
|
let Some(provided) = features.get(name) else {
|
|
return Err(format!(
|
|
"requires runtime feature {} this app build does not provide",
|
|
serde_json::to_string(name).expect("JSON string")
|
|
));
|
|
};
|
|
if *provided < wanted {
|
|
return Err(format!(
|
|
"requires runtime feature {name}@{wanted} (app provides @{provided})"
|
|
));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn compare_versions(first: &str, second: &str) -> Comparison {
|
|
fn parts(version: &str) -> impl Iterator<Item = isize> + '_ {
|
|
version
|
|
.strip_prefix('v')
|
|
.unwrap_or(version)
|
|
.split('.')
|
|
.map(|part| {
|
|
part.parse()
|
|
.unwrap_or_else(|error: std::num::ParseIntError| match error.kind() {
|
|
std::num::IntErrorKind::PosOverflow => isize::MAX,
|
|
std::num::IntErrorKind::NegOverflow => isize::MIN,
|
|
_ => 0,
|
|
})
|
|
})
|
|
}
|
|
let mut first = parts(first);
|
|
let mut second = parts(second);
|
|
loop {
|
|
let (left, right) = (first.next(), second.next());
|
|
if left.is_none() && right.is_none() {
|
|
return Comparison::Equal;
|
|
}
|
|
let result = left.unwrap_or(0).cmp(&right.unwrap_or(0));
|
|
if !result.is_eq() {
|
|
return result;
|
|
}
|
|
}
|
|
}
|