Skip to content
Open
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
5 changes: 5 additions & 0 deletions src/specify_cli/bundler/services/packager.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from pathlib import Path

from .. import BundlerError
from ..._download_security import MAX_ZIP_MEMBER_BYTES
from ..lib.yamlio import ensure_within
from ..models.manifest import BundleManifest
from .validator import validate_manifest
Expand Down Expand Up @@ -97,6 +98,10 @@ def build_bundle(
st = os.fstat(fh.fileno())
mode = 0o755 if st.st_mode & 0o111 else 0o644
info.external_attr = mode << 16
if st.st_size > MAX_ZIP_MEMBER_BYTES:
raise BundlerError(
f"Bundle file {arcname} exceeds {MAX_ZIP_MEMBER_BYTES}-byte limit"
)
archive.writestr(info, fh.read())
Comment on lines +101 to 105

return BuildResult(artifact_path=artifact_path, file_count=len(files))
Expand Down
26 changes: 26 additions & 0 deletions tests/unit/test_bundler_packager.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from specify_cli.bundler import BundlerError
from specify_cli.bundler.services.packager import build_bundle
from specify_cli._download_security import MAX_ZIP_MEMBER_BYTES
from tests.bundler_helpers import valid_manifest_dict


Expand Down Expand Up @@ -234,3 +235,28 @@ def test_toctou_stat_read_consistency(tmp_path: Path):
assert content == b"\x00\x01\x02\x03"
assert modes["assets/data.bin"] == 0o644
assert modes["README.md"] == 0o644


def test_oversized_asset_file_is_rejected(tmp_path: Path):
"""A single file exceeding MAX_ZIP_MEMBER_BYTES must be refused, not read
into memory unbounded."""
bundle = _make_bundle(tmp_path / "b")
oversized = bundle / "assets" / "huge.bin"
oversized.parent.mkdir(parents=True, exist_ok=True)
oversized.write_bytes(b"\x00" * (MAX_ZIP_MEMBER_BYTES + 1))

with pytest.raises(BundlerError, match="exceeds.*byte limit"):
build_bundle(bundle, output_dir=tmp_path / "out")


def test_asset_at_exact_size_limit_is_accepted(tmp_path: Path):
"""A file exactly at MAX_ZIP_MEMBER_BYTES must still be packaged."""
bundle = _make_bundle(tmp_path / "b")
at_limit = bundle / "assets" / "exact.bin"
at_limit.parent.mkdir(parents=True, exist_ok=True)
at_limit.write_bytes(b"\x00" * MAX_ZIP_MEMBER_BYTES)

result = build_bundle(bundle, output_dir=tmp_path / "out")
with zipfile.ZipFile(result.artifact_path) as archive:
content = archive.read("assets/exact.bin")
assert len(content) == MAX_ZIP_MEMBER_BYTES