Detailed changes
@@ -5385,11 +5385,13 @@ dependencies = [
"log",
"lsp",
"parking_lot",
+ "pretty_assertions",
"semantic_version",
"serde",
"serde_json",
"task",
"toml 0.8.20",
+ "url",
"util",
"wasm-encoder 0.221.3",
"wasmparser 0.221.3",
@@ -32,7 +32,11 @@ serde.workspace = true
serde_json.workspace = true
task.workspace = true
toml.workspace = true
+url.workspace = true
util.workspace = true
wasm-encoder.workspace = true
wasmparser.workspace = true
workspace-hack.workspace = true
+
+[dev-dependencies]
+pretty_assertions.workspace = true
@@ -11,6 +11,7 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
+use url::Url;
/// This is the old version of the extension manifest, from when it was `extension.json`.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
@@ -103,6 +104,7 @@ impl ExtensionManifest {
ExtensionCapability::ProcessExec(capability) => {
capability.allows(desired_command, desired_args)
}
+ _ => false,
});
if !is_allowed {
@@ -133,10 +135,11 @@ pub fn build_debug_adapter_schema_path(
/// A capability for an extension.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
-#[serde(tag = "kind")]
+#[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)]
@@ -182,6 +185,51 @@ impl ProcessExecCapability {
}
}
+#[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>,
@@ -329,6 +377,8 @@ fn manifest_from_old_manifest(
#[cfg(test)]
mod tests {
+ use pretty_assertions::assert_eq;
+
use super::*;
fn extension_manifest() -> ExtensionManifest {
@@ -380,7 +430,7 @@ mod tests {
}
#[test]
- fn test_allow_exact_match() {
+ fn test_allow_exec_exact_match() {
let manifest = ExtensionManifest {
capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
command: "ls".to_string(),
@@ -395,7 +445,7 @@ mod tests {
}
#[test]
- fn test_allow_wildcard_arg() {
+ fn test_allow_exec_wildcard_arg() {
let manifest = ExtensionManifest {
capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
command: "git".to_string(),
@@ -411,7 +461,7 @@ mod tests {
}
#[test]
- fn test_allow_double_wildcard() {
+ fn test_allow_exec_double_wildcard() {
let manifest = ExtensionManifest {
capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
command: "cargo".to_string(),
@@ -431,7 +481,7 @@ mod tests {
}
#[test]
- fn test_allow_mixed_wildcards() {
+ fn test_allow_exec_mixed_wildcards() {
let manifest = ExtensionManifest {
capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
command: "docker".to_string(),
@@ -454,4 +504,71 @@ 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
+ );
+ }
}
@@ -2,6 +2,7 @@ use std::sync::Arc;
use anyhow::{Result, bail};
use extension::{ExtensionCapability, ExtensionManifest};
+use url::Url;
pub struct CapabilityGranter {
granted_capabilities: Vec<ExtensionCapability>,
@@ -33,6 +34,7 @@ impl CapabilityGranter {
ExtensionCapability::ProcessExec(capability) => {
capability.allows(desired_command, desired_args)
}
+ _ => false,
});
if !is_allowed {
@@ -43,6 +45,24 @@ impl CapabilityGranter {
Ok(())
}
+
+ pub fn grant_download_file(&self, desired_url: &Url) -> Result<()> {
+ let is_allowed = self
+ .granted_capabilities
+ .iter()
+ .any(|capability| match capability {
+ ExtensionCapability::DownloadFile(capability) => capability.allows(desired_url),
+ _ => false,
+ });
+
+ if !is_allowed {
+ bail!(
+ "capability for download_file {desired_url} is not granted by the extension host",
+ );
+ }
+
+ Ok(())
+ }
}
#[cfg(test)]
@@ -7,9 +7,9 @@ use async_trait::async_trait;
use dap::{DebugRequest, StartDebuggingRequestArgumentsRequest};
use extension::{
CodeLabel, Command, Completion, ContextServerConfiguration, DebugAdapterBinary,
- DebugTaskDefinition, ExtensionCapability, ExtensionHostProxy, KeyValueStoreDelegate,
- ProcessExecCapability, ProjectDelegate, SlashCommand, SlashCommandArgumentCompletion,
- SlashCommandOutput, Symbol, WorktreeDelegate,
+ DebugTaskDefinition, DownloadFileCapability, ExtensionCapability, ExtensionHostProxy,
+ KeyValueStoreDelegate, ProcessExecCapability, ProjectDelegate, SlashCommand,
+ SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate,
};
use fs::{Fs, normalize_path};
use futures::future::LocalBoxFuture;
@@ -576,10 +576,16 @@ impl WasmHost {
node_runtime,
proxy,
release_channel: ReleaseChannel::global(cx),
- granted_capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
- command: "*".to_string(),
- args: vec!["**".to_string()],
- })],
+ granted_capabilities: vec![
+ ExtensionCapability::ProcessExec(ProcessExecCapability {
+ command: "*".to_string(),
+ args: vec!["**".to_string()],
+ }),
+ ExtensionCapability::DownloadFile(DownloadFileCapability {
+ host: "*".to_string(),
+ path: vec!["**".to_string()],
+ }),
+ ],
_main_thread_message_task: task,
main_thread_message_tx: tx,
})
@@ -30,6 +30,7 @@ use std::{
sync::{Arc, OnceLock},
};
use task::{SpawnInTerminal, ZedDebugConfig};
+use url::Url;
use util::{archive::extract_zip, fs::make_file_executable, maybe};
use wasmtime::component::{Linker, Resource};
@@ -1011,6 +1012,9 @@ impl ExtensionImports for WasmState {
file_type: DownloadedFileType,
) -> wasmtime::Result<Result<(), String>> {
maybe!(async {
+ let parsed_url = Url::parse(&url)?;
+ self.capability_granter.grant_download_file(&parsed_url)?;
+
let path = PathBuf::from(path);
let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref());