Skip to content

Commit b3eb0fe

Browse files
authored
Merge pull request #727 from YMuskrat/fix/zip-extension-case
fix(input): recognize ZIP extensions case-insensitively
2 parents e9f7427 + c589c32 commit b3eb0fe

2 files changed

Lines changed: 47 additions & 6 deletions

File tree

‎src/skillspector/input_handler.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -813,7 +813,7 @@ def resolve(self, input_path: str) -> tuple[Path, str]:
813813
if self._is_file_url(input_path):
814814
return self._download_file(input_path), "url"
815815
normalized_local_path = validate_local_input_path(Path(input_path))
816-
if input_path.endswith(".zip"):
816+
if input_path.lower().endswith(".zip"):
817817
return self._extract_zip(normalized_local_path), "zip"
818818
if input_path.endswith(".md"):
819819
return self._wrap_single_file(normalized_local_path), "file"
@@ -1366,7 +1366,7 @@ def _download_file(self, url: str) -> Path:
13661366
download_path.unlink(missing_ok=True)
13671367
raise
13681368

1369-
is_zip = filename.endswith(".zip") or content_type.startswith("application/zip")
1369+
is_zip = filename.lower().endswith(".zip") or content_type.startswith("application/zip")
13701370
if is_zip:
13711371
zip_path = temp_dir / "download.zip"
13721372
download_path.replace(zip_path)
@@ -1389,7 +1389,7 @@ def _download_transitive_file(self, url: str) -> Path:
13891389
raise ValueError(f"Failed to download file: {exc}") from exc
13901390
temp_dir = self._get_temp_dir()
13911391
self._record_artifacts(1)
1392-
if filename.endswith(".zip") or headers.get("content-type", "").startswith(
1392+
if filename.lower().endswith(".zip") or headers.get("content-type", "").startswith(
13931393
"application/zip"
13941394
):
13951395
zip_path = temp_dir / "download.zip"

‎tests/unit/test_input_handler.py‎

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,19 +374,21 @@ def test_windows_no_follow_open_rejects_canonical_path_mismatch(
374374
_open_regular_file_from_windows_handle(source)
375375

376376

377-
def test_resolve_zip_file(tmp_path: Path) -> None:
378-
"""Resolving a .zip file extracts and returns the extract dir."""
377+
@pytest.mark.parametrize("extension", [".zip", ".ZIP", ".ZiP"])
378+
def test_resolve_zip_file(tmp_path: Path, extension: str) -> None:
379+
"""ZIP extensions are recognized regardless of case for local inputs."""
379380
import zipfile
380381

381382
(tmp_path / "SKILL.md").write_text("# Skill", encoding="utf-8")
382-
zip_path = tmp_path / "skill.zip"
383+
zip_path = tmp_path / f"skill{extension}"
383384
with zipfile.ZipFile(zip_path, "w") as zf:
384385
zf.write(tmp_path / "SKILL.md", "SKILL.md")
385386
handler = InputHandler()
386387
try:
387388
resolved, source_type = handler.resolve(str(zip_path))
388389
assert resolved.is_dir()
389390
assert source_type == "zip"
391+
assert (resolved / "SKILL.md").read_text(encoding="utf-8") == "# Skill"
390392
finally:
391393
handler.cleanup()
392394

@@ -601,6 +603,45 @@ def serve(request: httpx.Request) -> httpx.Response:
601603
handler.cleanup()
602604

603605

606+
@pytest.mark.parametrize("budgeted", [False, True], ids=["direct", "workflow-budget"])
607+
@pytest.mark.parametrize("extension", [".zip", ".ZIP", ".ZiP"])
608+
def test_file_url_download_extracts_zip_regardless_of_extension_case(
609+
monkeypatch: pytest.MonkeyPatch, extension: str, budgeted: bool
610+
) -> None:
611+
"""ZIP filenames trigger extraction even without a ZIP content type."""
612+
import io
613+
import zipfile
614+
615+
skill = b"---\nname: demo\ndescription: demo\n---\n# Demo\n"
616+
archive = io.BytesIO()
617+
with zipfile.ZipFile(archive, "w") as zf:
618+
zf.writestr("SKILL.md", skill)
619+
url = f"https://raw.githubusercontent.com/org/repo/main/skill{extension}"
620+
requested: list[str] = []
621+
622+
def serve(request: httpx.Request) -> httpx.Response:
623+
requested.append(str(request.url))
624+
return httpx.Response(
625+
200, content=archive.getvalue(), headers={"content-type": "application/octet-stream"}
626+
)
627+
628+
real_client = httpx.Client
629+
monkeypatch.setattr(
630+
"skillspector.input_handler.httpx.Client",
631+
lambda *args, **kwargs: real_client(*args, transport=httpx.MockTransport(serve), **kwargs),
632+
)
633+
monkeypatch.setattr("skillspector.input_handler._is_private_ip", lambda _host: False)
634+
handler = InputHandler(transitive_budget=WorkflowResourceBudget() if budgeted else None)
635+
try:
636+
resolved, source_type = handler.resolve(url)
637+
638+
assert source_type == "url"
639+
assert requested == [url]
640+
assert (resolved / "SKILL.md").read_bytes() == skill
641+
finally:
642+
handler.cleanup()
643+
644+
604645
def test_validate_url_host_scp_extracts_github() -> None:
605646
"""_validate_url_host extracts 'github.com' from an scp-style URL."""
606647
with patch("skillspector.input_handler._is_private_ip", return_value=False):

0 commit comments

Comments
 (0)