Skip to content

Commit d384268

Browse files
Add ability to unzip artifacts zipped with zstd
1 parent e372ca4 commit d384268

File tree

3 files changed

+69
-3
lines changed

3 files changed

+69
-3
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ sentry-kafka-schemas==2.1.2
2020
sentry-sdk>=2.36.0
2121
sortedcontainers>=2.4.0
2222
typing-extensions>=4.15.0
23+
zipfile-zstd==0.0.4

src/launchpad/artifacts/providers/zip_provider.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import zipfile
22

33
from pathlib import Path
4-
from typing import List
4+
from typing import List, Set
55

66
from launchpad.utils.file_utils import cleanup_directory, create_temp_directory
77
from launchpad.utils.logging import get_logger
@@ -11,6 +11,9 @@
1111
DEFAULT_MAX_FILE_COUNT = 100000
1212
DEFAULT_MAX_UNCOMPRESSED_SIZE = 10 * 1024 * 1024 * 1024
1313

14+
# Compression method constants
15+
COMPRESSION_ZSTD = 93 # Zstandard compression method
16+
1417

1518
class UnreasonableZipError(ValueError):
1619
"""Raised when a zip file exceeds reasonable limits."""
@@ -92,13 +95,43 @@ def extract_to_temp_directory(self) -> Path:
9295
self._temp_dirs.append(temp_dir)
9396

9497
self._safe_extract(str(self.path), str(temp_dir))
95-
logger.debug(f"Extracted zip contents to {temp_dir} using system unzip")
98+
logger.debug(f"Extracted zip contents to {temp_dir}")
9699

97100
return temp_dir
98101

102+
def _detect_compression_methods(self, zip_path: str) -> Set[int]:
103+
"""Detect compression methods used in the zip file.
104+
105+
Args:
106+
zip_path: Path to the zip file
107+
108+
Returns:
109+
Set of compression method integers used in the zip file
110+
"""
111+
with zipfile.ZipFile(zip_path, "r") as zf:
112+
return {info.compress_type for info in zf.infolist()}
113+
99114
def _safe_extract(self, zip_path: str, extract_path: str):
100-
"""Extract the zip contents to a temporary directory, ensuring that the paths are safe from path traversal attacks."""
115+
"""Extract the zip contents to a temporary directory, ensuring that the paths are safe from path traversal attacks.
116+
117+
Supports both standard compression methods and Zstandard compression.
118+
"""
101119
base_dir = Path(extract_path)
120+
121+
# Detect if zstandard compression is used
122+
compression_methods = self._detect_compression_methods(zip_path)
123+
uses_zstd = COMPRESSION_ZSTD in compression_methods
124+
125+
if uses_zstd:
126+
logger.debug("Detected Zstandard compression in zip file")
127+
try:
128+
import zipfile_zstd # noqa: F401
129+
except ImportError:
130+
raise RuntimeError(
131+
"Zstandard-compressed zip file detected, but zipfile-zstd package is not installed. "
132+
"Install it with: pip install zipfile-zstd"
133+
)
134+
102135
with zipfile.ZipFile(zip_path, "r") as zip_ref:
103136
check_reasonable_zip(zip_ref)
104137
for member in zip_ref.namelist():

tests/unit/artifacts/providers/test_zip_provider.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pytest
77

88
from launchpad.artifacts.providers.zip_provider import (
9+
COMPRESSION_ZSTD,
910
UnreasonableZipError,
1011
UnsafePathError,
1112
ZipProvider,
@@ -88,6 +89,37 @@ def test_invalid_zip_file(self) -> None:
8889
with pytest.raises(zipfile.BadZipFile):
8990
provider.extract_to_temp_directory()
9091

92+
def test_detect_compression_methods(self, hackernews_xcarchive: Path) -> None:
93+
provider = ZipProvider(hackernews_xcarchive)
94+
methods = provider._detect_compression_methods(str(hackernews_xcarchive))
95+
96+
# Verify detection works and standard zips use deflate compression, not zstd
97+
assert zipfile.ZIP_DEFLATED in methods
98+
assert COMPRESSION_ZSTD not in methods
99+
100+
def test_extract_zstd_zip(self) -> None:
101+
"""Test that zstd-compressed zips can be extracted when zipfile-zstd is available."""
102+
try:
103+
import zipfile_zstd # noqa: F401
104+
except ImportError:
105+
pytest.skip("zipfile-zstd not installed")
106+
107+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as temp_file:
108+
temp_path = Path(temp_file.name)
109+
110+
with zipfile.ZipFile(temp_path, "w") as zf:
111+
zf.writestr("test.txt", "content", compress_type=COMPRESSION_ZSTD)
112+
113+
try:
114+
provider = ZipProvider(temp_path)
115+
temp_dir = provider.extract_to_temp_directory()
116+
117+
assert temp_dir.exists()
118+
assert (temp_dir / "test.txt").exists()
119+
assert (temp_dir / "test.txt").read_text() == "content"
120+
finally:
121+
temp_path.unlink(missing_ok=True)
122+
91123

92124
class TestIsSafePath:
93125
def test_valid_paths(self) -> None:

0 commit comments

Comments
 (0)