fix: zip extract permission bits (#2102)

* Restore Unix executable bits when extracting zip archives

- SafeZipExtractor._extract_member now chmods each extracted file with the
  Unix mode stored in ZipInfo.external_attr (POSIX only, no-op when the
  archive carries no Unix attributes, e.g. Windows-authored zips).
- stdlib zipfile never restores permission bits itself; a fix is tracked
  upstream at https://github.com/python/cpython/pull/150061.
- Fixes archives with more than one executable losing their exec bit after
  extraction, e.g. the bundled JBR inside the Kotlin Language Server
  distribution (jbr/bin/java and native libs), which previously only had
  its single top-level launcher script chmod'd by the language-server code.

Fixes oraios/serena#2100

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Ben KeilandCopilot authored and GitHub committed 2026-09-23 17:47:53 +02:00
1 parent 9fa4237107
commit b83b655ced
3 files changed
+34

No files matched your search

+8
View File
@@ -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,
+7
View File
@@ -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}")
+19
View File
@@ -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."""