1use anyhow::{Context as _, Result, bail};
2use collections::{BTreeMap, HashMap};
3use fs::Fs;
4use language::LanguageName;
5use lsp::LanguageServerName;
6use semantic_version::SemanticVersion;
7use serde::{Deserialize, Serialize};
8use std::{
9 ffi::OsStr,
10 fmt,
11 path::{Path, PathBuf},
12 sync::Arc,
13};
14
15/// This is the old version of the extension manifest, from when it was `extension.json`.
16#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
17pub struct OldExtensionManifest {
18 pub name: String,
19 pub version: Arc<str>,
20
21 #[serde(default)]
22 pub description: Option<String>,
23 #[serde(default)]
24 pub repository: Option<String>,
25 #[serde(default)]
26 pub authors: Vec<String>,
27
28 #[serde(default)]
29 pub themes: BTreeMap<Arc<str>, PathBuf>,
30 #[serde(default)]
31 pub languages: BTreeMap<Arc<str>, PathBuf>,
32 #[serde(default)]
33 pub grammars: BTreeMap<Arc<str>, PathBuf>,
34}
35
36/// The schema version of the [`ExtensionManifest`].
37#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
38pub struct SchemaVersion(pub i32);
39
40impl fmt::Display for SchemaVersion {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 write!(f, "{}", self.0)
43 }
44}
45
46impl SchemaVersion {
47 pub const ZERO: Self = Self(0);
48
49 pub fn is_v0(&self) -> bool {
50 self == &Self::ZERO
51 }
52}
53
54#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
55pub struct ExtensionManifest {
56 pub id: Arc<str>,
57 pub name: String,
58 pub version: Arc<str>,
59 pub schema_version: SchemaVersion,
60
61 #[serde(default)]
62 pub description: Option<String>,
63 #[serde(default)]
64 pub repository: Option<String>,
65 #[serde(default)]
66 pub authors: Vec<String>,
67 #[serde(default)]
68 pub lib: LibManifestEntry,
69
70 #[serde(default)]
71 pub themes: Vec<PathBuf>,
72 #[serde(default)]
73 pub icon_themes: Vec<PathBuf>,
74 #[serde(default)]
75 pub languages: Vec<PathBuf>,
76 #[serde(default)]
77 pub grammars: BTreeMap<Arc<str>, GrammarManifestEntry>,
78 #[serde(default)]
79 pub language_servers: BTreeMap<LanguageServerName, LanguageServerManifestEntry>,
80 #[serde(default)]
81 pub context_servers: BTreeMap<Arc<str>, ContextServerManifestEntry>,
82 #[serde(default)]
83 pub slash_commands: BTreeMap<Arc<str>, SlashCommandManifestEntry>,
84 #[serde(default)]
85 pub indexed_docs_providers: BTreeMap<Arc<str>, IndexedDocsProviderEntry>,
86 #[serde(default)]
87 pub snippets: Option<PathBuf>,
88 #[serde(default)]
89 pub capabilities: Vec<ExtensionCapability>,
90 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
91 pub debug_adapters: BTreeMap<Arc<str>, DebugAdapterManifestEntry>,
92 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
93 pub debug_locators: BTreeMap<Arc<str>, DebugLocatorManifestEntry>,
94}
95
96impl ExtensionManifest {
97 pub fn allow_exec(
98 &self,
99 desired_command: &str,
100 desired_args: &[impl AsRef<str> + std::fmt::Debug],
101 ) -> Result<()> {
102 let is_allowed = self.capabilities.iter().any(|capability| match capability {
103 ExtensionCapability::ProcessExec(capability) => {
104 capability.allows(desired_command, desired_args)
105 }
106 });
107
108 if !is_allowed {
109 bail!(
110 "capability for process:exec {desired_command} {desired_args:?} was not listed in the extension manifest",
111 );
112 }
113
114 Ok(())
115 }
116
117 pub fn allow_remote_load(&self) -> bool {
118 !self.language_servers.is_empty()
119 || !self.debug_adapters.is_empty()
120 || !self.debug_locators.is_empty()
121 }
122}
123
124pub fn build_debug_adapter_schema_path(
125 adapter_name: &Arc<str>,
126 meta: &DebugAdapterManifestEntry,
127) -> PathBuf {
128 meta.schema_path.clone().unwrap_or_else(|| {
129 Path::new("debug_adapter_schemas")
130 .join(Path::new(adapter_name.as_ref()).with_extension("json"))
131 })
132}
133
134/// A capability for an extension.
135#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
136#[serde(tag = "kind")]
137pub enum ExtensionCapability {
138 #[serde(rename = "process:exec")]
139 ProcessExec(ProcessExecCapability),
140}
141
142#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
143#[serde(rename_all = "snake_case")]
144pub struct ProcessExecCapability {
145 /// The command to execute.
146 pub command: String,
147 /// The arguments to pass to the command. Use `*` for a single wildcard argument.
148 /// If the last element is `**`, then any trailing arguments are allowed.
149 pub args: Vec<String>,
150}
151
152impl ProcessExecCapability {
153 /// Returns whether the capability allows the given command and arguments.
154 pub fn allows(
155 &self,
156 desired_command: &str,
157 desired_args: &[impl AsRef<str> + std::fmt::Debug],
158 ) -> bool {
159 if self.command != desired_command && self.command != "*" {
160 return false;
161 }
162
163 for (ix, arg) in self.args.iter().enumerate() {
164 if arg == "**" {
165 return true;
166 }
167
168 if ix >= desired_args.len() {
169 return false;
170 }
171
172 if arg != "*" && arg != desired_args[ix].as_ref() {
173 return false;
174 }
175 }
176
177 if self.args.len() < desired_args.len() {
178 return false;
179 }
180
181 true
182 }
183}
184
185#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
186pub struct LibManifestEntry {
187 pub kind: Option<ExtensionLibraryKind>,
188 pub version: Option<SemanticVersion>,
189}
190
191#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
192pub enum ExtensionLibraryKind {
193 Rust,
194}
195
196#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
197pub struct GrammarManifestEntry {
198 pub repository: String,
199 #[serde(alias = "commit")]
200 pub rev: String,
201 #[serde(default)]
202 pub path: Option<String>,
203}
204
205#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
206pub struct LanguageServerManifestEntry {
207 /// Deprecated in favor of `languages`.
208 #[serde(default)]
209 language: Option<LanguageName>,
210 /// The list of languages this language server should work with.
211 #[serde(default)]
212 languages: Vec<LanguageName>,
213 #[serde(default)]
214 pub language_ids: HashMap<String, String>,
215 #[serde(default)]
216 pub code_action_kinds: Option<Vec<lsp::CodeActionKind>>,
217}
218
219impl LanguageServerManifestEntry {
220 /// Returns the list of languages for the language server.
221 ///
222 /// Prefer this over accessing the `language` or `languages` fields directly,
223 /// as we currently support both.
224 ///
225 /// We can replace this with just field access for the `languages` field once
226 /// we have removed `language`.
227 pub fn languages(&self) -> impl IntoIterator<Item = LanguageName> + '_ {
228 let language = if self.languages.is_empty() {
229 self.language.clone()
230 } else {
231 None
232 };
233 self.languages.iter().cloned().chain(language)
234 }
235}
236
237#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
238pub struct ContextServerManifestEntry {}
239
240#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
241pub struct SlashCommandManifestEntry {
242 pub description: String,
243 pub requires_argument: bool,
244}
245
246#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
247pub struct IndexedDocsProviderEntry {}
248
249#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
250pub struct DebugAdapterManifestEntry {
251 pub schema_path: Option<PathBuf>,
252}
253
254#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
255pub struct DebugLocatorManifestEntry {}
256
257impl ExtensionManifest {
258 pub async fn load(fs: Arc<dyn Fs>, extension_dir: &Path) -> Result<Self> {
259 let extension_name = extension_dir
260 .file_name()
261 .and_then(OsStr::to_str)
262 .context("invalid extension name")?;
263
264 let mut extension_manifest_path = extension_dir.join("extension.json");
265 if fs.is_file(&extension_manifest_path).await {
266 let manifest_content = fs
267 .load(&extension_manifest_path)
268 .await
269 .with_context(|| format!("failed to load {extension_name} extension.json"))?;
270 let manifest_json = serde_json::from_str::<OldExtensionManifest>(&manifest_content)
271 .with_context(|| {
272 format!("invalid extension.json for extension {extension_name}")
273 })?;
274
275 Ok(manifest_from_old_manifest(manifest_json, extension_name))
276 } else {
277 extension_manifest_path.set_extension("toml");
278 let manifest_content = fs
279 .load(&extension_manifest_path)
280 .await
281 .with_context(|| format!("failed to load {extension_name} extension.toml"))?;
282 toml::from_str(&manifest_content)
283 .with_context(|| format!("invalid extension.toml for extension {extension_name}"))
284 }
285 }
286}
287
288fn manifest_from_old_manifest(
289 manifest_json: OldExtensionManifest,
290 extension_id: &str,
291) -> ExtensionManifest {
292 ExtensionManifest {
293 id: extension_id.into(),
294 name: manifest_json.name,
295 version: manifest_json.version,
296 description: manifest_json.description,
297 repository: manifest_json.repository,
298 authors: manifest_json.authors,
299 schema_version: SchemaVersion::ZERO,
300 lib: Default::default(),
301 themes: {
302 let mut themes = manifest_json.themes.into_values().collect::<Vec<_>>();
303 themes.sort();
304 themes.dedup();
305 themes
306 },
307 icon_themes: Vec::new(),
308 languages: {
309 let mut languages = manifest_json.languages.into_values().collect::<Vec<_>>();
310 languages.sort();
311 languages.dedup();
312 languages
313 },
314 grammars: manifest_json
315 .grammars
316 .into_keys()
317 .map(|grammar_name| (grammar_name, Default::default()))
318 .collect(),
319 language_servers: Default::default(),
320 context_servers: BTreeMap::default(),
321 slash_commands: BTreeMap::default(),
322 indexed_docs_providers: BTreeMap::default(),
323 snippets: None,
324 capabilities: Vec::new(),
325 debug_adapters: Default::default(),
326 debug_locators: Default::default(),
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 fn extension_manifest() -> ExtensionManifest {
335 ExtensionManifest {
336 id: "test".into(),
337 name: "Test".to_string(),
338 version: "1.0.0".into(),
339 schema_version: SchemaVersion::ZERO,
340 description: None,
341 repository: None,
342 authors: vec![],
343 lib: Default::default(),
344 themes: vec![],
345 icon_themes: vec![],
346 languages: vec![],
347 grammars: BTreeMap::default(),
348 language_servers: BTreeMap::default(),
349 context_servers: BTreeMap::default(),
350 slash_commands: BTreeMap::default(),
351 indexed_docs_providers: BTreeMap::default(),
352 snippets: None,
353 capabilities: vec![],
354 debug_adapters: Default::default(),
355 debug_locators: Default::default(),
356 }
357 }
358
359 #[test]
360 fn test_build_adapter_schema_path_with_schema_path() {
361 let adapter_name = Arc::from("my_adapter");
362 let entry = DebugAdapterManifestEntry {
363 schema_path: Some(PathBuf::from("foo/bar")),
364 };
365
366 let path = build_debug_adapter_schema_path(&adapter_name, &entry);
367 assert_eq!(path, PathBuf::from("foo/bar"));
368 }
369
370 #[test]
371 fn test_build_adapter_schema_path_without_schema_path() {
372 let adapter_name = Arc::from("my_adapter");
373 let entry = DebugAdapterManifestEntry { schema_path: None };
374
375 let path = build_debug_adapter_schema_path(&adapter_name, &entry);
376 assert_eq!(
377 path,
378 PathBuf::from("debug_adapter_schemas").join("my_adapter.json")
379 );
380 }
381
382 #[test]
383 fn test_allow_exact_match() {
384 let manifest = ExtensionManifest {
385 capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
386 command: "ls".to_string(),
387 args: vec!["-la".to_string()],
388 })],
389 ..extension_manifest()
390 };
391
392 assert!(manifest.allow_exec("ls", &["-la"]).is_ok());
393 assert!(manifest.allow_exec("ls", &["-l"]).is_err());
394 assert!(manifest.allow_exec("pwd", &[] as &[&str]).is_err());
395 }
396
397 #[test]
398 fn test_allow_wildcard_arg() {
399 let manifest = ExtensionManifest {
400 capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
401 command: "git".to_string(),
402 args: vec!["*".to_string()],
403 })],
404 ..extension_manifest()
405 };
406
407 assert!(manifest.allow_exec("git", &["status"]).is_ok());
408 assert!(manifest.allow_exec("git", &["commit"]).is_ok());
409 assert!(manifest.allow_exec("git", &["status", "-s"]).is_err()); // too many args
410 assert!(manifest.allow_exec("npm", &["install"]).is_err()); // wrong command
411 }
412
413 #[test]
414 fn test_allow_double_wildcard() {
415 let manifest = ExtensionManifest {
416 capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
417 command: "cargo".to_string(),
418 args: vec!["test".to_string(), "**".to_string()],
419 })],
420 ..extension_manifest()
421 };
422
423 assert!(manifest.allow_exec("cargo", &["test"]).is_ok());
424 assert!(manifest.allow_exec("cargo", &["test", "--all"]).is_ok());
425 assert!(
426 manifest
427 .allow_exec("cargo", &["test", "--all", "--no-fail-fast"])
428 .is_ok()
429 );
430 assert!(manifest.allow_exec("cargo", &["build"]).is_err()); // wrong first arg
431 }
432
433 #[test]
434 fn test_allow_mixed_wildcards() {
435 let manifest = ExtensionManifest {
436 capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability {
437 command: "docker".to_string(),
438 args: vec!["run".to_string(), "*".to_string(), "**".to_string()],
439 })],
440 ..extension_manifest()
441 };
442
443 assert!(manifest.allow_exec("docker", &["run", "nginx"]).is_ok());
444 assert!(manifest.allow_exec("docker", &["run"]).is_err());
445 assert!(
446 manifest
447 .allow_exec("docker", &["run", "ubuntu", "bash"])
448 .is_ok()
449 );
450 assert!(
451 manifest
452 .allow_exec("docker", &["run", "alpine", "sh", "-c", "echo hello"])
453 .is_ok()
454 );
455 assert!(manifest.allow_exec("docker", &["ps"]).is_err()); // wrong first arg
456 }
457}