Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 141 additions & 88 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ regex = "1.11.1"
reqwest = { version = "0.12.24", default-features = false, features = ["blocking", "json", "rustls-tls"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.150"
serde_yaml = "0.9.34"
serde-saphyr = { version = "2.0.0", default-features = false, features = ["deserialize"] }
sha2 = "0.11.0"
shlex = "2.0.1"
tar = "0.4.46"
Expand Down
6 changes: 3 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ fn load_optional_cli_config_file(path: &Path) -> Result<CliConfig> {
}

let contents = path_fs::read_to_string(path)?;
let file: CliConfigFile = serde_yaml::from_str(&contents)
let file: CliConfigFile = serde_saphyr::from_str(&contents)
.map_err(|error| Error::config_format(path_fs::format_path_error(path, error)))?;
Ok(CliConfig::from(file))
}
Expand Down Expand Up @@ -351,7 +351,7 @@ fn load_filetypes(dir: &Path) -> Result<Vec<FiletypeConfig>> {
.into_iter()
.map(|path| {
let contents = path_fs::read_to_string(&path)?;
let file: FiletypeFile = serde_yaml::from_str(&contents)
let file: FiletypeFile = serde_saphyr::from_str(&contents)
.map_err(|error| Error::config_format(path_fs::format_path_error(&path, error)))?;
let Some(stem) = path.file_stem().and_then(|value| value.to_str()) else {
return Err(Error::config_format(format!(
Expand Down Expand Up @@ -393,7 +393,7 @@ fn load_lsps(dir: &Path) -> Result<Vec<LspConfig>> {
.into_iter()
.map(|path| {
let contents = path_fs::read_to_string(&path)?;
let file: LspFile = serde_yaml::from_str(&contents)
let file: LspFile = serde_saphyr::from_str(&contents)
.map_err(|error| Error::config_format(path_fs::format_path_error(&path, error)))?;
let Some(stem) = path.file_stem().and_then(|value| value.to_str()) else {
return Err(Error::config_format(format!(
Expand Down
17 changes: 17 additions & 0 deletions src/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,3 +387,20 @@ fn rejects_invalid_request_window_sizes() {
assert!(error.contains("max-requests-in-flight must be a positive integer"));
}
}

#[test]
fn loads_yaml_aliases_and_string_scalars() {
let global = TestDir::new("cli-config-yaml-aliases");
global.write_file(
"lsp-cli.yaml",
"lsp:\n first: &servers\n - on\n - '123'\n second: *servers\n",
);

let config = load_cli_config(global.path(), None).expect("YAML aliases should load");

assert_eq!(config.lsp_preferences["first"], ["on", "123"]);
assert_eq!(
config.lsp_preferences["second"],
config.lsp_preferences["first"]
);
}
15 changes: 14 additions & 1 deletion src/mason/install/artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,8 @@ fn ensure_decompressed_size_limit(size: u64, path: &str) -> Result<()> {

#[cfg(unix)]
fn owner_writable_mode(mode: u32) -> u32 {
mode & !0o022
// Archive permissions must not grant special privileges to installed files.
(mode & 0o777) & !0o022
}

fn write_file(path: &Path, bytes: &[u8]) -> Result<()> {
Expand All @@ -466,3 +467,15 @@ pub(super) fn parse_archive_file_spec(file: &str) -> (&str, Option<&str>) {
None => (file, None),
}
}

#[cfg(all(test, unix))]
mod tests {
use super::owner_writable_mode;

#[test]
fn archive_modes_cannot_grant_special_privileges() {
for (mode, expected) in [(0o107_777, 0o755), (0o106_666, 0o644), (0o100_700, 0o700)] {
assert_eq!(owner_writable_mode(mode), expected);
}
}
}
40 changes: 32 additions & 8 deletions src/mason/install/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,25 @@ fn parses_archive_file_spec() {
);
}

#[test]
fn extracts_vsix_as_a_zip_archive() {
let directory = tempfile::tempdir().expect("temporary directory should initialize");
fn zip_with_file(path: &str, mode: u32) -> Vec<u8> {
let mut archive = zip::ZipWriter::new(Cursor::new(Vec::new()));
archive
.start_file(
"extension/server/dist/server.js",
zip::write::SimpleFileOptions::default(),
path,
// Encode special bits in the archive to exercise extraction of untrusted modes.
zip::write::SimpleFileOptions::default().external_attributes(mode << 16),
)
.expect("VSIX entry should initialize");
.expect("ZIP entry should initialize");
archive
.write_all(b"server")
.expect("VSIX entry should be written");
let bytes = archive.finish().expect("VSIX should finish").into_inner();
.expect("ZIP entry should be written");
archive.finish().expect("ZIP should finish").into_inner()
}

#[test]
fn extracts_vsix_as_a_zip_archive() {
let directory = tempfile::tempdir().expect("temporary directory should initialize");
let bytes = zip_with_file("extension/server/dist/server.js", 0o100_644);

install_downloaded_artifact(directory.path(), "server.vsix", &bytes)
.expect("VSIX should extract");
Expand Down Expand Up @@ -267,3 +272,22 @@ fn installer_command_scopes_toolchain_environment_to_cargo() {
);
assert!(!npm_environment.contains_key(std::ffi::OsStr::new("CARGO_HOME")));
}

#[cfg(unix)]
#[test]
fn zip_extraction_strips_special_permissions_and_preserves_executable_bits() {
use std::os::unix::fs::PermissionsExt;

let directory = tempfile::tempdir().expect("temporary directory should initialize");
for (mode, expected) in [(0o107_777, 0o755), (0o106_666, 0o644)] {
let bytes = zip_with_file("server", mode);

install_downloaded_artifact(directory.path(), "server.zip", &bytes)
.expect("ZIP should extract");

let permissions = fs::metadata(directory.path().join("server"))
.expect("server should be extracted")
.permissions();
assert_eq!(permissions.mode() & 0o7777, expected);
}
}
2 changes: 1 addition & 1 deletion tests/e2e/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ fs_extra = "1.3.0"
reqwest = { version = "0.12.24", default-features = false, features = ["blocking", "json", "rustls-tls"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.150"
serde_yaml = "0.9.34"
serde-saphyr = { version = "2.0.0", default-features = false, features = ["deserialize"] }
sha2 = "0.11.0"
tar = { version = "0.4.46", optional = true }
tempfile = "3.23.0"
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/case_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ pub(crate) fn yaml_paths(directory: &Path) -> Result<Vec<PathBuf>, String> {
pub(crate) fn read_yaml<T: DeserializeOwned>(path: &Path) -> Result<T, String> {
let contents = fs::read_to_string(path)
.map_err(|error| format!("failed to read {}: {error}", path.display()))?;
serde_yaml::from_str(&contents)
serde_saphyr::from_str(&contents)
.map_err(|error| format!("failed to parse {}: {error}", path.display()))
}

Expand Down
6 changes: 3 additions & 3 deletions tests/e2e/manifest_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ fn manifest_rejects_a_pair_without_test_behavior() {

#[test]
fn manifest_rejects_unknown_fields() {
let error = serde_yaml::from_str::<SuiteFile>(
let error = serde_saphyr::from_str::<SuiteFile>(
"schema-version: 4\ncoverage: partial\ncommands: []\nunknown: true\n",
)
.expect_err("unknown manifest fields should fail");
Expand All @@ -296,7 +296,7 @@ fn manifest_rejects_a_source_language_without_a_preferred_server() {

#[test]
fn language_case_filename_must_match_its_id() {
let case = serde_yaml::from_str::<LanguageFile>(
let case = serde_saphyr::from_str::<LanguageFile>(
"language:\n id: gomod\n kind: metadata\n project: playground/gomod\n",
)
.expect("language case should parse");
Expand All @@ -310,7 +310,7 @@ fn language_case_filename_must_match_its_id() {

#[test]
fn language_case_must_own_its_pairs() {
let case = serde_yaml::from_str::<LanguageFile>(
let case = serde_saphyr::from_str::<LanguageFile>(
"language:\n id: gomod\n kind: metadata\n project: playground/gomod\npairs:\n - language: gowork\n server: gopls\n",
)
.expect("language case should parse");
Expand Down
Loading