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 { command, args } if command == desired_command => {
104 for (ix, arg) in args.iter().enumerate() {
105 if arg == "**" {
106 return true;
107 }
108
109 if ix >= desired_args.len() {
110 return false;
111 }
112
113 if arg != "*" && arg != desired_args[ix].as_ref() {
114 return false;
115 }
116 }
117 if args.len() < desired_args.len() {
118 return false;
119 }
120 true
121 }
122 _ => false,
123 });
124
125 if !is_allowed {
126 bail!(
127 "capability for process:exec {desired_command} {desired_args:?} was not listed in the extension manifest",
128 );
129 }
130
131 Ok(())
132 }
133}
134
135/// A capability for an extension.
136#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
137#[serde(tag = "kind")]
138pub enum ExtensionCapability {
139 #[serde(rename = "process:exec")]
140 ProcessExec {
141 /// The command to execute.
142 command: String,
143 /// The arguments to pass to the command. Use `*` for a single wildcard argument.
144 /// If the last element is `**`, then any trailing arguments are allowed.
145 args: Vec<String>,
146 },
147}
148
149#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
150pub struct LibManifestEntry {
151 pub kind: Option<ExtensionLibraryKind>,
152 pub version: Option<SemanticVersion>,
153}
154
155#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
156pub enum ExtensionLibraryKind {
157 Rust,
158}
159
160#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
161pub struct GrammarManifestEntry {
162 pub repository: String,
163 #[serde(alias = "commit")]
164 pub rev: String,
165 #[serde(default)]
166 pub path: Option<String>,
167}
168
169#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
170pub struct LanguageServerManifestEntry {
171 /// Deprecated in favor of `languages`.
172 #[serde(default)]
173 language: Option<LanguageName>,
174 /// The list of languages this language server should work with.
175 #[serde(default)]
176 languages: Vec<LanguageName>,
177 #[serde(default)]
178 pub language_ids: HashMap<String, String>,
179 #[serde(default)]
180 pub code_action_kinds: Option<Vec<lsp::CodeActionKind>>,
181}
182
183impl LanguageServerManifestEntry {
184 /// Returns the list of languages for the language server.
185 ///
186 /// Prefer this over accessing the `language` or `languages` fields directly,
187 /// as we currently support both.
188 ///
189 /// We can replace this with just field access for the `languages` field once
190 /// we have removed `language`.
191 pub fn languages(&self) -> impl IntoIterator<Item = LanguageName> + '_ {
192 let language = if self.languages.is_empty() {
193 self.language.clone()
194 } else {
195 None
196 };
197 self.languages.iter().cloned().chain(language)
198 }
199}
200
201#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
202pub struct ContextServerManifestEntry {}
203
204#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
205pub struct SlashCommandManifestEntry {
206 pub description: String,
207 pub requires_argument: bool,
208}
209
210#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
211pub struct IndexedDocsProviderEntry {}
212
213#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
214pub struct DebugAdapterManifestEntry {
215 pub schema_path: Option<PathBuf>,
216}
217
218#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
219pub struct DebugLocatorManifestEntry {}
220
221impl ExtensionManifest {
222 pub async fn load(fs: Arc<dyn Fs>, extension_dir: &Path) -> Result<Self> {
223 let extension_name = extension_dir
224 .file_name()
225 .and_then(OsStr::to_str)
226 .context("invalid extension name")?;
227
228 let mut extension_manifest_path = extension_dir.join("extension.json");
229 if fs.is_file(&extension_manifest_path).await {
230 let manifest_content = fs
231 .load(&extension_manifest_path)
232 .await
233 .with_context(|| format!("failed to load {extension_name} extension.json"))?;
234 let manifest_json = serde_json::from_str::<OldExtensionManifest>(&manifest_content)
235 .with_context(|| {
236 format!("invalid extension.json for extension {extension_name}")
237 })?;
238
239 Ok(manifest_from_old_manifest(manifest_json, extension_name))
240 } else {
241 extension_manifest_path.set_extension("toml");
242 let manifest_content = fs
243 .load(&extension_manifest_path)
244 .await
245 .with_context(|| format!("failed to load {extension_name} extension.toml"))?;
246 toml::from_str(&manifest_content)
247 .with_context(|| format!("invalid extension.toml for extension {extension_name}"))
248 }
249 }
250}
251
252fn manifest_from_old_manifest(
253 manifest_json: OldExtensionManifest,
254 extension_id: &str,
255) -> ExtensionManifest {
256 ExtensionManifest {
257 id: extension_id.into(),
258 name: manifest_json.name,
259 version: manifest_json.version,
260 description: manifest_json.description,
261 repository: manifest_json.repository,
262 authors: manifest_json.authors,
263 schema_version: SchemaVersion::ZERO,
264 lib: Default::default(),
265 themes: {
266 let mut themes = manifest_json.themes.into_values().collect::<Vec<_>>();
267 themes.sort();
268 themes.dedup();
269 themes
270 },
271 icon_themes: Vec::new(),
272 languages: {
273 let mut languages = manifest_json.languages.into_values().collect::<Vec<_>>();
274 languages.sort();
275 languages.dedup();
276 languages
277 },
278 grammars: manifest_json
279 .grammars
280 .into_keys()
281 .map(|grammar_name| (grammar_name, Default::default()))
282 .collect(),
283 language_servers: Default::default(),
284 context_servers: BTreeMap::default(),
285 slash_commands: BTreeMap::default(),
286 indexed_docs_providers: BTreeMap::default(),
287 snippets: None,
288 capabilities: Vec::new(),
289 debug_adapters: Default::default(),
290 debug_locators: Default::default(),
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 fn extension_manifest() -> ExtensionManifest {
299 ExtensionManifest {
300 id: "test".into(),
301 name: "Test".to_string(),
302 version: "1.0.0".into(),
303 schema_version: SchemaVersion::ZERO,
304 description: None,
305 repository: None,
306 authors: vec![],
307 lib: Default::default(),
308 themes: vec![],
309 icon_themes: vec![],
310 languages: vec![],
311 grammars: BTreeMap::default(),
312 language_servers: BTreeMap::default(),
313 context_servers: BTreeMap::default(),
314 slash_commands: BTreeMap::default(),
315 indexed_docs_providers: BTreeMap::default(),
316 snippets: None,
317 capabilities: vec![],
318 debug_adapters: Default::default(),
319 debug_locators: Default::default(),
320 }
321 }
322
323 #[test]
324 fn test_allow_exact_match() {
325 let manifest = ExtensionManifest {
326 capabilities: vec![ExtensionCapability::ProcessExec {
327 command: "ls".to_string(),
328 args: vec!["-la".to_string()],
329 }],
330 ..extension_manifest()
331 };
332
333 assert!(manifest.allow_exec("ls", &["-la"]).is_ok());
334 assert!(manifest.allow_exec("ls", &["-l"]).is_err());
335 assert!(manifest.allow_exec("pwd", &[] as &[&str]).is_err());
336 }
337
338 #[test]
339 fn test_allow_wildcard_arg() {
340 let manifest = ExtensionManifest {
341 capabilities: vec![ExtensionCapability::ProcessExec {
342 command: "git".to_string(),
343 args: vec!["*".to_string()],
344 }],
345 ..extension_manifest()
346 };
347
348 assert!(manifest.allow_exec("git", &["status"]).is_ok());
349 assert!(manifest.allow_exec("git", &["commit"]).is_ok());
350 assert!(manifest.allow_exec("git", &["status", "-s"]).is_err()); // too many args
351 assert!(manifest.allow_exec("npm", &["install"]).is_err()); // wrong command
352 }
353
354 #[test]
355 fn test_allow_double_wildcard() {
356 let manifest = ExtensionManifest {
357 capabilities: vec![ExtensionCapability::ProcessExec {
358 command: "cargo".to_string(),
359 args: vec!["test".to_string(), "**".to_string()],
360 }],
361 ..extension_manifest()
362 };
363
364 assert!(manifest.allow_exec("cargo", &["test"]).is_ok());
365 assert!(manifest.allow_exec("cargo", &["test", "--all"]).is_ok());
366 assert!(
367 manifest
368 .allow_exec("cargo", &["test", "--all", "--no-fail-fast"])
369 .is_ok()
370 );
371 assert!(manifest.allow_exec("cargo", &["build"]).is_err()); // wrong first arg
372 }
373
374 #[test]
375 fn test_allow_mixed_wildcards() {
376 let manifest = ExtensionManifest {
377 capabilities: vec![ExtensionCapability::ProcessExec {
378 command: "docker".to_string(),
379 args: vec!["run".to_string(), "*".to_string(), "**".to_string()],
380 }],
381 ..extension_manifest()
382 };
383
384 assert!(manifest.allow_exec("docker", &["run", "nginx"]).is_ok());
385 assert!(manifest.allow_exec("docker", &["run"]).is_err());
386 assert!(
387 manifest
388 .allow_exec("docker", &["run", "ubuntu", "bash"])
389 .is_ok()
390 );
391 assert!(
392 manifest
393 .allow_exec("docker", &["run", "alpine", "sh", "-c", "echo hello"])
394 .is_ok()
395 );
396 assert!(manifest.allow_exec("docker", &["ps"]).is_err()); // wrong first arg
397 }
398}