diff --git a/CHANGELOG.md b/CHANGELOG.md index ea9c7316..c4a313ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,6 +107,14 @@ Status of the `main` branch. Changes prior to the next official version change w thread (#2038) * Language Servers: + - Fix: `SafeZipExtractor` discarded Unix executable permission bits stored in extracted + archives' `ZipInfo.external_attr` (a long-standing stdlib `zipfile` limitation, + tracked upstream at https://github.com/python/cpython/pull/150061), leaving every + extracted file with default, non-executable permissions. This broke language servers + whose archive contains more than the single top-level launcher script that + per-language-server setup code re-chmods, e.g. the Kotlin Language Server's bundled + JetBrains Runtime (`jbr/bin/java` and native libs), whose launcher failed to exec it + with a permission error. Executable bits are now restored for every extracted file (#2100) - Add Astro language server support via `@astrojs/language-server` with a companion TypeScript language server (`@astrojs/ts-plugin`) for cross-file code intelligence (#2085) - Fix: Dart analysis server no longer receives rootUri/rootPath, which added the monorepo root as an extra analysis root and could pin a CPU core at idle (#2045) - Fix: The C# language server opened every `.csproj` found anywhere under the repository root, diff --git a/src/solidlsp/util/zip.py b/src/solidlsp/util/zip.py index 2f7a1879..43b34984 100644 --- a/src/solidlsp/util/zip.py +++ b/src/solidlsp/util/zip.py @@ -101,6 +101,13 @@ class SafeZipExtractor: with zip_ref.open(member) as source, open(final_path, "wb") as target: target.write(source.read()) + # stdlib zipfile does not restore Unix permission bits on extraction; tracked + # upstream at https://github.com/python/cpython/pull/150061 + if os.name == "posix": + unix_mode = member.external_attr >> 16 + if unix_mode: + os.chmod(final_path, unix_mode) + if self.verbose: log.info(f"Extracted: {member.filename}") diff --git a/test/solidlsp/util/test_zip.py b/test/solidlsp/util/test_zip.py index 5fa545ba..5aaf9250 100644 --- a/test/solidlsp/util/test_zip.py +++ b/test/solidlsp/util/test_zip.py @@ -1,3 +1,4 @@ +import os import sys import zipfile from pathlib import Path @@ -90,6 +91,24 @@ def test_skip_on_error(monkeypatch, temp_zip_file: Path, tmp_path: Path) -> None assert (dest_dir / "folder" / "file3.txt").exists() +@pytest.mark.skipif(sys.platform.startswith("win"), reason="Unix permission bits are not applicable on Windows") +def test_restores_executable_permission(tmp_path: Path) -> None: + """Executable bits stored in the archive's external_attr should be restored on extraction.""" + zip_path = tmp_path / "exec.zip" + with zipfile.ZipFile(zip_path, "w") as zipf: + info = zipfile.ZipInfo("bin/tool") + info.external_attr = 0o755 << 16 + zipf.writestr(info, "#!/bin/sh\necho hi\n") + + dest_dir = tmp_path / "extracted" + extractor = SafeZipExtractor(zip_path, dest_dir, verbose=False) + extractor.extract_all() + + extracted_file = dest_dir / "bin" / "tool" + assert extracted_file.exists() + assert os.stat(extracted_file).st_mode & 0o111 + + @pytest.mark.skipif(not sys.platform.startswith("win"), reason="Windows-only test") def test_long_path_normalization(temp_zip_file: Path, tmp_path: Path) -> None: r"""Ensure _normalize_path adds \\?\\ prefix on Windows."""