Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 11 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,17 @@ def _local_manifest_source(arg: str):
error_type=BundlerError,
label="bundle manifest",
)
data = _yaml.safe_load(io.BytesIO(raw))
try:
data = _yaml.safe_load(io.BytesIO(raw))
except _yaml.YAMLError as exc:
# The sibling directory/bundle.yml branches reach YAML through
# load_yaml(), which turns a parse failure into a BundlerError. This
# branch parses inline, so without this it raises a raw YAMLError --
# neither a ValueError nor an OSError -- which escapes
# bundle_install()'s `except BundlerError` as a traceback.
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
44 changes: 44 additions & 0 deletions tests/integration/test_bundler_local_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,50 @@ def test_local_zip_uses_bounded_archive_open(tmp_path: Path):
_local_manifest_source(str(artifact))


def test_local_zip_wraps_malformed_manifest_yaml(tmp_path: Path):
"""A malformed bundle.yml inside a .zip must raise BundlerError.

The zip branch parses YAML inline rather than through load_yaml(), so the
raw yaml.YAMLError used to escape. It is neither a ValueError nor an
OSError, so nothing upstream caught it.
"""
artifact = tmp_path / "bad-manifest.zip"
with zipfile.ZipFile(artifact, "w") as archive:
archive.writestr("bundle.yml", "bundle: [unclosed\n id: demo\n")

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


def test_malformed_manifest_yaml_fails_alike_for_every_local_source(tmp_path: Path):
"""`bundle install` reports malformed YAML the same way for all 3 sources.

Directory and bundle.yml sources already exited 1 with an "Invalid YAML"
message; the .zip source dumped a yaml.parser.ParserError traceback.
"""
bad_yaml = "bundle: [unclosed\n id: demo\n"

directory = tmp_path / "dir-src"
directory.mkdir()
(directory / "bundle.yml").write_text(bad_yaml, encoding="utf-8")

manifest_file = tmp_path / "standalone.yml"
manifest_file.write_text(bad_yaml, encoding="utf-8")

artifact = tmp_path / "artifact.zip"
with zipfile.ZipFile(artifact, "w") as archive:
archive.writestr("bundle.yml", bad_yaml)

runner = CliRunner()
for source in (directory, manifest_file, artifact):
result = runner.invoke(app, ["bundle", "install", str(source)])
assert result.exit_code == 1, f"{source.name}: {result.output}"
assert result.exception is None or isinstance(
result.exception, SystemExit
), f"{source.name} leaked {type(result.exception).__name__}"
assert "Invalid YAML" in result.output, f"{source.name}: {result.output}"


def test_invalid_local_manifest_is_rejected_before_project_init(
tmp_path: Path,
monkeypatch,
Expand Down