From 07b6549002965fb8d2f7ee8eee60593245e5a2be Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:53:20 -0700 Subject: [PATCH 1/2] FIX handle converter preview path inspection errors Treat filesystem inspection failures as raw media data while preserving serializer errors and existing local file paths. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- pyrit/backend/services/converter_service.py | 29 +++++---- tests/unit/backend/test_converter_service.py | 68 +++++++++++++++++++- 2 files changed, 82 insertions(+), 15 deletions(-) diff --git a/pyrit/backend/services/converter_service.py b/pyrit/backend/services/converter_service.py index 7ec6aec799..13e75c7152 100644 --- a/pyrit/backend/services/converter_service.py +++ b/pyrit/backend/services/converter_service.py @@ -196,20 +196,23 @@ async def preview_conversion_async(self, *, request: ConverterPreviewRequest) -> ) await serializer.save_b64_image_async(data=value) original_value = str(serializer.value) - # Already an existing file on disk — keep as-is - elif Path(original_value).is_file(): - pass else: - # Treat as raw base64 - ext = DEFAULT_MEDIA_EXTENSIONS.get(str(data_type), ".bin") - - serializer = data_serializer_factory( - category="prompt-memory-entries", - data_type=data_type, - extension=ext, - ) - await serializer.save_b64_image_async(data=original_value) - original_value = str(serializer.value) + try: + is_existing_file = Path(original_value).is_file() + except (OSError, ValueError): + is_existing_file = False + + if not is_existing_file: + # Treat as raw base64 + ext = DEFAULT_MEDIA_EXTENSIONS.get(str(data_type), ".bin") + + serializer = data_serializer_factory( + category="prompt-memory-entries", + data_type=data_type, + extension=ext, + ) + await serializer.save_b64_image_async(data=original_value) + original_value = str(serializer.value) converters = self._gather_converters(converter_ids=request.converter_ids) steps, final_value, final_type = await self._apply_converters_async( diff --git a/tests/unit/backend/test_converter_service.py b/tests/unit/backend/test_converter_service.py index 8a0d4ed03e..ceab484216 100644 --- a/tests/unit/backend/test_converter_service.py +++ b/tests/unit/backend/test_converter_service.py @@ -441,7 +441,7 @@ async def test_preview_conversion_persists_raw_base64_for_audio_path(self) -> No mock_serializer.value = "/tmp/persisted.wav" mock_serializer.save_b64_image_async = AsyncMock() - raw_b64 = "UklGRiQAAABXQVZF" + raw_b64 = "UklGRiQ" + "A" * 5000 request = ConverterPreviewRequest( original_value=raw_b64, original_value_data_type="audio_path", @@ -452,13 +452,77 @@ async def test_preview_conversion_persists_raw_base64_for_audio_path(self) -> No "pyrit.backend.services.converter_service.data_serializer_factory", return_value=mock_serializer, ) as mock_factory: - await service.preview_conversion_async(request=request) + result = await service.preview_conversion_async(request=request) mock_factory.assert_called_once() # ext is the audio_path mapping from DEFAULT_MEDIA_EXTENSIONS assert mock_factory.call_args.kwargs["extension"] == ".wav" assert mock_factory.call_args.kwargs["data_type"] == "audio_path" mock_serializer.save_b64_image_async.assert_awaited_once_with(data=raw_b64) + assert result.model_dump(mode="json")["original_value"] == raw_b64 + + async def test_preview_conversion_persists_raw_base64_when_path_inspection_fails(self) -> None: + """Filesystem inspection failures classify the value as raw base64.""" + service = ConverterService() + mock_serializer = MagicMock() + mock_serializer.value = "/tmp/persisted.wav" + mock_serializer.save_b64_image_async = AsyncMock() + raw_b64 = "UklGRiQAAABXQVZF" + request = ConverterPreviewRequest( + original_value=raw_b64, + original_value_data_type="audio_path", + converter_ids=[], + ) + + with ( + patch.object(Path, "is_file", side_effect=OSError("path inspection failed")), + patch( + "pyrit.backend.services.converter_service.data_serializer_factory", + return_value=mock_serializer, + ), + ): + result = await service.preview_conversion_async(request=request) + + mock_serializer.save_b64_image_async.assert_awaited_once_with(data=raw_b64) + assert result.converted_value == "/tmp/persisted.wav" + + async def test_preview_conversion_propagates_invalid_base64_error_after_path_failure(self) -> None: + """Errors from raw base64 persistence are not mistaken for path inspection failures.""" + service = ConverterService() + mock_serializer = MagicMock() + mock_serializer.save_b64_image_async = AsyncMock(side_effect=ValueError("invalid base64")) + request = ConverterPreviewRequest( + original_value="A", + original_value_data_type="audio_path", + converter_ids=[], + ) + + with ( + patch.object(Path, "is_file", side_effect=ValueError("invalid path")), + patch( + "pyrit.backend.services.converter_service.data_serializer_factory", + return_value=mock_serializer, + ), + pytest.raises(ValueError, match="invalid base64"), + ): + await service.preview_conversion_async(request=request) + + async def test_preview_conversion_preserves_existing_file(self, tmp_path: Path) -> None: + """Existing local media paths pass through without being persisted again.""" + service = ConverterService() + media_path = tmp_path / "input.wav" + media_path.write_bytes(b"RIFF") + request = ConverterPreviewRequest( + original_value=str(media_path), + original_value_data_type="audio_path", + converter_ids=[], + ) + + with patch("pyrit.backend.services.converter_service.data_serializer_factory") as mock_factory: + result = await service.preview_conversion_async(request=request) + + mock_factory.assert_not_called() + assert result.converted_value == str(media_path) class TestGetConverterObjectsForIds: From 53f058764751df748723a5bea3e9b7b40c89ccc0 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:46:40 -0700 Subject: [PATCH 2/2] FIX preserve unrelated converter path errors Only classify path inspection failures as raw media when the value is syntactically valid base64. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fc71346a-510b-4b93-a031-1e810452dcb0 --- pyrit/backend/services/converter_service.py | 17 ++++++++++++ tests/unit/backend/test_converter_service.py | 29 +++++++++++++++++--- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/pyrit/backend/services/converter_service.py b/pyrit/backend/services/converter_service.py index 13e75c7152..24440978bc 100644 --- a/pyrit/backend/services/converter_service.py +++ b/pyrit/backend/services/converter_service.py @@ -13,6 +13,7 @@ """ import base64 +import binascii import mimetypes import uuid from functools import lru_cache @@ -200,6 +201,8 @@ async def preview_conversion_async(self, *, request: ConverterPreviewRequest) -> try: is_existing_file = Path(original_value).is_file() except (OSError, ValueError): + if not self._is_raw_base64(original_value): + raise is_existing_file = False if not is_existing_file: @@ -361,6 +364,20 @@ async def _apply_converters_async( return steps, current_value, current_type + @staticmethod + def _is_raw_base64(value: str) -> bool: + """ + Determine whether a value is syntactically valid raw base64. + + Returns: + True when the value is valid raw base64, otherwise False. + """ + try: + base64.b64decode(value, validate=True) + except binascii.Error: + return False + return True + # ============================================================================ # Singleton diff --git a/tests/unit/backend/test_converter_service.py b/tests/unit/backend/test_converter_service.py index ceab484216..f489e231ef 100644 --- a/tests/unit/backend/test_converter_service.py +++ b/tests/unit/backend/test_converter_service.py @@ -441,7 +441,7 @@ async def test_preview_conversion_persists_raw_base64_for_audio_path(self) -> No mock_serializer.value = "/tmp/persisted.wav" mock_serializer.save_b64_image_async = AsyncMock() - raw_b64 = "UklGRiQ" + "A" * 5000 + raw_b64 = base64.b64encode(b"RIFF" + b"\0" * 5000).decode() request = ConverterPreviewRequest( original_value=raw_b64, original_value_data_type="audio_path", @@ -461,7 +461,10 @@ async def test_preview_conversion_persists_raw_base64_for_audio_path(self) -> No mock_serializer.save_b64_image_async.assert_awaited_once_with(data=raw_b64) assert result.model_dump(mode="json")["original_value"] == raw_b64 - async def test_preview_conversion_persists_raw_base64_when_path_inspection_fails(self) -> None: + @pytest.mark.parametrize("path_error", [OSError("path inspection failed"), ValueError("invalid path")]) + async def test_preview_conversion_persists_raw_base64_when_path_inspection_fails( + self, path_error: OSError | ValueError + ) -> None: """Filesystem inspection failures classify the value as raw base64.""" service = ConverterService() mock_serializer = MagicMock() @@ -475,7 +478,7 @@ async def test_preview_conversion_persists_raw_base64_when_path_inspection_fails ) with ( - patch.object(Path, "is_file", side_effect=OSError("path inspection failed")), + patch.object(Path, "is_file", side_effect=path_error), patch( "pyrit.backend.services.converter_service.data_serializer_factory", return_value=mock_serializer, @@ -486,13 +489,31 @@ async def test_preview_conversion_persists_raw_base64_when_path_inspection_fails mock_serializer.save_b64_image_async.assert_awaited_once_with(data=raw_b64) assert result.converted_value == "/tmp/persisted.wav" + async def test_preview_conversion_propagates_non_base64_path_inspection_error(self) -> None: + """Filesystem errors for non-base64 values remain visible to callers.""" + service = ConverterService() + request = ConverterPreviewRequest( + original_value="not raw base64!", + original_value_data_type="audio_path", + converter_ids=[], + ) + + with ( + patch.object(Path, "is_file", side_effect=PermissionError("permission denied")), + patch("pyrit.backend.services.converter_service.data_serializer_factory") as mock_factory, + pytest.raises(PermissionError, match="permission denied"), + ): + await service.preview_conversion_async(request=request) + + mock_factory.assert_not_called() + async def test_preview_conversion_propagates_invalid_base64_error_after_path_failure(self) -> None: """Errors from raw base64 persistence are not mistaken for path inspection failures.""" service = ConverterService() mock_serializer = MagicMock() mock_serializer.save_b64_image_async = AsyncMock(side_effect=ValueError("invalid base64")) request = ConverterPreviewRequest( - original_value="A", + original_value="UklGRiQAAABXQVZF", original_value_data_type="audio_path", converter_ids=[], )