From 1b8153bd465e9b047caeef9951cd9b230e41456e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Feb 2026 16:04:12 +0100 Subject: [PATCH] address bot review comemnts --- esphome/bundle.py | 18 ++++++++++++++--- tests/unit_tests/test_bundle.py | 35 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/esphome/bundle.py b/esphome/bundle.py index 53cf15dc63..dd11a75514 100644 --- a/esphome/bundle.py +++ b/esphome/bundle.py @@ -36,6 +36,7 @@ BUNDLE_EXTENSION = ".esphomebundle.tar.gz" MANIFEST_FILENAME = "manifest.json" CURRENT_MANIFEST_VERSION = 1 MAX_DECOMPRESSED_SIZE = 500 * 1024 * 1024 # 500 MB +MAX_MANIFEST_SIZE = 1024 * 1024 # 1 MB # Directories preserved across bundle extractions (build caches) _PRESERVE_DIRS = (".esphome", ".pioenvs", ".pio") @@ -510,6 +511,12 @@ def _read_manifest_from_tar(tar: tarfile.TarFile) -> dict[str, Any]: if f is None: raise EsphomeError("Invalid bundle: manifest.json is not a regular file") + if member.size > MAX_MANIFEST_SIZE: + raise EsphomeError( + f"Invalid bundle: manifest.json too large " + f"({member.size} bytes, max {MAX_MANIFEST_SIZE})" + ) + try: manifest = json.loads(f.read()) except (json.JSONDecodeError, UnicodeDecodeError) as err: @@ -616,11 +623,16 @@ def _default_target_dir(bundle_path: Path) -> Path: def _restore_preserved_dirs(preserved: dict[str, Path], target_dir: Path) -> None: - """Move preserved build cache directories back into target_dir.""" + """Move preserved build cache directories back into target_dir. + + If the bundle contained entries under a preserved directory name, + the extracted copy is removed so the original cache always wins. + """ for dirname, src in preserved.items(): dst = target_dir / dirname - if not dst.exists(): - shutil.move(str(src), str(dst)) + if dst.exists(): + shutil.rmtree(dst) + shutil.move(str(src), str(dst)) def prepare_bundle_for_compile( diff --git a/tests/unit_tests/test_bundle.py b/tests/unit_tests/test_bundle.py index 5f073e2d31..8dac080884 100644 --- a/tests/unit_tests/test_bundle.py +++ b/tests/unit_tests/test_bundle.py @@ -411,6 +411,18 @@ def test_extract_bundle_manifest_version_zero(tmp_path: Path) -> None: extract_bundle(bundle_path, tmp_path / "out") +def test_extract_bundle_manifest_too_large( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Oversized manifest.json is rejected.""" + monkeypatch.setattr("esphome.bundle.MAX_MANIFEST_SIZE", 50) + + bundle_path = _make_bundle(tmp_path) + + with pytest.raises(EsphomeError, match="manifest.json too large"): + extract_bundle(bundle_path, tmp_path / "out") + + def test_extract_bundle_manifest_not_regular_file(tmp_path: Path) -> None: """manifest.json that is a directory entry raises EsphomeError.""" bundle_path = tmp_path / f"dirmanifest{BUNDLE_EXTENSION}" @@ -530,6 +542,29 @@ def test_prepare_bundle_missing_file(tmp_path: Path) -> None: prepare_bundle_for_compile(missing) +def test_prepare_bundle_cache_wins_over_bundle_content(tmp_path: Path) -> None: + """Pre-existing build cache is restored even if the bundle contains those dirs.""" + bundle_path = _make_bundle( + tmp_path, + extra_files={ + ".esphome/from_bundle.json": b'{"from": "bundle"}', + }, + ) + target = tmp_path / "work" + target.mkdir() + + # Pre-existing build cache + esphome_dir = target / ".esphome" + esphome_dir.mkdir() + (esphome_dir / "local_cache.json").write_text('{"from": "local"}') + + prepare_bundle_for_compile(bundle_path, target) + + # Local cache should win over bundle content + assert (target / ".esphome" / "local_cache.json").read_text() == '{"from": "local"}' + assert not (target / ".esphome" / "from_bundle.json").exists() + + def test_prepare_bundle_default_target_dir(tmp_path: Path) -> None: """prepare_bundle_for_compile uses default dir when target_dir is None.""" bundle_path = _make_bundle(tmp_path)