Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/specify_cli/commands/bundle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,20 @@ def _local_manifest_source(arg: str):
error_type=BundlerError,
label="bundle manifest",
)
data = _yaml.safe_load(io.BytesIO(raw))
# The bounded-zip helpers above keep archive failures inside the
# BundlerError contract, but the manifest *parse* needs the same
# treatment: PyYAML raises YAMLError for malformed YAML, and its
# Reader wraps undecodable bytes in ReaderError (a YAMLError
# subclass) when fed a byte stream, so one clause covers both
# corruption modes. Mirrors yamlio.load_yaml's "Invalid YAML in ..."
# message so a manifest inside a .zip fails like the directory and
# bundle.yml sources do.
try:
data = _yaml.safe_load(io.BytesIO(raw))
except _yaml.YAMLError as exc:
Comment thread
marcelsafin marked this conversation as resolved.
raise BundlerError(
f"Invalid YAML in bundle.yml inside '{candidate}': {exc}"
) from exc
return BundleManifest.from_dict(data)

if candidate.name == "bundle.yml" or candidate.suffix in (".yml", ".yaml"):
Expand Down
33 changes: 33 additions & 0 deletions tests/integration/test_bundler_local_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,39 @@ def test_local_source_rejects_unknown_file(tmp_path: Path):
_local_manifest_source(str(weird))


def test_local_source_zip_invalid_manifest_yaml_raises_bundler_error(tmp_path: Path):
"""Malformed YAML inside a .zip's bundle.yml must raise BundlerError.

The zip open and member read already degrade into BundlerError via the
shared bounded-zip helpers, but the subsequent ``yaml.safe_load`` did
not: an invalid manifest escaped as a raw ``yaml.YAMLError`` traceback,
while the same manifest in a directory or as a plain bundle.yml goes
through ``load_yaml``'s "Invalid YAML in ..." BundlerError contract.
"""
artifact = tmp_path / "demo.zip"
with zipfile.ZipFile(artifact, "w") as archive:
archive.writestr("bundle.yml", "bundle: [unclosed\n")

with pytest.raises(BundlerError, match="Invalid YAML"):
_local_manifest_source(str(artifact))


def test_local_source_zip_non_utf8_manifest_raises_bundler_error(tmp_path: Path):
"""Undecodable bundle.yml bytes inside a .zip must raise BundlerError.

PyYAML's Reader wraps invalid bytes from a byte stream in ReaderError —
a ``YAMLError`` subclass — so this corruption mode rides the same clause,
but it deserves its own coverage: it is the realistic on-disk failure
(e.g. a UTF-16 manifest produced by PowerShell's ``Out-File``).
"""
artifact = tmp_path / "demo.zip"
with zipfile.ZipFile(artifact, "w") as archive:
archive.writestr("bundle.yml", b"\xff\xfe bundle \xc3\x28\n")

with pytest.raises(BundlerError, match="Invalid YAML"):
_local_manifest_source(str(artifact))


def test_install_bundled_extension_from_zip_offline(tmp_path: Path):
"""End-to-end: build → install (offline, local .zip) → list → remove."""
project = make_project(tmp_path / "proj")
Expand Down