extension: Reorganize capabilities (#35143)
This PR reorganizes the capabilities within the `extension` crate to make it easier to add more. Release Notes: - N/A
This commit is contained in:
parent
6a9a539b10
commit
2a0170dc3c
5 changed files with 259 additions and 165 deletions
16
crates/extension/src/capabilities.rs
Normal file
16
crates/extension/src/capabilities.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
mod download_file_capability;
|
||||
mod process_exec_capability;
|
||||
|
||||
pub use download_file_capability::*;
|
||||
pub use process_exec_capability::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A capability for an extension.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum ExtensionCapability {
|
||||
#[serde(rename = "process:exec")]
|
||||
ProcessExec(ProcessExecCapability),
|
||||
DownloadFile(DownloadFileCapability),
|
||||
}
|
121
crates/extension/src/capabilities/download_file_capability.rs
Normal file
121
crates/extension/src/capabilities/download_file_capability.rs
Normal file
|
@ -0,0 +1,121 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct DownloadFileCapability {
|
||||
pub host: String,
|
||||
pub path: Vec<String>,
|
||||
}
|
||||
|
||||
impl DownloadFileCapability {
|
||||
/// Returns whether the capability allows downloading a file from the given URL.
|
||||
pub fn allows(&self, url: &Url) -> bool {
|
||||
let Some(desired_host) = url.host_str() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(desired_path) = url.path_segments() else {
|
||||
return false;
|
||||
};
|
||||
let desired_path = desired_path.collect::<Vec<_>>();
|
||||
|
||||
if self.host != desired_host && self.host != "*" {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (ix, path_segment) in self.path.iter().enumerate() {
|
||||
if path_segment == "**" {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ix >= desired_path.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if path_segment != "*" && path_segment != desired_path[ix] {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if self.path.len() < desired_path.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_allows() {
|
||||
let capability = DownloadFileCapability {
|
||||
host: "*".to_string(),
|
||||
path: vec!["**".to_string()],
|
||||
};
|
||||
assert_eq!(
|
||||
capability.allows(&"https://example.com/some/path".parse().unwrap()),
|
||||
true
|
||||
);
|
||||
|
||||
let capability = DownloadFileCapability {
|
||||
host: "github.com".to_string(),
|
||||
path: vec!["**".to_string()],
|
||||
};
|
||||
assert_eq!(
|
||||
capability.allows(&"https://github.com/some-owner/some-repo".parse().unwrap()),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
capability.allows(
|
||||
&"https://fake-github.com/some-owner/some-repo"
|
||||
.parse()
|
||||
.unwrap()
|
||||
),
|
||||
false
|
||||
);
|
||||
|
||||
let capability = DownloadFileCapability {
|
||||
host: "github.com".to_string(),
|
||||
path: vec!["specific-owner".to_string(), "*".to_string()],
|
||||
};
|
||||
assert_eq!(
|
||||
capability.allows(&"https://github.com/some-owner/some-repo".parse().unwrap()),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
capability.allows(
|
||||
&"https://github.com/specific-owner/some-repo"
|
||||
.parse()
|
||||
.unwrap()
|
||||
),
|
||||
true
|
||||
);
|
||||
|
||||
let capability = DownloadFileCapability {
|
||||
host: "github.com".to_string(),
|
||||
path: vec!["specific-owner".to_string(), "*".to_string()],
|
||||
};
|
||||
assert_eq!(
|
||||
capability.allows(
|
||||
&"https://github.com/some-owner/some-repo/extra"
|
||||
.parse()
|
||||
.unwrap()
|
||||
),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
capability.allows(
|
||||
&"https://github.com/specific-owner/some-repo/extra"
|
||||
.parse()
|
||||
.unwrap()
|
||||
),
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
116
crates/extension/src/capabilities/process_exec_capability.rs
Normal file
116
crates/extension/src/capabilities/process_exec_capability.rs
Normal file
|
@ -0,0 +1,116 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct ProcessExecCapability {
|
||||
/// The command to execute.
|
||||
pub command: String,
|
||||
/// The arguments to pass to the command. Use `*` for a single wildcard argument.
|
||||
/// If the last element is `**`, then any trailing arguments are allowed.
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
impl ProcessExecCapability {
|
||||
/// Returns whether the capability allows the given command and arguments.
|
||||
pub fn allows(
|
||||
&self,
|
||||
desired_command: &str,
|
||||
desired_args: &[impl AsRef<str> + std::fmt::Debug],
|
||||
) -> bool {
|
||||
if self.command != desired_command && self.command != "*" {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (ix, arg) in self.args.iter().enumerate() {
|
||||
if arg == "**" {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ix >= desired_args.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if arg != "*" && arg != desired_args[ix].as_ref() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if self.args.len() < desired_args.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_allows_with_exact_match() {
|
||||
let capability = ProcessExecCapability {
|
||||
command: "ls".to_string(),
|
||||
args: vec!["-la".to_string()],
|
||||
};
|
||||
|
||||
assert_eq!(capability.allows("ls", &["-la"]), true);
|
||||
assert_eq!(capability.allows("ls", &["-l"]), false);
|
||||
assert_eq!(capability.allows("pwd", &[] as &[&str]), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allows_with_wildcard_arg() {
|
||||
let capability = ProcessExecCapability {
|
||||
command: "git".to_string(),
|
||||
args: vec!["*".to_string()],
|
||||
};
|
||||
|
||||
assert_eq!(capability.allows("git", &["status"]), true);
|
||||
assert_eq!(capability.allows("git", &["commit"]), true);
|
||||
// Too many args.
|
||||
assert_eq!(capability.allows("git", &["status", "-s"]), false);
|
||||
// Wrong command.
|
||||
assert_eq!(capability.allows("npm", &["install"]), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allows_with_double_wildcard() {
|
||||
let capability = ProcessExecCapability {
|
||||
command: "cargo".to_string(),
|
||||
args: vec!["test".to_string(), "**".to_string()],
|
||||
};
|
||||
|
||||
assert_eq!(capability.allows("cargo", &["test"]), true);
|
||||
assert_eq!(capability.allows("cargo", &["test", "--all"]), true);
|
||||
assert_eq!(
|
||||
capability.allows("cargo", &["test", "--all", "--no-fail-fast"]),
|
||||
true
|
||||
);
|
||||
// Wrong first arg.
|
||||
assert_eq!(capability.allows("cargo", &["build"]), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allows_with_mixed_wildcards() {
|
||||
let capability = ProcessExecCapability {
|
||||
command: "docker".to_string(),
|
||||
args: vec!["run".to_string(), "*".to_string(), "**".to_string()],
|
||||
};
|
||||
|
||||
assert_eq!(capability.allows("docker", &["run", "nginx"]), true);
|
||||
assert_eq!(capability.allows("docker", &["run"]), false);
|
||||
assert_eq!(
|
||||
capability.allows("docker", &["run", "ubuntu", "bash"]),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
capability.allows("docker", &["run", "alpine", "sh", "-c", "echo hello"]),
|
||||
true
|
||||
);
|
||||
// Wrong first arg.
|
||||
assert_eq!(capability.allows("docker", &["ps"]), false);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
mod capabilities;
|
||||
pub mod extension_builder;
|
||||
mod extension_events;
|
||||
mod extension_host_proxy;
|
||||
|
@ -16,6 +17,7 @@ use language::LanguageName;
|
|||
use semantic_version::SemanticVersion;
|
||||
use task::{SpawnInTerminal, ZedDebugConfig};
|
||||
|
||||
pub use crate::capabilities::*;
|
||||
pub use crate::extension_events::*;
|
||||
pub use crate::extension_host_proxy::*;
|
||||
pub use crate::extension_manifest::*;
|
||||
|
|
|
@ -11,7 +11,8 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::ExtensionCapability;
|
||||
|
||||
/// This is the old version of the extension manifest, from when it was `extension.json`.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
|
@ -133,103 +134,6 @@ pub fn build_debug_adapter_schema_path(
|
|||
})
|
||||
}
|
||||
|
||||
/// A capability for an extension.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum ExtensionCapability {
|
||||
#[serde(rename = "process:exec")]
|
||||
ProcessExec(ProcessExecCapability),
|
||||
DownloadFile(DownloadFileCapability),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct ProcessExecCapability {
|
||||
/// The command to execute.
|
||||
pub command: String,
|
||||
/// The arguments to pass to the command. Use `*` for a single wildcard argument.
|
||||
/// If the last element is `**`, then any trailing arguments are allowed.
|
||||
pub args: Vec<String>,
|
||||
}
|
||||
|
||||
impl ProcessExecCapability {
|
||||
/// Returns whether the capability allows the given command and arguments.
|
||||
pub fn allows(
|
||||
&self,
|
||||
desired_command: &str,
|
||||
desired_args: &[impl AsRef<str> + std::fmt::Debug],
|
||||
) -> bool {
|
||||
if self.command != desired_command && self.command != "*" {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (ix, arg) in self.args.iter().enumerate() {
|
||||
if arg == "**" {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ix >= desired_args.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if arg != "*" && arg != desired_args[ix].as_ref() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if self.args.len() < desired_args.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct DownloadFileCapability {
|
||||
pub host: String,
|
||||
pub path: Vec<String>,
|
||||
}
|
||||
|
||||
impl DownloadFileCapability {
|
||||
/// Returns whether the capability allows downloading a file from the given URL.
|
||||
pub fn allows(&self, url: &Url) -> bool {
|
||||
let Some(desired_host) = url.host_str() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(desired_path) = url.path_segments() else {
|
||||
return false;
|
||||
};
|
||||
let desired_path = desired_path.collect::<Vec<_>>();
|
||||
|
||||
if self.host != desired_host && self.host != "*" {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (ix, path_segment) in self.path.iter().enumerate() {
|
||||
if path_segment == "**" {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ix >= desired_path.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if path_segment != "*" && path_segment != desired_path[ix] {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if self.path.len() < desired_path.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
pub struct LibManifestEntry {
|
||||
pub kind: Option<ExtensionLibraryKind>,
|
||||
|
@ -379,6 +283,8 @@ fn manifest_from_old_manifest(
|
|||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::ProcessExecCapability;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn extension_manifest() -> ExtensionManifest {
|
||||
|
@ -504,71 +410,4 @@ mod tests {
|
|||
);
|
||||
assert!(manifest.allow_exec("docker", &["ps"]).is_err()); // wrong first arg
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_download_file_capability_allows() {
|
||||
let capability = DownloadFileCapability {
|
||||
host: "*".to_string(),
|
||||
path: vec!["**".to_string()],
|
||||
};
|
||||
assert_eq!(
|
||||
capability.allows(&"https://example.com/some/path".parse().unwrap()),
|
||||
true
|
||||
);
|
||||
|
||||
let capability = DownloadFileCapability {
|
||||
host: "github.com".to_string(),
|
||||
path: vec!["**".to_string()],
|
||||
};
|
||||
assert_eq!(
|
||||
capability.allows(&"https://github.com/some-owner/some-repo".parse().unwrap()),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
capability.allows(
|
||||
&"https://fake-github.com/some-owner/some-repo"
|
||||
.parse()
|
||||
.unwrap()
|
||||
),
|
||||
false
|
||||
);
|
||||
|
||||
let capability = DownloadFileCapability {
|
||||
host: "github.com".to_string(),
|
||||
path: vec!["specific-owner".to_string(), "*".to_string()],
|
||||
};
|
||||
assert_eq!(
|
||||
capability.allows(&"https://github.com/some-owner/some-repo".parse().unwrap()),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
capability.allows(
|
||||
&"https://github.com/specific-owner/some-repo"
|
||||
.parse()
|
||||
.unwrap()
|
||||
),
|
||||
true
|
||||
);
|
||||
|
||||
let capability = DownloadFileCapability {
|
||||
host: "github.com".to_string(),
|
||||
path: vec!["specific-owner".to_string(), "*".to_string()],
|
||||
};
|
||||
assert_eq!(
|
||||
capability.allows(
|
||||
&"https://github.com/some-owner/some-repo/extra"
|
||||
.parse()
|
||||
.unwrap()
|
||||
),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
capability.allows(
|
||||
&"https://github.com/specific-owner/some-repo/extra"
|
||||
.parse()
|
||||
.unwrap()
|
||||
),
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue