diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs index f0e789994127c9347c8eb6b8d16417ba7eaaf831..f67e54946955167116b5c44f9bc73c09ae99fca5 100644 --- a/crates/extension/src/extension_builder.rs +++ b/crates/extension/src/extension_builder.rs @@ -15,7 +15,7 @@ use std::{ str::FromStr, sync::Arc, }; -use util::command::Stdio; +use util::{command::Stdio, rel_path::PathExt}; use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _}; use wasmparser::Parser; @@ -108,7 +108,7 @@ impl ExtensionBuilder { for (debug_adapter_name, meta) in &mut extension_manifest.debug_adapters { let debug_adapter_schema_path = - extension_dir.join(build_debug_adapter_schema_path(debug_adapter_name, meta)); + extension_dir.join(build_debug_adapter_schema_path(debug_adapter_name, meta)?); let debug_adapter_schema = fs::read_to_string(&debug_adapter_schema_path) .with_context(|| { @@ -582,8 +582,9 @@ async fn populate_defaults( let language_dir = language_dir?; let config_path = language_dir.join(LanguageConfig::FILE_NAME); if fs.is_file(config_path.as_path()).await { - let relative_language_dir = - language_dir.strip_prefix(extension_path)?.to_path_buf(); + let relative_language_dir = language_dir + .strip_prefix(extension_path)? + .to_rel_path_buf()?; if !manifest.languages.contains(&relative_language_dir) { manifest.languages.push(relative_language_dir); } @@ -601,7 +602,8 @@ async fn populate_defaults( while let Some(theme_path) = theme_dir_entries.next().await { let theme_path = theme_path?; if theme_path.extension() == Some("json".as_ref()) { - let relative_theme_path = theme_path.strip_prefix(extension_path)?.to_path_buf(); + let relative_theme_path = + theme_path.strip_prefix(extension_path)?.to_rel_path_buf()?; if !manifest.themes.contains(&relative_theme_path) { manifest.themes.push(relative_theme_path); } @@ -619,8 +621,9 @@ async fn populate_defaults( while let Some(icon_theme_path) = icon_theme_dir_entries.next().await { let icon_theme_path = icon_theme_path?; if icon_theme_path.extension() == Some("json".as_ref()) { - let relative_icon_theme_path = - icon_theme_path.strip_prefix(extension_path)?.to_path_buf(); + let relative_icon_theme_path = icon_theme_path + .strip_prefix(extension_path)? + .to_rel_path_buf()?; if !manifest.icon_themes.contains(&relative_icon_theme_path) { manifest.icon_themes.push(relative_icon_theme_path); } diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index d3bef88713733b6f52301fc877bcc3a1eb16eec6..919051867d5f16942c0e2c9aca916d96e6ae124a 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/crates/extension/src/extension_manifest.rs @@ -11,6 +11,7 @@ use language::LanguageName; use lsp::LanguageServerName; use semver::Version; use serde::{Deserialize, Serialize}; +use util::rel_path::{PathExt, RelPathBuf}; use crate::ExtensionCapability; @@ -28,11 +29,11 @@ pub struct OldExtensionManifest { pub authors: Vec, #[serde(default)] - pub themes: BTreeMap, PathBuf>, + pub themes: BTreeMap, RelPathBuf>, #[serde(default)] - pub languages: BTreeMap, PathBuf>, + pub languages: BTreeMap, RelPathBuf>, #[serde(default)] - pub grammars: BTreeMap, PathBuf>, + pub grammars: BTreeMap, RelPathBuf>, } /// The schema version of the [`ExtensionManifest`]. @@ -94,11 +95,11 @@ pub struct ExtensionManifest { pub lib: LibManifestEntry, #[serde(default)] - pub themes: Vec, + pub themes: Vec, #[serde(default)] - pub icon_themes: Vec, + pub icon_themes: Vec, #[serde(default)] - pub languages: Vec, + pub languages: Vec, #[serde(default)] pub grammars: BTreeMap, GrammarManifestEntry>, #[serde(default)] @@ -195,11 +196,13 @@ impl ExtensionManifest { pub fn build_debug_adapter_schema_path( adapter_name: &Arc, meta: &DebugAdapterManifestEntry, -) -> PathBuf { - meta.schema_path.clone().unwrap_or_else(|| { - Path::new("debug_adapter_schemas") +) -> anyhow::Result { + match &meta.schema_path { + Some(path) => Ok(path.clone()), + None => Path::new("debug_adapter_schemas") .join(Path::new(adapter_name.as_ref()).with_extension("json")) - }) + .to_rel_path_buf(), + } } #[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] @@ -350,7 +353,7 @@ pub struct SlashCommandManifestEntry { #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] pub struct DebugAdapterManifestEntry { - pub schema_path: Option, + pub schema_path: Option, } #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] @@ -442,7 +445,9 @@ fn manifest_from_old_manifest( #[cfg(test)] mod tests { + use indoc::indoc; use pretty_assertions::assert_eq; + use util::rel_path::rel_path_buf; use crate::ProcessExecCapability; @@ -478,11 +483,11 @@ mod tests { fn test_build_adapter_schema_path_with_schema_path() { let adapter_name = Arc::from("my_adapter"); let entry = DebugAdapterManifestEntry { - schema_path: Some(PathBuf::from("foo/bar")), + schema_path: Some(rel_path_buf("foo/bar")), }; - let path = build_debug_adapter_schema_path(&adapter_name, &entry); - assert_eq!(path, PathBuf::from("foo/bar")); + let path = build_debug_adapter_schema_path(&adapter_name, &entry).unwrap(); + assert_eq!(path, rel_path_buf("foo/bar")); } #[test] @@ -490,11 +495,8 @@ mod tests { let adapter_name = Arc::from("my_adapter"); let entry = DebugAdapterManifestEntry { schema_path: None }; - let path = build_debug_adapter_schema_path(&adapter_name, &entry); - assert_eq!( - path, - PathBuf::from("debug_adapter_schemas").join("my_adapter.json") - ); + let path = build_debug_adapter_schema_path(&adapter_name, &entry).unwrap(); + assert_eq!(path, rel_path_buf("debug_adapter_schemas/my_adapter.json")); } #[test] @@ -572,22 +574,37 @@ mod tests { ); assert!(manifest.allow_exec("docker", &["ps"]).is_err()); // wrong first arg } + + #[test] + #[cfg(target_os = "windows")] + fn test_deserialize_manifest_with_windows_separators() { + let content = indoc! {r#" + id = "test-manifest" + name = "Test Manifest" + version = "0.0.1" + schema_version = 0 + languages = ["foo\\bar"] + "#}; + let manifest: ExtensionManifest = toml::from_str(&content).expect("manifest should parse"); + assert_eq!(manifest.languages, vec![rel_path_buf("foo/bar")]); + } + #[test] fn parse_manifest_with_agent_server_archive_launcher() { - let toml_src = r#" -id = "example.agent-server-ext" -name = "Agent Server Example" -version = "1.0.0" -schema_version = 0 - -[agent_servers.foo] -name = "Foo Agent" - -[agent_servers.foo.targets.linux-x86_64] -archive = "https://example.com/agent-linux-x64.tar.gz" -cmd = "./agent" -args = ["--serve"] -"#; + let toml_src = indoc! {r#" + id = "example.agent-server-ext" + name = "Agent Server Example" + version = "1.0.0" + schema_version = 0 + + [agent_servers.foo] + name = "Foo Agent" + + [agent_servers.foo.targets.linux-x86_64] + archive = "https://example.com/agent-linux-x64.tar.gz" + cmd = "./agent" + args = ["--serve"] + "#}; let manifest: ExtensionManifest = toml::from_str(toml_src).expect("manifest should parse"); assert_eq!(manifest.id.as_ref(), "example.agent-server-ext"); diff --git a/crates/extension_cli/src/main.rs b/crates/extension_cli/src/main.rs index 57845754fc8263c516bc3aec7d1ae0a2ffe68a2f..38dc626562beec542cfc7c1523ec8674af3002ca 100644 --- a/crates/extension_cli/src/main.rs +++ b/crates/extension_cli/src/main.rs @@ -165,6 +165,7 @@ async fn copy_extension_resources( let output_themes_dir = output_dir.join("themes"); fs::create_dir_all(&output_themes_dir)?; for theme_path in &manifest.themes { + let theme_path = theme_path.as_std_path(); fs::copy( extension_path.join(theme_path), output_themes_dir.join(theme_path.file_name().context("invalid theme path")?), @@ -177,6 +178,7 @@ async fn copy_extension_resources( let output_icon_themes_dir = output_dir.join("icon_themes"); fs::create_dir_all(&output_icon_themes_dir)?; for icon_theme_path in &manifest.icon_themes { + let icon_theme_path = icon_theme_path.as_std_path(); fs::copy( extension_path.join(icon_theme_path), output_icon_themes_dir.join( @@ -224,6 +226,7 @@ async fn copy_extension_resources( let output_languages_dir = output_dir.join("languages"); fs::create_dir_all(&output_languages_dir)?; for language_path in &manifest.languages { + let language_path = language_path.as_std_path(); copy_recursive( fs.as_ref(), &extension_path.join(language_path), @@ -243,14 +246,11 @@ async fn copy_extension_resources( if !manifest.debug_adapters.is_empty() { for (debug_adapter, entry) in &manifest.debug_adapters { - let schema_path = entry.schema_path.clone().unwrap_or_else(|| { - PathBuf::from("debug_adapter_schemas".to_owned()) - .join(debug_adapter.as_ref()) - .with_extension("json") - }); + let schema_path = extension::build_debug_adapter_schema_path(debug_adapter, entry)?; let parent = schema_path .parent() .with_context(|| format!("invalid empty schema path for {debug_adapter}"))?; + let schema_path = schema_path.as_std_path(); fs::create_dir_all(output_dir.join(parent))?; copy_recursive( fs.as_ref(), @@ -265,7 +265,7 @@ async fn copy_extension_resources( .with_context(|| { format!( "failed to copy debug adapter schema '{}'", - schema_path.display() + schema_path.display(), ) })?; } diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 03f340a56a98eb826110b245505c2b92774a0e0f..ca43b4a3993f6ef1053a85ae261fdbd7afc174a6 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -56,7 +56,7 @@ use std::{ }; use task::TaskTemplates; use url::Url; -use util::{ResultExt, paths::RemotePathBuf}; +use util::{ResultExt, paths::RemotePathBuf, rel_path::PathExt}; use wasm_host::{ WasmExtension, WasmHost, wit::{is_supported_wasm_api_version, wasm_api_version_range}, @@ -1244,13 +1244,16 @@ impl ExtensionStore { })); themes_to_add.extend(extension.manifest.themes.iter().map(|theme_path| { let mut path = self.installed_dir.clone(); - path.extend([Path::new(extension_id.as_ref()), theme_path.as_path()]); + path.extend([Path::new(extension_id.as_ref()), theme_path.as_std_path()]); path })); icon_themes_to_add.extend(extension.manifest.icon_themes.iter().map( |icon_theme_path| { let mut path = self.installed_dir.clone(); - path.extend([Path::new(extension_id.as_ref()), icon_theme_path.as_path()]); + path.extend([ + Path::new(extension_id.as_ref()), + icon_theme_path.as_std_path(), + ]); let mut icons_root_path = self.installed_dir.clone(); icons_root_path.extend([Path::new(extension_id.as_ref())]); @@ -1560,7 +1563,7 @@ impl ExtensionStore { })?; let config = ::toml::from_str::(&config)?; - let relative_path = relative_path.to_path_buf(); + let relative_path = relative_path.to_rel_path_buf()?; if !extension_manifest.languages.contains(&relative_path) { extension_manifest.languages.push(relative_path.clone()); } @@ -1569,7 +1572,7 @@ impl ExtensionStore { config.name.clone(), ExtensionIndexLanguageEntry { extension: extension_id.clone(), - path: relative_path, + path: relative_path.as_std_path().to_path_buf(), matcher: config.matcher, hidden: config.hidden, grammar: config.grammar, @@ -1593,7 +1596,7 @@ impl ExtensionStore { continue; }; - let relative_path = relative_path.to_path_buf(); + let relative_path = relative_path.to_rel_path_buf()?; if !extension_manifest.themes.contains(&relative_path) { extension_manifest.themes.push(relative_path.clone()); } @@ -1603,7 +1606,7 @@ impl ExtensionStore { theme_name.into(), ExtensionIndexThemeEntry { extension: extension_id.clone(), - path: relative_path.clone(), + path: relative_path.as_std_path().to_path_buf(), }, ); } @@ -1625,7 +1628,7 @@ impl ExtensionStore { continue; }; - let relative_path = relative_path.to_path_buf(); + let relative_path = relative_path.to_rel_path_buf()?; if !extension_manifest.icon_themes.contains(&relative_path) { extension_manifest.icon_themes.push(relative_path.clone()); } @@ -1635,7 +1638,7 @@ impl ExtensionStore { icon_theme_name.into(), ExtensionIndexIconThemeEntry { extension: extension_id.clone(), - path: relative_path.clone(), + path: relative_path.as_std_path().to_path_buf(), }, ); } @@ -1721,15 +1724,15 @@ impl ExtensionStore { } for (adapter_name, meta) in loaded_extension.manifest.debug_adapters.iter() { - let schema_path = &extension::build_debug_adapter_schema_path(adapter_name, meta); + let schema_path = extension::build_debug_adapter_schema_path(adapter_name, meta)?; - if fs.is_file(&src_dir.join(schema_path)).await { + if fs.is_file(&src_dir.join(&schema_path)).await { if let Some(parent) = schema_path.parent() { fs.create_dir(&tmp_dir.join(parent)).await? } fs.copy_file( - &src_dir.join(schema_path), - &tmp_dir.join(schema_path), + &src_dir.join(&schema_path), + &tmp_dir.join(&schema_path), fs::CopyOptions::default(), ) .await? diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index c395aedb26003631e77bb86b45f059c440540708..abdb3ffd3fad2bcb53ff1171f51538e63cdb6dee 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -26,7 +26,7 @@ use std::{ sync::Arc, }; use theme::ThemeRegistry; -use util::test::TempTree; +use util::{rel_path::rel_path_buf, test::TempTree}; #[cfg(test)] #[ctor::ctor] @@ -150,7 +150,10 @@ async fn test_extension_store(cx: &mut TestAppContext) { themes: Default::default(), icon_themes: Vec::new(), lib: Default::default(), - languages: vec!["languages/erb".into(), "languages/ruby".into()], + languages: vec![ + rel_path_buf("languages/erb"), + rel_path_buf("languages/ruby"), + ], grammars: [ ("embedded_template".into(), GrammarManifestEntry::default()), ("ruby".into(), GrammarManifestEntry::default()), @@ -182,8 +185,8 @@ async fn test_extension_store(cx: &mut TestAppContext) { authors: vec![], repository: None, themes: vec![ - "themes/monokai-pro.json".into(), - "themes/monokai.json".into(), + rel_path_buf("themes/monokai-pro.json"), + rel_path_buf("themes/monokai.json"), ], icon_themes: Vec::new(), lib: Default::default(), @@ -367,7 +370,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { description: None, authors: vec![], repository: None, - themes: vec!["themes/gruvbox.json".into()], + themes: vec![rel_path_buf("themes/gruvbox.json")], icon_themes: Vec::new(), lib: Default::default(), languages: Default::default(), diff --git a/crates/extension_host/src/headless_host.rs b/crates/extension_host/src/headless_host.rs index 7c30228257dbaa037fbc772be822a1000adfdfef..725e8e571dac67cf9bf7eddaf58c3b0af8e6860c 100644 --- a/crates/extension_host/src/headless_host.rs +++ b/crates/extension_host/src/headless_host.rs @@ -194,7 +194,7 @@ impl HeadlessExtensionStore { } for (debug_adapter, meta) in &manifest.debug_adapters { - let schema_path = extension::build_debug_adapter_schema_path(debug_adapter, meta); + let schema_path = extension::build_debug_adapter_schema_path(debug_adapter, meta)?; this.update(cx, |this, _cx| { this.proxy.register_debug_adapter( diff --git a/crates/util/src/rel_path.rs b/crates/util/src/rel_path.rs index 5e20aacad5fe177cd1af65dc98aeb45565a3082e..8916236925e9be501d51290ca2aee7f963530dfd 100644 --- a/crates/util/src/rel_path.rs +++ b/crates/util/src/rel_path.rs @@ -27,7 +27,7 @@ pub struct RelPath(str); /// relative and normalized. /// /// This type is to [`RelPath`] as [`std::path::PathBuf`] is to [`std::path::Path`] -#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Clone, Ord, PartialOrd, Serialize)] pub struct RelPathBuf(String); impl RelPath { @@ -333,12 +333,36 @@ impl RelPathBuf { } } +impl<'de> Deserialize<'de> for RelPathBuf { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + let path = String::deserialize(deserializer)?; + let rel_path = + RelPath::new(Path::new(&path), PathStyle::local()).map_err(serde::de::Error::custom)?; + Ok(rel_path.into_owned()) + } +} + impl Into> for RelPathBuf { fn into(self) -> Arc { Arc::from(self.as_rel_path()) } } +impl AsRef for RelPathBuf { + fn as_ref(&self) -> &Path { + self.as_std_path() + } +} + +impl AsRef for RelPath { + fn as_ref(&self) -> &Path { + self.as_std_path() + } +} + impl AsRef for RelPathBuf { fn as_ref(&self) -> &RelPath { self.as_rel_path() @@ -378,12 +402,28 @@ pub fn rel_path(path: &str) -> &RelPath { RelPath::unix(path).unwrap() } +#[cfg(any(test, feature = "test-support"))] +#[track_caller] +pub fn rel_path_buf(path: &str) -> RelPathBuf { + RelPath::unix(path).unwrap().to_rel_path_buf() +} + impl PartialEq for RelPath { fn eq(&self, other: &str) -> bool { self.0 == *other } } +pub trait PathExt { + fn to_rel_path_buf(&self) -> Result; +} + +impl + ?Sized> PathExt for T { + fn to_rel_path_buf(&self) -> Result { + Ok(RelPath::new(self.as_ref(), PathStyle::local())?.into_owned()) + } +} + #[derive(Default)] pub struct RelPathComponents<'a>(&'a str);