From 36aefc929c7600f5408dbe6399210f2d5b729376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 7 May 2026 23:10:34 -0400 Subject: [PATCH] Store plugin packages --- Cargo.lock | 37 +- Cargo.toml | 1 + crates/ely_app/Cargo.toml | 1 + crates/ely_app/src/services/mod.rs | 1 + .../src/services/plugin_package_store.rs | 361 ++++++++++++++++++ .../ely_app/src/services/plugin_packages.rs | 198 +++++++++- crates/ely_app/src/shell/plugins.rs | 42 +- 7 files changed, 618 insertions(+), 23 deletions(-) create mode 100644 crates/ely_app/src/services/plugin_package_store.rs diff --git a/Cargo.lock b/Cargo.lock index efd158e..ad3b689 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1997,6 +1997,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys 0.5.0", +] + [[package]] name = "dirs" version = "4.0.0" @@ -2022,7 +2031,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", - "redox_users", + "redox_users 0.4.6", "winapi", ] @@ -2034,10 +2043,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.4.6", "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -2196,6 +2217,7 @@ dependencies = [ name = "ely_app" version = "0.1.0" dependencies = [ + "directories", "ely_browser_core", "ely_design_system", "ely_domain", @@ -7210,6 +7232,17 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + [[package]] name = "ref-cast" version = "1.0.25" diff --git a/Cargo.toml b/Cargo.toml index 6fdfb0a..5111ae7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ license = "Apache-2.0" rust-version = "1.95" [workspace.dependencies] +directories = "6.0.0" dpi = "0.1" gpui = "0.2.2" gpui-component = "0.5.1" diff --git a/crates/ely_app/Cargo.toml b/crates/ely_app/Cargo.toml index d0ef03f..9593960 100644 --- a/crates/ely_app/Cargo.toml +++ b/crates/ely_app/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true rust-version.workspace = true [dependencies] +directories.workspace = true ely_browser_core = { path = "../ely_browser_core" } ely_design_system = { path = "../ely_design_system" } ely_domain = { path = "../ely_domain" } diff --git a/crates/ely_app/src/services/mod.rs b/crates/ely_app/src/services/mod.rs index c99d24b..214c801 100644 --- a/crates/ely_app/src/services/mod.rs +++ b/crates/ely_app/src/services/mod.rs @@ -1,3 +1,4 @@ pub mod download_checksums; pub mod download_files; +pub mod plugin_package_store; pub mod plugin_packages; diff --git a/crates/ely_app/src/services/plugin_package_store.rs b/crates/ely_app/src/services/plugin_package_store.rs new file mode 100644 index 0000000..c43a7bc --- /dev/null +++ b/crates/ely_app/src/services/plugin_package_store.rs @@ -0,0 +1,361 @@ +use std::{ + fs, io, + path::{Path, PathBuf}, + time::{SystemTime, SystemTimeError, UNIX_EPOCH}, +}; + +use directories::ProjectDirs; +use ely_domain::PluginManifest; +use thiserror::Error; + +use super::plugin_packages::{PluginPackageError, PluginPackageReader, VerifiedPluginPackage}; + +const PLUGIN_PACKAGES_DIR: &str = "plugin-packages"; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StoredPluginPackage { + manifest: PluginManifest, + package_hash: String, + path: PathBuf, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PluginPackageStore { + root: PathBuf, +} + +#[derive(Debug, Error)] +pub enum PluginPackageStoreError { + #[error("application data directory is unavailable")] + DataDirectoryUnavailable, + + #[error("stored plugin package is invalid: {path}")] + InvalidStoredPackage { path: PathBuf, source: PluginPackageError }, + + #[error("stored plugin package does not match selected package: {path}")] + StoredPackageMismatch { path: PathBuf }, + + #[error("plugin package changed while storing: {path}")] + SourcePackageChanged { path: PathBuf }, + + #[error("plugin package entry cannot be copied through a symlink: {path}")] + SymlinkEntry { path: PathBuf }, + + #[error("plugin package entry type is unsupported: {path}")] + UnsupportedEntry { path: PathBuf }, + + #[error("failed to create plugin package directory: {path}")] + CreateDirectoryFailed { path: PathBuf, source: io::Error }, + + #[error("failed to read plugin package directory: {path}")] + ReadDirectoryFailed { path: PathBuf, source: io::Error }, + + #[error("failed to copy plugin package file from {source_path} to {destination_path}")] + CopyFileFailed { source_path: PathBuf, destination_path: PathBuf, source: io::Error }, + + #[error("failed to move plugin package from {source_path} to {destination_path}")] + MovePackageFailed { source_path: PathBuf, destination_path: PathBuf, source: io::Error }, + + #[error("failed to clean plugin package staging directory: {path}")] + CleanupFailed { path: PathBuf, source: io::Error }, + + #[error("system clock is unavailable for plugin package staging")] + ClockUnavailable { source: SystemTimeError }, +} + +impl StoredPluginPackage { + fn new(manifest: PluginManifest, package_hash: String, path: PathBuf) -> Self { + Self { manifest, package_hash, path } + } + + #[must_use] + pub fn manifest(&self) -> &PluginManifest { + &self.manifest + } +} + +impl PluginPackageStore { + pub fn application() -> Result { + let Some(project_dirs) = ProjectDirs::from("com", "elydora", "ELY Browser") else { + return Err(PluginPackageStoreError::DataDirectoryUnavailable); + }; + + Ok(Self::new(project_dirs.data_local_dir().join(PLUGIN_PACKAGES_DIR))) + } + + #[must_use] + pub fn new(root: PathBuf) -> Self { + Self { root } + } + + pub fn store( + &self, + package: &VerifiedPluginPackage, + ) -> Result { + let destination = self.package_path(package); + if destination.exists() { + return self.read_existing_package(package, destination); + } + + let parent = self.plugin_root(package); + create_directory(&parent)?; + let staging = parent.join(staging_package_name(package.package_hash())?); + if let Err(error) = copy_package_directory(package.source_path(), staging.as_path()) { + return Err(remove_staging_after_error(staging.as_path(), error)); + } + + let staged_package = match PluginPackageReader::read_directory_package(staging.as_path()) { + Ok(staged_package) => staged_package, + Err(source) => { + let error = + PluginPackageStoreError::InvalidStoredPackage { path: staging.clone(), source }; + return Err(remove_staging_after_error(staging.as_path(), error)); + } + }; + if staged_package.manifest().id() != package.manifest().id() + || staged_package.package_hash() != package.package_hash() + { + let error = PluginPackageStoreError::SourcePackageChanged { + path: package.source_path().to_path_buf(), + }; + return Err(remove_staging_after_error(staging.as_path(), error)); + } + + if let Err(source) = fs::rename(staging.as_path(), destination.as_path()) { + let error = PluginPackageStoreError::MovePackageFailed { + source_path: staging.clone(), + destination_path: destination.clone(), + source, + }; + return Err(remove_staging_after_error(staging.as_path(), error)); + } + + Ok(StoredPluginPackage::new( + staged_package.manifest().clone(), + staged_package.package_hash().to_string(), + destination, + )) + } + + fn package_path(&self, package: &VerifiedPluginPackage) -> PathBuf { + self.plugin_root(package).join(format!("{}.rplug", package.package_hash())) + } + + fn plugin_root(&self, package: &VerifiedPluginPackage) -> PathBuf { + self.root.join(package.manifest().id().as_str()) + } + + fn read_existing_package( + &self, + source_package: &VerifiedPluginPackage, + path: PathBuf, + ) -> Result { + let stored_package = + PluginPackageReader::read_directory_package(path.as_path()).map_err(|source| { + PluginPackageStoreError::InvalidStoredPackage { path: path.clone(), source } + })?; + + if stored_package.manifest().id() != source_package.manifest().id() + || stored_package.package_hash() != source_package.package_hash() + { + return Err(PluginPackageStoreError::StoredPackageMismatch { path }); + } + + Ok(StoredPluginPackage::new( + stored_package.manifest().clone(), + stored_package.package_hash().to_string(), + path, + )) + } +} + +fn copy_package_directory( + source_path: &Path, + destination_path: &Path, +) -> Result<(), PluginPackageStoreError> { + create_directory(destination_path)?; + let entries = fs::read_dir(source_path).map_err(|source| { + PluginPackageStoreError::ReadDirectoryFailed { path: source_path.to_path_buf(), source } + })?; + + for entry in entries { + let entry = entry.map_err(|source| PluginPackageStoreError::ReadDirectoryFailed { + path: source_path.to_path_buf(), + source, + })?; + let source_entry = entry.path(); + let destination_entry = destination_path.join(entry.file_name()); + let metadata = fs::symlink_metadata(source_entry.as_path()).map_err(|source| { + PluginPackageStoreError::ReadDirectoryFailed { path: source_entry.clone(), source } + })?; + let file_type = metadata.file_type(); + + if file_type.is_symlink() { + return Err(PluginPackageStoreError::SymlinkEntry { path: source_entry }); + } + if file_type.is_dir() { + copy_package_directory(source_entry.as_path(), destination_entry.as_path())?; + continue; + } + if file_type.is_file() { + fs::copy(source_entry.as_path(), destination_entry.as_path()).map_err(|source| { + PluginPackageStoreError::CopyFileFailed { + source_path: source_entry, + destination_path: destination_entry, + source, + } + })?; + continue; + } + + return Err(PluginPackageStoreError::UnsupportedEntry { path: source_entry }); + } + + Ok(()) +} + +fn create_directory(path: &Path) -> Result<(), PluginPackageStoreError> { + fs::create_dir_all(path).map_err(|source| PluginPackageStoreError::CreateDirectoryFailed { + path: path.to_path_buf(), + source, + }) +} + +fn remove_staging_after_error( + path: &Path, + error: PluginPackageStoreError, +) -> PluginPackageStoreError { + match fs::remove_dir_all(path) { + Ok(()) => error, + Err(source) if source.kind() == io::ErrorKind::NotFound => error, + Err(source) => PluginPackageStoreError::CleanupFailed { path: path.to_path_buf(), source }, + } +} + +fn staging_package_name(package_hash: &str) -> Result { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|source| PluginPackageStoreError::ClockUnavailable { source })? + .as_nanos(); + Ok(format!(".staging-{package_hash}-{nanos}.rplug")) +} + +#[cfg(test)] +mod tests { + use std::{ + error::Error, + fs, + path::{Path, PathBuf}, + time::{SystemTime, UNIX_EPOCH}, + }; + + use sha2::{Digest, Sha256}; + + use super::{PluginPackageStore, PluginPackageStoreError}; + use crate::services::plugin_packages::PluginPackageReader; + + #[test] + fn stores_verified_package_under_plugin_hash_path() -> Result<(), Box> { + let tree = TempTree::new("store")?; + let source_package = write_package(tree.path(), "verified", b"wasm component")?; + let package = PluginPackageReader::read_directory_package(source_package.as_path())?; + let store = PluginPackageStore::new(tree.path().join("store")); + + let stored_package = store.store(&package)?; + + assert_eq!(stored_package.manifest().id().as_str(), "com.elydora.verified"); + assert_eq!(stored_package.package_hash, package.package_hash()); + assert_eq!( + stored_package.path, + tree.path() + .join("store") + .join("com.elydora.verified") + .join(format!("{}.rplug", package.package_hash())) + ); + assert!(stored_package.path.join("plugin.toml").is_file()); + assert!(stored_package.path.join("component.wasm").is_file()); + assert!(stored_package.path.join("signatures").join("ed25519.sig").is_file()); + Ok(()) + } + + #[test] + fn rejects_corrupt_existing_stored_package() -> Result<(), Box> { + let tree = TempTree::new("corrupt")?; + let source_package = write_package(tree.path(), "verified", b"wasm component")?; + let package = PluginPackageReader::read_directory_package(source_package.as_path())?; + let store = PluginPackageStore::new(tree.path().join("store")); + let stored_package = store.store(&package)?; + fs::write(stored_package.path.join("component.wasm"), b"changed")?; + + let error = store + .store(&package) + .err() + .ok_or_else(|| std::io::Error::other("corrupt stored package was accepted"))?; + + assert!(matches!(error, PluginPackageStoreError::InvalidStoredPackage { .. })); + Ok(()) + } + + fn write_package(root: &Path, name: &str, component: &[u8]) -> Result> { + let package = root.join(format!("{name}.rplug")); + fs::create_dir_all(package.join("signatures"))?; + fs::write(package.join("component.wasm"), component)?; + + let checksum = sha256_bytes(component); + let signature = "b".repeat(128); + fs::write(package.join("signatures").join("ed25519.sig"), &signature)?; + fs::write( + package.join("plugin.toml"), + manifest_toml(name, checksum.as_str(), signature.as_str()), + )?; + + Ok(package) + } + + fn manifest_toml(name: &str, checksum: &str, signature: &str) -> String { + format!( + r#" +id = "com.elydora.{name}" +name = "Verified Plugin" +description = "Exports verified content." +author = "Elydora" +homepage = "https://elydora.com/plugins/{name}" +permissions = ["page:metadata"] +contributes = ["command-bar-command"] +min_ely_build = "0.1.0" +checksum = "{checksum}" + +[signature] +algorithm = "ed25519" +value = "{signature}" +"# + ) + } + + fn sha256_bytes(bytes: &[u8]) -> String { + format!("{:x}", Sha256::digest(bytes)) + } + + struct TempTree { + path: PathBuf, + } + + impl TempTree { + fn new(name: &str) -> Result> { + let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); + let path = std::env::temp_dir().join(format!("ely-plugin-store-{name}-{nanos}")); + fs::create_dir_all(&path)?; + Ok(Self { path }) + } + + fn path(&self) -> &Path { + &self.path + } + } + + impl Drop for TempTree { + fn drop(&mut self) { + _ = fs::remove_dir_all(self.path.as_path()); + } + } +} diff --git a/crates/ely_app/src/services/plugin_packages.rs b/crates/ely_app/src/services/plugin_packages.rs index 53d2e1c..7ba08e7 100644 --- a/crates/ely_app/src/services/plugin_packages.rs +++ b/crates/ely_app/src/services/plugin_packages.rs @@ -1,13 +1,21 @@ use std::{ + ffi::OsStr, fs, io::{self, Read}, - path::{Path, PathBuf}, + path::{Component, Path, PathBuf}, }; use ely_domain::PluginManifest; use sha2::{Digest, Sha256}; use thiserror::Error; +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VerifiedPluginPackage { + source_path: PathBuf, + manifest: PluginManifest, + package_hash: String, +} + #[derive(Debug, Error)] pub enum PluginPackageError { #[error("plugin package must use .rplug extension: {path}")] @@ -25,6 +33,15 @@ pub enum PluginPackageError { #[error("failed to read plugin package entry {entry}: {path}")] ReadFailed { entry: &'static str, path: PathBuf, source: io::Error }, + #[error("plugin package entry name must be valid utf-8: {path}")] + InvalidEntryName { path: PathBuf }, + + #[error("plugin package entry cannot be a symlink: {path}")] + SymlinkEntry { path: PathBuf }, + + #[error("plugin package entry type is unsupported: {path}")] + UnsupportedEntry { path: PathBuf }, + #[error("plugin package checksum mismatch for component.wasm")] ChecksumMismatch, @@ -38,7 +55,9 @@ pub enum PluginPackageError { pub struct PluginPackageReader; impl PluginPackageReader { - pub fn read_directory_package(path: &Path) -> Result { + pub fn read_directory_package( + path: &Path, + ) -> Result { require_rplug_directory(path)?; let manifest_path = path.join("plugin.toml"); @@ -60,7 +79,29 @@ impl PluginPackageReader { return Err(PluginPackageError::SignatureMismatch); } - Ok(manifest) + let package_hash = sha256_directory(path)?; + Ok(VerifiedPluginPackage::new(path.to_path_buf(), manifest, package_hash)) + } +} + +impl VerifiedPluginPackage { + fn new(source_path: PathBuf, manifest: PluginManifest, package_hash: String) -> Self { + Self { source_path, manifest, package_hash } + } + + #[must_use] + pub fn source_path(&self) -> &Path { + &self.source_path + } + + #[must_use] + pub fn manifest(&self) -> &PluginManifest { + &self.manifest + } + + #[must_use] + pub fn package_hash(&self) -> &str { + &self.package_hash } } @@ -120,6 +161,140 @@ fn sha256_file(path: &Path) -> Result { Ok(format!("{:x}", hasher.finalize())) } +fn sha256_directory(path: &Path) -> Result { + let mut entries = Vec::new(); + collect_package_entries(path, Path::new(""), &mut entries)?; + entries.sort_by(|left, right| left.relative_path.cmp(&right.relative_path)); + + let mut hasher = Sha256::new(); + for entry in entries { + let relative_path = canonical_relative_path(entry.relative_path.as_path())?; + match entry.kind { + PackageEntryKind::Directory => { + hasher.update(b"dir\0"); + hasher.update(relative_path.as_bytes()); + hasher.update(b"\0"); + } + PackageEntryKind::File => { + hasher.update(b"file\0"); + hasher.update(relative_path.as_bytes()); + hasher.update(b"\0"); + hash_file_content(entry.absolute_path.as_path(), &mut hasher)?; + } + } + } + + Ok(format!("{:x}", hasher.finalize())) +} + +fn collect_package_entries( + root: &Path, + relative_dir: &Path, + entries: &mut Vec, +) -> Result<(), PluginPackageError> { + let absolute_dir = root.join(relative_dir); + for entry in fs::read_dir(absolute_dir.as_path()).map_err(|source| { + PluginPackageError::ReadFailed { entry: "package", path: absolute_dir.clone(), source } + })? { + let entry = entry.map_err(|source| PluginPackageError::ReadFailed { + entry: "package", + path: absolute_dir.clone(), + source, + })?; + let absolute_path = entry.path(); + let relative_path = relative_dir.join(entry.file_name()); + let metadata = fs::symlink_metadata(absolute_path.as_path()).map_err(|source| { + PluginPackageError::ReadFailed { entry: "package", path: absolute_path.clone(), source } + })?; + let file_type = metadata.file_type(); + + if file_type.is_symlink() { + return Err(PluginPackageError::SymlinkEntry { path: absolute_path }); + } + if file_type.is_dir() { + entries.push(PackageEntry::new( + relative_path.clone(), + absolute_path, + PackageEntryKind::Directory, + )); + collect_package_entries(root, relative_path.as_path(), entries)?; + continue; + } + if file_type.is_file() { + entries.push(PackageEntry::new(relative_path, absolute_path, PackageEntryKind::File)); + continue; + } + + return Err(PluginPackageError::UnsupportedEntry { path: absolute_path }); + } + + Ok(()) +} + +fn canonical_relative_path(path: &Path) -> Result { + let mut parts = Vec::new(); + for component in path.components() { + let Component::Normal(value) = component else { + return Err(PluginPackageError::InvalidEntryName { path: path.to_path_buf() }); + }; + parts.push(utf8_entry_name(value, path)?); + } + Ok(parts.join("/")) +} + +fn utf8_entry_name<'a>(value: &'a OsStr, path: &Path) -> Result<&'a str, PluginPackageError> { + value.to_str().ok_or_else(|| PluginPackageError::InvalidEntryName { path: path.to_path_buf() }) +} + +fn hash_file_content(path: &Path, hasher: &mut Sha256) -> Result<(), PluginPackageError> { + let mut file = fs::File::open(path).map_err(|source| PluginPackageError::ReadFailed { + entry: "package", + path: path.to_path_buf(), + source, + })?; + let metadata = file.metadata().map_err(|source| PluginPackageError::ReadFailed { + entry: "package", + path: path.to_path_buf(), + source, + })?; + hasher.update(metadata.len().to_le_bytes()); + hasher.update(b"\0"); + let mut buffer = [0_u8; 8192]; + + loop { + let read = file.read(&mut buffer).map_err(|source| PluginPackageError::ReadFailed { + entry: "package", + path: path.to_path_buf(), + source, + })?; + if read == 0 { + break; + } + hasher.update(&buffer[..read]); + } + + Ok(()) +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct PackageEntry { + relative_path: PathBuf, + absolute_path: PathBuf, + kind: PackageEntryKind, +} + +impl PackageEntry { + fn new(relative_path: PathBuf, absolute_path: PathBuf, kind: PackageEntryKind) -> Self { + Self { relative_path, absolute_path, kind } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum PackageEntryKind { + Directory, + File, +} + #[cfg(test)] mod tests { use std::{ @@ -137,7 +312,8 @@ mod tests { fn reads_verified_directory_package() -> Result<(), Box> { let package = write_package("verified", b"wasm component")?; - let manifest = PluginPackageReader::read_directory_package(&package)?; + let package = PluginPackageReader::read_directory_package(&package)?; + let manifest = package.manifest(); assert_eq!(manifest.id().as_str(), "com.elydora.verified"); assert_eq!(manifest.name(), "Verified Plugin"); @@ -183,6 +359,20 @@ mod tests { Ok(()) } + #[test] + fn package_hash_covers_non_wasm_entries() -> Result<(), Box> { + let package = write_package("package-hash", b"wasm component")?; + let first_hash = + PluginPackageReader::read_directory_package(&package)?.package_hash().to_string(); + fs::write(package.join("README.md"), "updated package metadata")?; + + let second_hash = + PluginPackageReader::read_directory_package(&package)?.package_hash().to_string(); + + assert_ne!(first_hash, second_hash); + Ok(()) + } + fn write_package(name: &str, component: &[u8]) -> Result> { let package = temp_root()?.join(format!("{name}.rplug")); fs::create_dir_all(package.join("signatures"))?; diff --git a/crates/ely_app/src/shell/plugins.rs b/crates/ely_app/src/shell/plugins.rs index 9d58dd1..52c3679 100644 --- a/crates/ely_app/src/shell/plugins.rs +++ b/crates/ely_app/src/shell/plugins.rs @@ -3,23 +3,26 @@ use std::path::PathBuf; use ely_domain::{PluginManifest, PluginPermission}; use gpui::{Context, PathPromptOptions, Window}; -use crate::services::plugin_packages::{PluginPackageError, PluginPackageReader}; +use crate::services::{ + plugin_package_store::PluginPackageStore, + plugin_packages::{PluginPackageError, PluginPackageReader, VerifiedPluginPackage}, +}; use super::{ElyShell, ShellState}; #[derive(Clone, Debug)] pub(super) struct PendingPluginInstall { - manifest: PluginManifest, + package: VerifiedPluginPackage, high_risk_permissions: Vec, } impl PendingPluginInstall { - fn new(manifest: PluginManifest, high_risk_permissions: Vec) -> Self { - Self { manifest, high_risk_permissions } + fn new(package: VerifiedPluginPackage, high_risk_permissions: Vec) -> Self { + Self { package, high_risk_permissions } } pub(super) fn manifest(&self) -> &PluginManifest { - &self.manifest + self.package.manifest() } pub(super) fn high_risk_permissions(&self) -> &[PluginPermission] { @@ -75,7 +78,7 @@ impl ElyShell { return; }; - self.install_plugin_manifest(pending.manifest, true, cx); + self.install_plugin_package(pending.package, true, cx); } pub(super) fn cancel_plugin_install(&mut self, cx: &mut Context) { @@ -85,11 +88,11 @@ impl ElyShell { fn handle_plugin_package_result( &mut self, - result: Result, + result: Result, cx: &mut Context, ) { match result { - Ok(manifest) => self.install_plugin_manifest(manifest, false, cx), + Ok(package) => self.install_plugin_package(package, false, cx), Err(error) => { self.plugin_install_error = Some(error.to_string()); self.pending_plugin_install = None; @@ -98,26 +101,31 @@ impl ElyShell { } } - fn install_plugin_manifest( + fn install_plugin_package( &mut self, - manifest: PluginManifest, + package: VerifiedPluginPackage, high_risk_confirmed: bool, cx: &mut Context, ) { - let high_risk_permissions = manifest.high_risk_permissions().cloned().collect::>(); + let high_risk_permissions = + package.manifest().high_risk_permissions().cloned().collect::>(); if !high_risk_confirmed && !high_risk_permissions.is_empty() { self.pending_plugin_install = - Some(PendingPluginInstall::new(manifest, high_risk_permissions)); + Some(PendingPluginInstall::new(package, high_risk_permissions)); self.plugin_install_error = None; cx.notify(); return; } let result = match &mut self.state { - ShellState::Ready(core) => core - .install_plugin(manifest, high_risk_confirmed) - .map(|_| ()) - .map_err(|error| error.to_string()), + ShellState::Ready(core) => PluginPackageStore::application() + .and_then(|store| store.store(&package)) + .map_err(|error| error.to_string()) + .and_then(|stored_package| { + core.install_plugin(stored_package.manifest().clone(), high_risk_confirmed) + .map(|_| ()) + .map_err(|error| error.to_string()) + }), ShellState::StartupError(message) => Err(message.clone()), }; @@ -129,6 +137,6 @@ impl ElyShell { } } -fn load_plugin_package(path: PathBuf) -> Result { +fn load_plugin_package(path: PathBuf) -> Result { PluginPackageReader::read_directory_package(&path) }