fix: decode the zipped manifest as UTF-8 before parsing (#3958)

Review follow-up: feeding PyYAML the byte stream let its Reader honour
a UTF-16 BOM and accept a manifest yamlio.load_yaml rejects, so zip and
directory sources diverged. Decode raw as UTF-8 (UnicodeError ->
BundlerError 'Could not read ...') then parse, and cover a well-formed
UTF-16 manifest in the regression tests.

Assisted-by: GitHub Copilot (model: claude-fable-5, autonomous)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Marsel Safin
2026-08-13 21:47:30 +02:00
committed by GitHub
parent 7346819039
commit 56aec8a936
2 changed files with 46 additions and 3 deletions
+13 -3
View File
@@ -772,8 +772,6 @@ def _local_manifest_source(arg: str):
return BundleManifest.from_file(manifest_path)
if candidate.suffix == ".zip":
import io
import yaml as _yaml
from ..._download_security import open_zip_bounded, read_zip_member_limited
@@ -791,8 +789,20 @@ def _local_manifest_source(arg: str):
error_type=BundlerError,
label="bundle manifest",
)
# The bounded-zip helpers above keep archive failures inside the
# BundlerError contract, but the manifest bytes need the same
# treatment as yamlio.load_yaml: decode as UTF-8 explicitly —
# feeding PyYAML the byte stream would let its Reader auto-detect
# a UTF-16 BOM and accept a manifest the directory and bundle.yml
# sources reject.
try:
data = _yaml.safe_load(io.BytesIO(raw))
text = raw.decode("utf-8")
except UnicodeError as exc:
raise BundlerError(
f"Could not read bundle.yml inside '{candidate}': {exc}"
) from exc
try:
data = _yaml.safe_load(text)
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
@@ -62,6 +62,39 @@ def test_local_source_rejects_unknown_file(tmp_path: Path):
_local_manifest_source(str(weird))
def test_local_source_zip_non_utf8_manifest_raises_bundler_error(tmp_path: Path):
"""Undecodable bundle.yml bytes inside a .zip must raise BundlerError.
The manifest bytes are decoded as UTF-8 explicitly, matching
``yamlio.load_yaml``'s "Could not read ..." contract, instead of
escaping as a raw ``UnicodeDecodeError``/``ReaderError`` traceback.
"""
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="Could not read"):
_local_manifest_source(str(artifact))
def test_local_source_zip_utf16_manifest_rejected_like_directory(tmp_path: Path):
"""A well-formed UTF-16 manifest must fail the same way in a .zip.
``yamlio.load_yaml`` decodes strictly as UTF-8, so a UTF-16 bundle.yml
(the realistic PowerShell ``Out-File`` output) is rejected when read
from a directory. Feeding the zip bytes straight to PyYAML would let
its Reader honour the UTF-16 BOM and *accept* the same manifest,
making zip and directory sources diverge.
"""
artifact = tmp_path / "demo.zip"
manifest_text = "bundle:\n id: demo-bundle\n version: 1.0.0\n"
with zipfile.ZipFile(artifact, "w") as archive:
archive.writestr("bundle.yml", manifest_text.encode("utf-16"))
with pytest.raises(BundlerError, match="Could not read"):
_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")