Validate plugin manifests
This commit is contained in:
Generated
+3
@@ -2223,7 +2223,10 @@ version = "0.1.0"
|
|||||||
name = "ely_domain"
|
name = "ely_domain"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
|
"toml 1.1.2+spec-1.1.0",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ gpui-component = "0.5.1"
|
|||||||
gpui-component-assets = "0.5.1"
|
gpui-component-assets = "0.5.1"
|
||||||
servo = "0.1.0"
|
servo = "0.1.0"
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
|
semver = "1.0.28"
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
|
toml = "1.1.2"
|
||||||
url = "2.5.4"
|
url = "2.5.4"
|
||||||
uuid = { version = "1.12.1", features = ["v7"] }
|
uuid = { version = "1.12.1", features = ["v7"] }
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ license.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
semver.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
toml.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -25,4 +25,37 @@ pub enum DomainError {
|
|||||||
|
|
||||||
#[error("download progress {received_bytes} exceeds total {total_bytes}")]
|
#[error("download progress {received_bytes} exceeds total {total_bytes}")]
|
||||||
InvalidDownloadProgress { received_bytes: u64, total_bytes: u64 },
|
InvalidDownloadProgress { received_bytes: u64, total_bytes: u64 },
|
||||||
|
|
||||||
|
#[error("invalid plugin manifest: {reason}")]
|
||||||
|
InvalidPluginManifest { reason: String },
|
||||||
|
|
||||||
|
#[error("invalid plugin id: {value}")]
|
||||||
|
InvalidPluginId { value: String },
|
||||||
|
|
||||||
|
#[error("invalid plugin homepage: {value}")]
|
||||||
|
InvalidPluginHomepage { value: String },
|
||||||
|
|
||||||
|
#[error("invalid plugin permission: {value}")]
|
||||||
|
InvalidPluginPermission { value: String },
|
||||||
|
|
||||||
|
#[error("duplicate plugin permission: {value}")]
|
||||||
|
DuplicatePluginPermission { value: String },
|
||||||
|
|
||||||
|
#[error("invalid plugin contribution: {value}")]
|
||||||
|
InvalidPluginContribution { value: String },
|
||||||
|
|
||||||
|
#[error("duplicate plugin contribution: {value}")]
|
||||||
|
DuplicatePluginContribution { value: String },
|
||||||
|
|
||||||
|
#[error("invalid plugin build requirement: {value}")]
|
||||||
|
InvalidPluginBuildRequirement { value: String },
|
||||||
|
|
||||||
|
#[error("invalid plugin checksum: {value}")]
|
||||||
|
InvalidPluginChecksum { value: String },
|
||||||
|
|
||||||
|
#[error("invalid plugin signature algorithm: {value}")]
|
||||||
|
InvalidPluginSignatureAlgorithm { value: String },
|
||||||
|
|
||||||
|
#[error("invalid plugin signature: {value}")]
|
||||||
|
InvalidPluginSignature { value: String },
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ mod download;
|
|||||||
mod error;
|
mod error;
|
||||||
mod history;
|
mod history;
|
||||||
mod identifiers;
|
mod identifiers;
|
||||||
|
mod plugin;
|
||||||
mod profile;
|
mod profile;
|
||||||
mod space;
|
mod space;
|
||||||
mod split;
|
mod split;
|
||||||
@@ -19,6 +20,10 @@ pub use download::{
|
|||||||
pub use error::DomainError;
|
pub use error::DomainError;
|
||||||
pub use history::HistoryEntry;
|
pub use history::HistoryEntry;
|
||||||
pub use identifiers::{DownloadId, ProfileId, SpaceId, SplitId, TabId, WebViewId};
|
pub use identifiers::{DownloadId, ProfileId, SpaceId, SplitId, TabId, WebViewId};
|
||||||
|
pub use plugin::{
|
||||||
|
PluginContributionPoint, PluginId, PluginManifest, PluginPermission, PluginSignature,
|
||||||
|
PluginSignatureAlgorithm,
|
||||||
|
};
|
||||||
pub use profile::{Profile, ProfileKind};
|
pub use profile::{Profile, ProfileKind};
|
||||||
pub use space::{ArchivePolicy, Space};
|
pub use space::{ArchivePolicy, Space};
|
||||||
pub use split::{SplitAxis, SplitLayout, SplitPane};
|
pub use split::{SplitAxis, SplitLayout, SplitPane};
|
||||||
|
|||||||
@@ -0,0 +1,417 @@
|
|||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
use semver::Version;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::DomainError;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct PluginId(String);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct PluginManifest {
|
||||||
|
id: PluginId,
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
author: String,
|
||||||
|
homepage: String,
|
||||||
|
permissions: Vec<PluginPermission>,
|
||||||
|
contributes: Vec<PluginContributionPoint>,
|
||||||
|
min_ely_build: Version,
|
||||||
|
checksum: String,
|
||||||
|
signature: PluginSignature,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum PluginPermission {
|
||||||
|
TabsRead,
|
||||||
|
TabsWrite,
|
||||||
|
SpacesRead,
|
||||||
|
SpacesWrite,
|
||||||
|
BookmarksRead,
|
||||||
|
BookmarksWrite,
|
||||||
|
HistoryRead,
|
||||||
|
DownloadsRead,
|
||||||
|
DownloadsWrite,
|
||||||
|
PageMetadata,
|
||||||
|
PageScreenshot,
|
||||||
|
PageScript,
|
||||||
|
ClipboardRead,
|
||||||
|
ClipboardWrite,
|
||||||
|
FilesystemRead,
|
||||||
|
FilesystemWrite,
|
||||||
|
NetworkFetch,
|
||||||
|
SettingsRead,
|
||||||
|
SettingsWrite,
|
||||||
|
SyncPlugin,
|
||||||
|
UiPanel,
|
||||||
|
UiCommand,
|
||||||
|
UiContextMenu,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum PluginContributionPoint {
|
||||||
|
CommandBarCommand,
|
||||||
|
TabContextMenu,
|
||||||
|
PageContextMenu,
|
||||||
|
SidebarPanel,
|
||||||
|
SettingsPage,
|
||||||
|
StatusBarIndicator,
|
||||||
|
DownloadAction,
|
||||||
|
BookmarkAction,
|
||||||
|
ReadingModeExporter,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum PluginSignatureAlgorithm {
|
||||||
|
Ed25519,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct PluginSignature {
|
||||||
|
algorithm: PluginSignatureAlgorithm,
|
||||||
|
value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct RawPluginManifest {
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
author: String,
|
||||||
|
homepage: String,
|
||||||
|
permissions: Vec<String>,
|
||||||
|
contributes: Vec<String>,
|
||||||
|
min_ely_build: String,
|
||||||
|
checksum: String,
|
||||||
|
signature: RawPluginSignature,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct RawPluginSignature {
|
||||||
|
algorithm: String,
|
||||||
|
value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginId {
|
||||||
|
pub fn parse(value: impl Into<String>) -> Result<Self, DomainError> {
|
||||||
|
let value = value.into();
|
||||||
|
let value = value.trim();
|
||||||
|
if !is_valid_plugin_id(value) {
|
||||||
|
return Err(DomainError::InvalidPluginId { value: value.to_string() });
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(value.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginManifest {
|
||||||
|
pub fn from_toml(value: &str) -> Result<Self, DomainError> {
|
||||||
|
let raw: RawPluginManifest = toml::from_str(value)
|
||||||
|
.map_err(|error| DomainError::InvalidPluginManifest { reason: error.to_string() })?;
|
||||||
|
|
||||||
|
Self::from_raw(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_raw(raw: RawPluginManifest) -> Result<Self, DomainError> {
|
||||||
|
Ok(Self {
|
||||||
|
id: PluginId::parse(raw.id)?,
|
||||||
|
name: non_empty_plugin_field("name", raw.name)?,
|
||||||
|
description: non_empty_plugin_field("description", raw.description)?,
|
||||||
|
author: non_empty_plugin_field("author", raw.author)?,
|
||||||
|
homepage: plugin_homepage(raw.homepage)?,
|
||||||
|
permissions: parse_unique_permissions(&raw.permissions)?,
|
||||||
|
contributes: parse_unique_contributions(&raw.contributes)?,
|
||||||
|
min_ely_build: parse_ely_build(raw.min_ely_build)?,
|
||||||
|
checksum: plugin_checksum(raw.checksum)?,
|
||||||
|
signature: PluginSignature::from_raw(raw.signature)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn id(&self) -> &PluginId {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn description(&self) -> &str {
|
||||||
|
&self.description
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn author(&self) -> &str {
|
||||||
|
&self.author
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn homepage(&self) -> &str {
|
||||||
|
&self.homepage
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn permissions(&self) -> &[PluginPermission] {
|
||||||
|
&self.permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn contributes(&self) -> &[PluginContributionPoint] {
|
||||||
|
&self.contributes
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn min_ely_build(&self) -> &Version {
|
||||||
|
&self.min_ely_build
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn checksum(&self) -> &str {
|
||||||
|
&self.checksum
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn signature(&self) -> &PluginSignature {
|
||||||
|
&self.signature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginPermission {
|
||||||
|
pub fn parse(value: &str) -> Result<Self, DomainError> {
|
||||||
|
match value {
|
||||||
|
"tabs:read" => Ok(Self::TabsRead),
|
||||||
|
"tabs:write" => Ok(Self::TabsWrite),
|
||||||
|
"spaces:read" => Ok(Self::SpacesRead),
|
||||||
|
"spaces:write" => Ok(Self::SpacesWrite),
|
||||||
|
"bookmarks:read" => Ok(Self::BookmarksRead),
|
||||||
|
"bookmarks:write" => Ok(Self::BookmarksWrite),
|
||||||
|
"history:read" => Ok(Self::HistoryRead),
|
||||||
|
"downloads:read" => Ok(Self::DownloadsRead),
|
||||||
|
"downloads:write" => Ok(Self::DownloadsWrite),
|
||||||
|
"page:metadata" => Ok(Self::PageMetadata),
|
||||||
|
"page:screenshot" => Ok(Self::PageScreenshot),
|
||||||
|
"page:script" => Ok(Self::PageScript),
|
||||||
|
"clipboard:read" => Ok(Self::ClipboardRead),
|
||||||
|
"clipboard:write" => Ok(Self::ClipboardWrite),
|
||||||
|
"filesystem:read" => Ok(Self::FilesystemRead),
|
||||||
|
"filesystem:write" => Ok(Self::FilesystemWrite),
|
||||||
|
"network:fetch" => Ok(Self::NetworkFetch),
|
||||||
|
"settings:read" => Ok(Self::SettingsRead),
|
||||||
|
"settings:write" => Ok(Self::SettingsWrite),
|
||||||
|
"sync:plugin" => Ok(Self::SyncPlugin),
|
||||||
|
"ui:panel" => Ok(Self::UiPanel),
|
||||||
|
"ui:command" => Ok(Self::UiCommand),
|
||||||
|
"ui:context_menu" => Ok(Self::UiContextMenu),
|
||||||
|
_ => Err(DomainError::InvalidPluginPermission { value: value.to_string() }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::TabsRead => "tabs:read",
|
||||||
|
Self::TabsWrite => "tabs:write",
|
||||||
|
Self::SpacesRead => "spaces:read",
|
||||||
|
Self::SpacesWrite => "spaces:write",
|
||||||
|
Self::BookmarksRead => "bookmarks:read",
|
||||||
|
Self::BookmarksWrite => "bookmarks:write",
|
||||||
|
Self::HistoryRead => "history:read",
|
||||||
|
Self::DownloadsRead => "downloads:read",
|
||||||
|
Self::DownloadsWrite => "downloads:write",
|
||||||
|
Self::PageMetadata => "page:metadata",
|
||||||
|
Self::PageScreenshot => "page:screenshot",
|
||||||
|
Self::PageScript => "page:script",
|
||||||
|
Self::ClipboardRead => "clipboard:read",
|
||||||
|
Self::ClipboardWrite => "clipboard:write",
|
||||||
|
Self::FilesystemRead => "filesystem:read",
|
||||||
|
Self::FilesystemWrite => "filesystem:write",
|
||||||
|
Self::NetworkFetch => "network:fetch",
|
||||||
|
Self::SettingsRead => "settings:read",
|
||||||
|
Self::SettingsWrite => "settings:write",
|
||||||
|
Self::SyncPlugin => "sync:plugin",
|
||||||
|
Self::UiPanel => "ui:panel",
|
||||||
|
Self::UiCommand => "ui:command",
|
||||||
|
Self::UiContextMenu => "ui:context_menu",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginContributionPoint {
|
||||||
|
pub fn parse(value: &str) -> Result<Self, DomainError> {
|
||||||
|
match value {
|
||||||
|
"command-bar-command" => Ok(Self::CommandBarCommand),
|
||||||
|
"tab-context-menu" => Ok(Self::TabContextMenu),
|
||||||
|
"page-context-menu" => Ok(Self::PageContextMenu),
|
||||||
|
"sidebar-panel" => Ok(Self::SidebarPanel),
|
||||||
|
"settings-page" => Ok(Self::SettingsPage),
|
||||||
|
"status-bar-indicator" => Ok(Self::StatusBarIndicator),
|
||||||
|
"download-action" => Ok(Self::DownloadAction),
|
||||||
|
"bookmark-action" => Ok(Self::BookmarkAction),
|
||||||
|
"reading-mode-exporter" => Ok(Self::ReadingModeExporter),
|
||||||
|
_ => Err(DomainError::InvalidPluginContribution { value: value.to_string() }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::CommandBarCommand => "command-bar-command",
|
||||||
|
Self::TabContextMenu => "tab-context-menu",
|
||||||
|
Self::PageContextMenu => "page-context-menu",
|
||||||
|
Self::SidebarPanel => "sidebar-panel",
|
||||||
|
Self::SettingsPage => "settings-page",
|
||||||
|
Self::StatusBarIndicator => "status-bar-indicator",
|
||||||
|
Self::DownloadAction => "download-action",
|
||||||
|
Self::BookmarkAction => "bookmark-action",
|
||||||
|
Self::ReadingModeExporter => "reading-mode-exporter",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginSignature {
|
||||||
|
fn from_raw(raw: RawPluginSignature) -> Result<Self, DomainError> {
|
||||||
|
let algorithm = PluginSignatureAlgorithm::parse(raw.algorithm.as_str())?;
|
||||||
|
let value = plugin_signature_value(&algorithm, raw.value)?;
|
||||||
|
Ok(Self { algorithm, value })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn algorithm(&self) -> &PluginSignatureAlgorithm {
|
||||||
|
&self.algorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn value(&self) -> &str {
|
||||||
|
&self.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginSignatureAlgorithm {
|
||||||
|
fn parse(value: &str) -> Result<Self, DomainError> {
|
||||||
|
match value {
|
||||||
|
"ed25519" => Ok(Self::Ed25519),
|
||||||
|
_ => Err(DomainError::InvalidPluginSignatureAlgorithm { value: value.to_string() }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Ed25519 => "ed25519",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_unique_permissions(values: &[String]) -> Result<Vec<PluginPermission>, DomainError> {
|
||||||
|
let mut seen = BTreeSet::new();
|
||||||
|
let mut permissions = Vec::with_capacity(values.len());
|
||||||
|
for value in values {
|
||||||
|
let permission = PluginPermission::parse(value.as_str())?;
|
||||||
|
if !seen.insert(permission.as_str()) {
|
||||||
|
return Err(DomainError::DuplicatePluginPermission {
|
||||||
|
value: permission.as_str().to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
permissions.push(permission);
|
||||||
|
}
|
||||||
|
Ok(permissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_unique_contributions(
|
||||||
|
values: &[String],
|
||||||
|
) -> Result<Vec<PluginContributionPoint>, DomainError> {
|
||||||
|
let mut seen = BTreeSet::new();
|
||||||
|
let mut contributions = Vec::with_capacity(values.len());
|
||||||
|
for value in values {
|
||||||
|
let contribution = PluginContributionPoint::parse(value.as_str())?;
|
||||||
|
if !seen.insert(contribution.as_str()) {
|
||||||
|
return Err(DomainError::DuplicatePluginContribution {
|
||||||
|
value: contribution.as_str().to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
contributions.push(contribution);
|
||||||
|
}
|
||||||
|
Ok(contributions)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn non_empty_plugin_field(
|
||||||
|
field: &'static str,
|
||||||
|
value: impl Into<String>,
|
||||||
|
) -> Result<String, DomainError> {
|
||||||
|
let value = value.into();
|
||||||
|
let value = value.trim();
|
||||||
|
if value.is_empty() {
|
||||||
|
return Err(DomainError::EmptyField { field });
|
||||||
|
}
|
||||||
|
Ok(value.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plugin_homepage(value: impl Into<String>) -> Result<String, DomainError> {
|
||||||
|
let value = non_empty_plugin_field("homepage", value)?;
|
||||||
|
let parsed = Url::parse(value.as_str())
|
||||||
|
.map_err(|_| DomainError::InvalidPluginHomepage { value: value.clone() })?;
|
||||||
|
if !matches!(parsed.scheme(), "http" | "https") || parsed.host_str().is_none() {
|
||||||
|
return Err(DomainError::InvalidPluginHomepage { value });
|
||||||
|
}
|
||||||
|
Ok(parsed.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_ely_build(value: impl Into<String>) -> Result<Version, DomainError> {
|
||||||
|
let value = non_empty_plugin_field("min_ely_build", value)?;
|
||||||
|
Version::parse(value.as_str()).map_err(|_| DomainError::InvalidPluginBuildRequirement { value })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plugin_checksum(value: impl Into<String>) -> Result<String, DomainError> {
|
||||||
|
let value = non_empty_plugin_field("checksum", value)?;
|
||||||
|
if !is_hex_of_len(value.as_str(), 64) {
|
||||||
|
return Err(DomainError::InvalidPluginChecksum { value });
|
||||||
|
}
|
||||||
|
Ok(value.to_ascii_lowercase())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plugin_signature_value(
|
||||||
|
algorithm: &PluginSignatureAlgorithm,
|
||||||
|
value: impl Into<String>,
|
||||||
|
) -> Result<String, DomainError> {
|
||||||
|
let value = non_empty_plugin_field("signature", value)?;
|
||||||
|
let valid = match algorithm {
|
||||||
|
PluginSignatureAlgorithm::Ed25519 => is_hex_of_len(value.as_str(), 128),
|
||||||
|
};
|
||||||
|
if !valid {
|
||||||
|
return Err(DomainError::InvalidPluginSignature { value });
|
||||||
|
}
|
||||||
|
Ok(value.to_ascii_lowercase())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_valid_plugin_id(value: &str) -> bool {
|
||||||
|
(3..=128).contains(&value.len())
|
||||||
|
&& value.split('.').all(|segment| {
|
||||||
|
!segment.is_empty()
|
||||||
|
&& segment.chars().all(|ch| {
|
||||||
|
ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-' || ch == '_'
|
||||||
|
})
|
||||||
|
&& segment
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.is_some_and(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit())
|
||||||
|
&& segment
|
||||||
|
.chars()
|
||||||
|
.last()
|
||||||
|
.is_some_and(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_hex_of_len(value: &str, len: usize) -> bool {
|
||||||
|
value.len() == len && value.chars().all(|ch| ch.is_ascii_hexdigit())
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
use std::{error::Error, io};
|
||||||
|
|
||||||
|
use ely_domain::{
|
||||||
|
DomainError, PluginContributionPoint, PluginManifest, PluginPermission,
|
||||||
|
PluginSignatureAlgorithm,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_signed_plugin_manifest() -> Result<(), Box<dyn Error>> {
|
||||||
|
let manifest = PluginManifest::from_toml(valid_manifest().as_str())?;
|
||||||
|
|
||||||
|
assert_eq!(manifest.id().as_str(), "com.elydora.reader");
|
||||||
|
assert_eq!(manifest.name(), "Reader Exporter");
|
||||||
|
assert_eq!(manifest.homepage(), "https://elydora.com/plugins/reader");
|
||||||
|
assert_eq!(
|
||||||
|
manifest.permissions(),
|
||||||
|
&[PluginPermission::PageMetadata, PluginPermission::UiCommand]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
manifest.contributes(),
|
||||||
|
&[PluginContributionPoint::CommandBarCommand, PluginContributionPoint::ReadingModeExporter]
|
||||||
|
);
|
||||||
|
assert_eq!(manifest.min_ely_build().to_string(), "0.1.0");
|
||||||
|
assert_eq!(
|
||||||
|
manifest.checksum(),
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
);
|
||||||
|
assert_eq!(manifest.signature().algorithm(), &PluginSignatureAlgorithm::Ed25519);
|
||||||
|
assert_eq!(
|
||||||
|
manifest.signature().value(),
|
||||||
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_unknown_plugin_permission() -> Result<(), Box<dyn Error>> {
|
||||||
|
let manifest = valid_manifest().replace("page:metadata", "tabs:admin");
|
||||||
|
|
||||||
|
let error = parse_error(manifest.as_str())?;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
error,
|
||||||
|
DomainError::InvalidPluginPermission { value } if value == "tabs:admin"
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_duplicate_plugin_permission() -> Result<(), Box<dyn Error>> {
|
||||||
|
let manifest = valid_manifest()
|
||||||
|
.replace("[\"page:metadata\", \"ui:command\"]", "[\"page:metadata\", \"page:metadata\"]");
|
||||||
|
|
||||||
|
let error = parse_error(manifest.as_str())?;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
error,
|
||||||
|
DomainError::DuplicatePluginPermission { value } if value == "page:metadata"
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_unknown_plugin_contribution() -> Result<(), Box<dyn Error>> {
|
||||||
|
let manifest = valid_manifest().replace("reading-mode-exporter", "hidden-root-hook");
|
||||||
|
|
||||||
|
let error = parse_error(manifest.as_str())?;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
error,
|
||||||
|
DomainError::InvalidPluginContribution { value } if value == "hidden-root-hook"
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_unsigned_plugin_manifest() -> Result<(), Box<dyn Error>> {
|
||||||
|
let manifest = valid_manifest().replace(
|
||||||
|
"value = \"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\"",
|
||||||
|
"value = \"\"",
|
||||||
|
);
|
||||||
|
|
||||||
|
let error = parse_error(manifest.as_str())?;
|
||||||
|
|
||||||
|
assert!(matches!(error, DomainError::EmptyField { field } if field == "signature"));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_mismatched_plugin_checksum() -> Result<(), Box<dyn Error>> {
|
||||||
|
let manifest = valid_manifest().replace(
|
||||||
|
"checksum = \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"",
|
||||||
|
"checksum = \"1234\"",
|
||||||
|
);
|
||||||
|
|
||||||
|
let error = parse_error(manifest.as_str())?;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
error,
|
||||||
|
DomainError::InvalidPluginChecksum { value } if value == "1234"
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_non_http_plugin_homepage() -> Result<(), Box<dyn Error>> {
|
||||||
|
let manifest = valid_manifest().replace(
|
||||||
|
"homepage = \"https://elydora.com/plugins/reader\"",
|
||||||
|
"homepage = \"file:///tmp/reader\"",
|
||||||
|
);
|
||||||
|
|
||||||
|
let error = parse_error(manifest.as_str())?;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
error,
|
||||||
|
DomainError::InvalidPluginHomepage { value } if value == "file:///tmp/reader"
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_extra_manifest_fields() -> Result<(), Box<dyn Error>> {
|
||||||
|
let manifest = valid_manifest().replace("[signature]", "process_escape = true\n\n[signature]");
|
||||||
|
|
||||||
|
let error = parse_error(manifest.as_str())?;
|
||||||
|
|
||||||
|
assert!(matches!(error, DomainError::InvalidPluginManifest { .. }));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_error(manifest: &str) -> Result<DomainError, Box<dyn Error>> {
|
||||||
|
match PluginManifest::from_toml(manifest) {
|
||||||
|
Ok(_) => Err(io::Error::other("manifest parsed successfully").into()),
|
||||||
|
Err(error) => Ok(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_manifest() -> String {
|
||||||
|
r#"
|
||||||
|
id = "com.elydora.reader"
|
||||||
|
name = "Reader Exporter"
|
||||||
|
description = "Exports the active reader view."
|
||||||
|
author = "Elydora"
|
||||||
|
homepage = "https://elydora.com/plugins/reader"
|
||||||
|
permissions = ["page:metadata", "ui:command"]
|
||||||
|
contributes = ["command-bar-command", "reading-mode-exporter"]
|
||||||
|
min_ely_build = "0.1.0"
|
||||||
|
checksum = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||||
|
|
||||||
|
[signature]
|
||||||
|
algorithm = "ed25519"
|
||||||
|
value = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
|
||||||
|
"#
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user