fix(bundle): wrap local .zip errors in BundlerError instead of raw crash#3713
fix(bundle): wrap local .zip errors in BundlerError instead of raw crash#3713jawwad-ali wants to merge 3 commits into
Conversation
_local_manifest_source's .zip branch called zipfile.ZipFile() and yaml.safe_load() with no error handling. A .zip-suffixed file that is not a valid archive raised a raw zipfile.BadZipFile, and a valid zip with a malformed embedded bundle.yml raised a raw yaml.YAMLError. bundle_install only does `except BundlerError`, so both escaped to an unhandled traceback instead of the intended clean `Error: ...` + exit 1. Wrap both failure modes in BundlerError, mirroring the remote-download sibling (_download_remote_manifest already wraps its _local_manifest_source call for exactly this). The directory and bundle.yml branches already report cleanly via BundleManifest.from_file -> load_yaml; this aligns the .zip branch. Valid archives are unaffected. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Hardens local bundle ZIP parsing by translating archive and YAML failures into BundlerError.
Changes:
- Wraps invalid ZIP and malformed YAML errors.
- Adds regression tests for both cases.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/commands/bundle/__init__.py |
Adds error translation for local ZIP manifests. |
tests/integration/test_bundler_local_install.py |
Tests corrupt ZIP and malformed YAML handling. |
Review details
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Medium
Address review feedback: ZipFile() validates only the central directory, so a
corrupt MEMBER still failed after the open and escaped bundle_install's
`except BundlerError` as a traceback.
Verified both failure modes on a directory-intact archive: a flipped byte in a
stored member raises zipfile.BadZipFile("Bad CRC-32 for file 'bundle.yml'"), and
a mangled deflate stream raises zlib.error. Neither is an OSError subclass, so
the read boundary lists BadZipFile/OSError/zlib.error/EOFError explicitly (the
review named BadZipFile; zlib.error is the same class of corruption and would
have escaped an OSError-only clause).
Valid archives are unaffected.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Confirmed and fixed in 6d5f8d2 — you're right that I reproduced both corruption modes on a directory-intact archive:
Since neither is an |
…ied text
Self-review follow-up: the exception sets were still incomplete, so a raw
exception could escape bundle_install's `except BundlerError` after all.
Verified against CPython's zipfile:
read() RuntimeError -- password-protected artifact (`zip -e`)
NotImplementedError -- unsupported compression method
open() NotImplementedError -- unsupported zip version
ValueError
None are OSError subclasses, so each is now listed explicitly.
Also escape the interpolated exception text: zipfile embeds bytes taken FROM
THE ARCHIVE in some messages -- BadZipFile("File name in directory 'bundle.yml'
and header b'[/red]abcd' differ.") -- and that text reaches the user through
_fail(), which renders Rich markup. Unescaped, a crafted member name raised
MarkupError instead of reporting the corruption. The user's own typed path is
left as-is, matching the other messages in this module.
Three new tests (encrypted, unsupported compression, crafted member name) all
fail before this commit.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Problem
The local
.zipbranch of_local_manifest_sourceopens the archive and parses its embeddedbundle.ymlwith no error handling:.zip-suffixed file that isn't a valid archive →zipfile.BadZipFile.bundle.ymlis malformed →yaml.YAMLError.Neither is a
BundlerError, andbundle_installonly doesexcept BundlerError, so both escape to an unhandled traceback instead of the intended cleanError: …+ exit 1. Reproduced both onmain(4d3a428).This is a parity gap: the directory and
bundle.ymlbranches go throughBundleManifest.from_file→load_yaml, which wraps failures inBundlerError; and the remote sibling_download_remote_manifestexplicitly wraps its_local_manifest_sourcecall intry/except → BundlerError. Only the direct local.zipbranch is unguarded.Fix
Wrap both failure modes in
BundlerError(hardening the source function, so every caller benefits):Valid archives are unaffected — no behaviour change for correct input.
Verification
test_local_source_corrupt_zip_raises_clean_errorandtest_local_source_zip_with_malformed_bundle_yml_raises_clean_error: fail before (rawBadZipFile/YAMLError), pass after (cleanBundlerError).test_bundler_local_install.py: 12 passed.ruffclean.AI-assisted: authored with Claude Code. I reproduced both raw crashes on
mainvia a direct_local_manifest_sourcecall and mirrored the remote-download path's existing error wrapping.