diff --git a/python/semantic_kernel/contents/utils/data_uri.py b/python/semantic_kernel/contents/utils/data_uri.py index 22f514fac9b0..194c7fb47ef1 100644 --- a/python/semantic_kernel/contents/utils/data_uri.py +++ b/python/semantic_kernel/contents/utils/data_uri.py @@ -135,7 +135,16 @@ def from_data_uri(cls: type[_T], data_uri: str | Url, default_mime_type: str = " if not data or "," not in data: raise ContentInitializationError("Invalid data uri format. The data is missing.") - pattern = "(((?P[a-zA-Z]+/[a-zA-Z-]+)(?P(;[a-zA-Z0-9]+=+[a-zA-Z0-9]+)*))?(;+(?P.*)))?(,(?P.*))" # noqa: E501 + # Parameters use the RFC 2045 token alphabet, which is broader than a + # media type's restricted-name alphabet. RFC 2397 allows parameters even + # when the default text/plain media type is omitted. + parameter_token = r"[a-zA-Z0-9!#$%&'*+.^_`|~-]+" + pattern = ( + r"(?P[a-zA-Z0-9!#$&^_.+-]+/[a-zA-Z0-9!#$&^_.+-]+)?" + rf"(?P(;{parameter_token}={parameter_token})*)" + r"(;+(?P[a-zA-Z0-9!#$&^_.+-]*))?" + r"(,(?P.*))" + ) match = re.match(pattern, data) if not match: raise ContentInitializationError("Invalid data uri format.") diff --git a/python/tests/unit/contents/test_data_uri.py b/python/tests/unit/contents/test_data_uri.py index fc98679a9765..b2812d236c84 100644 --- a/python/tests/unit/contents/test_data_uri.py +++ b/python/tests/unit/contents/test_data_uri.py @@ -66,6 +66,33 @@ "base64", id="with_params", ), + pytest.param( + "data:image/svg+xml;base64,PHN2Zy8+", + b"", + "PHN2Zy8+", + "image/svg+xml", + {}, + "base64", + id="mime_type_with_plus", + ), + pytest.param( + "data:text/plain;charset=utf-8;base64,SGVsbG8=", + b"Hello", + "SGVsbG8=", + "text/plain", + {"charset": "utf-8"}, + "base64", + id="parameter_before_data_format", + ), + pytest.param( + "data:application/vnd.ms-excel;base64,AAAA", + b"\x00\x00\x00", + "AAAA", + "application/vnd.ms-excel", + {}, + "base64", + id="vendor_mime_type", + ), ], ) def test_data_uri_from_data_uri_str( @@ -310,3 +337,20 @@ def test_array(): def test__data_str(data_bytes, data_str, data_array, data_format, expected_output): data_uri = DataUri(data_bytes=data_bytes, data_str=data_str, data_array=data_array, data_format=data_format) assert data_uri._data_str() == expected_output + + +@pytest.mark.parametrize("media_type", ["text/plain", ""]) +@pytest.mark.parametrize("encoding,payload", [("", "Hello"), (";base64", "SGVsbG8=")]) +def test_data_uri_parameter_without_required_media_type(media_type, encoding, payload): + result = DataUri.from_data_uri(f"data:{media_type};charset=utf-8{encoding},{payload}") + assert result.data_bytes == b"Hello" + assert result.mime_type == "text/plain" + assert result.parameters == {"charset": "utf-8"} + assert result.data_format == ("base64" if encoding else None) + + +@pytest.mark.parametrize("value", ["foo*bar", "foo'bar", "foo~bar", "foo%20bar", "foo%3Bbar"]) +def test_data_uri_parameter_uses_parameter_token_alphabet(value): + result = DataUri.from_data_uri(f"data:text/plain;name={value},Hello") + assert result.data_bytes == b"Hello" + assert result.parameters == {"name": value} diff --git a/python/tests/unit/contents/test_image_content.py b/python/tests/unit/contents/test_image_content.py index 7d84a1746271..aeec05f6f1dd 100644 --- a/python/tests/unit/contents/test_image_content.py +++ b/python/tests/unit/contents/test_image_content.py @@ -38,6 +38,12 @@ def test_create_data(): assert image.data == b"test_data" +def test_create_svg_data_uri(): + image = ImageContent(data_uri="data:image/svg+xml;base64,PHN2Zy8+") + assert image.mime_type == "image/svg+xml" + assert image.data == b"" + + def test_to_str_uri(): image = ImageContent(uri="http://test_uri") assert str(image) == "http://test_uri/"