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