From 33484108a982678208a9619d03e67d691df49f03 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Aug 2026 09:04:24 -0500 Subject: [PATCH] [core] Replace a damaged existing file in write_file_if_changed (#18665) --- esphome/helpers.py | 13 ++++++++++++- tests/unit_tests/test_helpers.py | 25 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index 9b2a461ccd..7aa1a9a88c 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -552,7 +552,18 @@ def write_file_if_changed(path: Path, text: str) -> bool: """ src_content = None if path.is_file(): - src_content = read_file(path) + try: + src_content = path.read_text(encoding="utf-8") + except UnicodeDecodeError as err: + # Replace a damaged file rather than abort the regeneration that + # fixes it; an OSError may hide an intact file, so it still raises + _LOGGER.warning("Replacing damaged file %s: %s", path, err) + with suppress(OSError): + path.unlink(missing_ok=True) + except OSError as err: + from esphome.core import EsphomeError + + raise EsphomeError(f"Error reading file {path}: {err}") from err if src_content == text: return False write_file(path, text) diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index 6e00e5b80f..eaa7d5a8dc 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -253,6 +253,31 @@ class Test_write_file_if_changed: assert dst.read_text() == text + def test_damaged_existing_file_is_replaced( + self, tmp_path: Path, caplog: pytest.LogCaptureFixture + ): + """A non-UTF-8 existing file is logged and overwritten.""" + dst = tmp_path / "generated.txt" + dst.write_bytes(b"\xff\xfe") + + assert helpers.write_file_if_changed(dst, "fresh content") is True + + assert dst.read_text(encoding="utf-8") == "fresh content" + assert "Replacing damaged file" in caplog.text + + def test_unreadable_existing_file_still_raises(self, tmp_path: Path): + """An OSError on the comparison read still raises EsphomeError.""" + dst = tmp_path / "generated.txt" + dst.write_text("intact") + + with ( + patch.object(Path, "read_text", side_effect=OSError("permission denied")), + pytest.raises(EsphomeError, match="Error reading file"), + ): + helpers.write_file_if_changed(dst, "fresh content") + + assert dst.exists() + def test_dst_does_not_exist(self, tmp_path: Path): text = "A files are unique.\n" dst = tmp_path / "file-a.txt"