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
11 changes: 11 additions & 0 deletions src/specify_cli/bundles/packager.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ def build_bundle(
"README.md describing it."
)

# The packager never packages symlinked files, so a symlinked manifest or
# README would pass the checks above yet be left out of the artifact,
# producing a zip that 'specify bundle install' rejects. Fail the build.
for required in (manifest_path, bundle_dir / "README.md"):
if required.is_symlink():
raise BundlerError(
f"'{required.name}' in '{bundle_dir}' is a symlink. The manifest "
"and README.md must be regular files so they are included in "
"the artifact."
)

manifest = BundleManifest.from_file(manifest_path)
report = validate_manifest(manifest)
if not report.ok:
Expand Down
16 changes: 16 additions & 0 deletions tests/specify_cli/bundles/test_packager.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,22 @@ def test_symlinked_directory_is_not_followed(tmp_path: Path):
assert not any("secret" in name for name in names)


@pytest.mark.parametrize("name", ["bundle.yml", "README.md"])
def test_build_refuses_symlinked_manifest_or_readme(tmp_path: Path, name: str):
bundle = _make_bundle(tmp_path / "b")
(bundle / "shared").mkdir()
(bundle / name).rename(bundle / "shared" / name)
try:
(bundle / name).symlink_to(Path("shared") / name)
except (OSError, NotImplementedError):
pytest.skip("symlinks not supported on this platform")
# The packager skips symlinked files, so building would otherwise succeed
# and emit an artifact without the manifest or README at its root.
with pytest.raises(BundlerError, match="symlink"):
build_bundle(bundle, output_dir=tmp_path / "out")
assert not list((tmp_path / "out").glob("*.zip"))


def test_unsafe_bundle_id_is_rejected_before_build(tmp_path: Path):
data = valid_manifest_dict()
data["bundle"]["id"] = "../evil"
Expand Down
Loading