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)]
91 pub debug_adapters: Vec<Arc<str>>,
92 #[serde(default)]
93 pub debug_locators: Vec<Arc<str>>,
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
213impl ExtensionManifest {
214 pub async fn load(fs: Arc<dyn Fs>, extension_dir: &Path) -> Result<Self> {
215 let extension_name = extension_dir
216 .file_name()
217 .and_then(OsStr::to_str)
218 .context("invalid extension name")?;
219
220 let mut extension_manifest_path = extension_dir.join("extension.json");
221 if fs.is_file(&extension_manifest_path).await {
222 let manifest_content = fs
223 .load(&extension_manifest_path)
224 .await
225 .with_context(|| format!("failed to load {extension_name} extension.json"))?;
226 let manifest_json = serde_json::from_str::<OldExtensionManifest>(&manifest_content)
227 .with_context(|| {
228 format!("invalid extension.json for extension {extension_name}")
229 })?;
230
231 Ok(manifest_from_old_manifest(manifest_json, extension_name))
232 } else {
233 extension_manifest_path.set_extension("toml");
234 let manifest_content = fs
235 .load(&extension_manifest_path)
236 .await
237 .with_context(|| format!("failed to load {extension_name} extension.toml"))?;
238 toml::from_str(&manifest_content)
239 .with_context(|| format!("invalid extension.toml for extension {extension_name}"))
240 }
241 }
242}
243
244fn manifest_from_old_manifest(
245 manifest_json: OldExtensionManifest,
246 extension_id: &str,
247) -> ExtensionManifest {
248 ExtensionManifest {
249 id: extension_id.into(),
250 name: manifest_json.name,
251 version: manifest_json.version,
252 description: manifest_json.description,
253 repository: manifest_json.repository,
254 authors: manifest_json.authors,
255 schema_version: SchemaVersion::ZERO,
256 lib: Default::default(),
257 themes: {
258 let mut themes = manifest_json.themes.into_values().collect::<Vec<_>>();
259 themes.sort();
260 themes.dedup();
261 themes
262 },
263 icon_themes: Vec::new(),
264 languages: {
265 let mut languages = manifest_json.languages.into_values().collect::<Vec<_>>();
266 languages.sort();
267 languages.dedup();
268 languages
269 },
270 grammars: manifest_json
271 .grammars
272 .into_keys()
273 .map(|grammar_name| (grammar_name, Default::default()))
274 .collect(),
275 language_servers: Default::default(),
276 context_servers: BTreeMap::default(),
277 slash_commands: BTreeMap::default(),
278 indexed_docs_providers: BTreeMap::default(),
279 snippets: None,
280 capabilities: Vec::new(),
281 debug_adapters: vec![],
282 debug_locators: vec![],
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 fn extension_manifest() -> ExtensionManifest {
291 ExtensionManifest {
292 id: "test".into(),
293 name: "Test".to_string(),
294 version: "1.0.0".into(),
295 schema_version: SchemaVersion::ZERO,
296 description: None,
297 repository: None,
298 authors: vec![],
299 lib: Default::default(),
300 themes: vec![],
301 icon_themes: vec![],
302 languages: vec![],
303 grammars: BTreeMap::default(),
304 language_servers: BTreeMap::default(),
305 context_servers: BTreeMap::default(),
306 slash_commands: BTreeMap::default(),
307 indexed_docs_providers: BTreeMap::default(),
308 snippets: None,
309 capabilities: vec![],
310 debug_adapters: Default::default(),
311 debug_locators: Default::default(),
312 }
313 }
314
315 #[test]
316 fn test_allow_exact_match() {
317 let manifest = ExtensionManifest {
318 capabilities: vec![ExtensionCapability::ProcessExec {
319 command: "ls".to_string(),
320 args: vec!["-la".to_string()],
321 }],
322 ..extension_manifest()
323 };
324
325 assert!(manifest.allow_exec("ls", &["-la"]).is_ok());
326 assert!(manifest.allow_exec("ls", &["-l"]).is_err());
327 assert!(manifest.allow_exec("pwd", &[] as &[&str]).is_err());
328 }
329
330 #[test]
331 fn test_allow_wildcard_arg() {
332 let manifest = ExtensionManifest {
333 capabilities: vec![ExtensionCapability::ProcessExec {
334 command: "git".to_string(),
335 args: vec!["*".to_string()],
336 }],
337 ..extension_manifest()
338 };
339
340 assert!(manifest.allow_exec("git", &["status"]).is_ok());
341 assert!(manifest.allow_exec("git", &["commit"]).is_ok());
342 assert!(manifest.allow_exec("git", &["status", "-s"]).is_err()); // too many args
343 assert!(manifest.allow_exec("npm", &["install"]).is_err()); // wrong command
344 }
345
346 #[test]
347 fn test_allow_double_wildcard() {
348 let manifest = ExtensionManifest {
349 capabilities: vec![ExtensionCapability::ProcessExec {
350 command: "cargo".to_string(),
351 args: vec!["test".to_string(), "**".to_string()],
352 }],
353 ..extension_manifest()
354 };
355
356 assert!(manifest.allow_exec("cargo", &["test"]).is_ok());
357 assert!(manifest.allow_exec("cargo", &["test", "--all"]).is_ok());
358 assert!(
359 manifest
360 .allow_exec("cargo", &["test", "--all", "--no-fail-fast"])
361 .is_ok()
362 );
363 assert!(manifest.allow_exec("cargo", &["build"]).is_err()); // wrong first arg
364 }
365
366 #[test]
367 fn test_allow_mixed_wildcards() {
368 let manifest = ExtensionManifest {
369 capabilities: vec![ExtensionCapability::ProcessExec {
370 command: "docker".to_string(),
371 args: vec!["run".to_string(), "*".to_string(), "**".to_string()],
372 }],
373 ..extension_manifest()
374 };
375
376 assert!(manifest.allow_exec("docker", &["run", "nginx"]).is_ok());
377 assert!(manifest.allow_exec("docker", &["run"]).is_err());
378 assert!(
379 manifest
380 .allow_exec("docker", &["run", "ubuntu", "bash"])
381 .is_ok()
382 );
383 assert!(
384 manifest
385 .allow_exec("docker", &["run", "alpine", "sh", "-c", "echo hello"])
386 .is_ok()
387 );
388 assert!(manifest.allow_exec("docker", &["ps"]).is_err()); // wrong first arg
389 }
390}