Validate plugin manifests
This commit is contained in:
Generated
+3
@@ -2223,7 +2223,10 @@ version = "0.1.0"
|
||||
name = "ely_domain"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"semver",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -20,7 +20,10 @@ gpui-component = "0.5.1"
|
||||
gpui-component-assets = "0.5.1"
|
||||
servo = "0.1.0"
|
||||
sha2 = "0.10.9"
|
||||
semver = "1.0.28"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
thiserror = "2.0.12"
|
||||
toml = "1.1.2"
|
||||
url = "2.5.4"
|
||||
uuid = { version = "1.12.1", features = ["v7"] }
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
toml.workspace = true
|
||||
url.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
|
||||
@@ -25,4 +25,37 @@ pub enum DomainError {
|
||||
|
||||
#[error("download progress {received_bytes} exceeds total {total_bytes}")]
|
||||
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 history;
|
||||
mod identifiers;
|
||||
mod plugin;
|
||||
mod profile;
|
||||
mod space;
|
||||
mod split;
|
||||
@@ -19,6 +20,10 @@ pub use download::{
|
||||
pub use error::DomainError;
|
||||
pub use history::HistoryEntry;
|
||||
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 space::{ArchivePolicy, Space};
|
||||
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