From a61f5fe5933596be5549dec5061a753fe3abd44d Mon Sep 17 00:00:00 2001 From: Arjun Ganesh Date: Sun, 27 Sep 2026 20:41:23 +0200 Subject: [PATCH] fix(environment): prevent truncation on invalid file content Validate content type and encode text before opening the target so invalid model output cannot truncate an existing file. Signed-off-by: Arjun Ganesh --- .../adk/environment/_local_environment.py | 10 ++++--- .../environment/test_local_environment.py | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/google/adk/environment/_local_environment.py b/src/google/adk/environment/_local_environment.py index 33b99c56965..7c81533752d 100644 --- a/src/google/adk/environment/_local_environment.py +++ b/src/google/adk/environment/_local_environment.py @@ -236,8 +236,10 @@ def _sync_read(path: Path) -> bytes: def _sync_write(path: Path, content: str | bytes) -> None: os.makedirs(path.parent, exist_ok=True) if isinstance(content, str): - with open(path, 'w', encoding='utf-8', newline='') as f: - f.write(content) + data = content.encode('utf-8') + elif isinstance(content, bytes): + data = content else: - with open(path, 'wb') as f: - f.write(content) + raise TypeError('content must be str or bytes') + with open(path, 'wb') as f: + f.write(data) diff --git a/tests/unittests/environment/test_local_environment.py b/tests/unittests/environment/test_local_environment.py index 5cdafda5af2..6f314cbf758 100644 --- a/tests/unittests/environment/test_local_environment.py +++ b/tests/unittests/environment/test_local_environment.py @@ -80,6 +80,32 @@ async def test_write_preserves_explicit_crlf(self, env: LocalEnvironment): assert data == b"first\r\nsecond\r\n" + @pytest.mark.asyncio + async def test_unencodable_text_does_not_truncate_existing_file( + self, env: LocalEnvironment + ): + """An encoding error leaves existing file bytes intact.""" + original = b"keep these bytes\n" + await env.write_file("existing.txt", original) + + with pytest.raises(UnicodeEncodeError): + await env.write_file("existing.txt", "replacement \ud800") + + assert await env.read_file("existing.txt") == original + + @pytest.mark.asyncio + async def test_invalid_content_does_not_truncate_existing_file( + self, env: LocalEnvironment + ): + """An invalid content type leaves existing file bytes intact.""" + original = b"keep these bytes\n" + await env.write_file("existing.txt", original) + + with pytest.raises(TypeError, match="content must be str or bytes"): + await env.write_file("existing.txt", 123) + + assert await env.read_file("existing.txt") == original + @pytest.mark.asyncio async def test_write_creates_parent_dirs(self, env: LocalEnvironment): """Parent directories are created automatically."""