extension: Reorganize capabilities (#35143)

Marshall Bowers created

This PR reorganizes the capabilities within the `extension` crate to
make it easier to add more.

Release Notes:

- N/A

Change summary

crates/extension/src/capabilities.rs                          |  16 
crates/extension/src/capabilities/download_file_capability.rs | 121 +++
crates/extension/src/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(-)

Detailed changes

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),
+}

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<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
+        );
+    }
+}

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<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);
+    }
+}

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::*;

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<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
-        );
-    }
 }