From 2a0170dc3c553b74d14a13c1c4ccbe8ab5841a3c Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Sat, 26 Jul 2025 18:05:22 -0400 Subject: [PATCH] extension: Reorganize capabilities (#35143) This PR reorganizes the capabilities within the `extension` crate to make it easier to add more. Release Notes: - N/A --- crates/extension/src/capabilities.rs | 16 ++ .../capabilities/download_file_capability.rs | 121 +++++++++++++ .../capabilities/process_exec_capability.rs | 116 ++++++++++++ crates/extension/src/extension.rs | 2 + crates/extension/src/extension_manifest.rs | 169 +----------------- 5 files changed, 259 insertions(+), 165 deletions(-) create mode 100644 crates/extension/src/capabilities.rs create mode 100644 crates/extension/src/capabilities/download_file_capability.rs create mode 100644 crates/extension/src/capabilities/process_exec_capability.rs diff --git a/crates/extension/src/capabilities.rs b/crates/extension/src/capabilities.rs new file mode 100644 index 0000000000..f88f107991 --- /dev/null +++ b/crates/extension/src/capabilities.rs @@ -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), +} diff --git a/crates/extension/src/capabilities/download_file_capability.rs b/crates/extension/src/capabilities/download_file_capability.rs new file mode 100644 index 0000000000..a76755b593 --- /dev/null +++ b/crates/extension/src/capabilities/download_file_capability.rs @@ -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, +} + +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::>(); + + 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 + ); + } +} diff --git a/crates/extension/src/capabilities/process_exec_capability.rs b/crates/extension/src/capabilities/process_exec_capability.rs new file mode 100644 index 0000000000..053a7b212b --- /dev/null +++ b/crates/extension/src/capabilities/process_exec_capability.rs @@ -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, +} + +impl ProcessExecCapability { + /// Returns whether the capability allows the given command and arguments. + pub fn allows( + &self, + desired_command: &str, + desired_args: &[impl AsRef + 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); + } +} diff --git a/crates/extension/src/extension.rs b/crates/extension/src/extension.rs index 8b150e19b9..35f7f41938 100644 --- a/crates/extension/src/extension.rs +++ b/crates/extension/src/extension.rs @@ -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::*; diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index 68d4b9b835..e3235cf561 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -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, -} - -impl ProcessExecCapability { - /// Returns whether the capability allows the given command and arguments. - pub fn allows( - &self, - desired_command: &str, - desired_args: &[impl AsRef + 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, -} - -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::>(); - - 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, @@ -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 - ); - } }