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