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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user