Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/1356.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Refactored :class:`~icalendar.prop.binary.vBinary` to store raw ``bytes`` internally, ensuring lossless round-tripping of non-UTF-8 binary data, and updated :class:`~icalendar.prop.image.Image` to handle this change correctly. @uwezkhan
22 changes: 14 additions & 8 deletions src/icalendar/prop/binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,19 @@
from icalendar.compatibility import Self
from icalendar.error import JCalParsingError
from icalendar.parser import Parameters
from icalendar.parser_tools import to_unicode


class vBinary:
"""Binary property values are base 64 encoded."""
Comment thread
uwezkhan marked this conversation as resolved.
Outdated

default_value: ClassVar[str] = "BINARY"
params: Parameters
obj: str
obj: bytes

def __init__(self, obj: str | bytes, params: dict[str, str] | None = None) -> None:
self.obj = to_unicode(obj)
if isinstance(obj, str):
self.obj = obj.encode("utf-8")
else:
self.obj = obj
self.params = Parameters(encoding="BASE64", value="BINARY")
if params:
self.params.update(params)
Expand All @@ -27,7 +28,7 @@ def __repr__(self) -> str:
return f"vBinary({self.to_ical()})"

def to_ical(self) -> bytes:
return binascii.b2a_base64(self.obj.encode("utf-8"))[:-1]
return base64.b64encode(self.obj)

@staticmethod
def from_ical(ical: str | bytes) -> bytes:
Expand Down Expand Up @@ -57,12 +58,17 @@ def to_jcal(self, name: str) -> list:
if params.get("encoding") == "BASE64":
# BASE64 is the only allowed encoding
del params["encoding"]
return [name, params, self.VALUE.lower(), self.obj]
return [
name,
params,
self.VALUE.lower(),
base64.b64encode(self.obj).decode("ascii"),
]

@property
def ical_value(self) -> bytes:
"""The bytes value of the BINARY property."""
return self.from_ical(self.obj)
return self.obj

@classmethod
def from_jcal(cls, jcal_property: list) -> Self:
Expand All @@ -77,7 +83,7 @@ def from_jcal(cls, jcal_property: list) -> Self:
JCalParsingError.validate_property(jcal_property, cls)
JCalParsingError.validate_value_type(jcal_property[3], str, cls, 3)
return cls(
jcal_property[3],
cls.from_ical(jcal_property[3]),
params=Parameters.from_jcal_property(jcal_property),
)

Expand Down
8 changes: 6 additions & 2 deletions src/icalendar/prop/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,13 @@ def from_property_value(cls, value: vUri | vBinary | vText) -> Image:
if value_type == "URI" or isinstance(value, vUri):
params["uri"] = str(value)
elif isinstance(value, vBinary):
params["b64data"] = value.obj
params["b64data"] = base64.b64encode(value.obj).decode("ascii")
elif value_type == "BINARY":
params["b64data"] = str(value)
params["b64data"] = (
value
if isinstance(value, str)
else base64.b64encode(value).decode("ascii")
)
else:
raise TypeError(
f"The VALUE parameter must be URI or BINARY, not {value_type!r}."
Expand Down
17 changes: 5 additions & 12 deletions src/icalendar/tests/prop/test_vBinary.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""Test vBinary"""

import base64

import pytest

from icalendar import vBinary
Expand All @@ -23,8 +21,8 @@ def test_binary():


def test_param():
assert isinstance(vBinary("txt").params, Parameters)
Comment thread
uwezkhan marked this conversation as resolved.
assert vBinary("txt").params == {"VALUE": "BINARY", "ENCODING": "BASE64"}
assert isinstance(vBinary(b"txt").params, Parameters)
assert vBinary(b"txt").params == {"VALUE": "BINARY", "ENCODING": "BASE64"}


def test_long_data():
Expand Down Expand Up @@ -61,14 +59,9 @@ def test_from_ical_rejects_non_base64_characters(value):


def test_ical_value():
"""ical_value property returns the string value."""
magic_string = base64.b64encode(b"magic string")
Comment thread
uwezkhan marked this conversation as resolved.
assert vBinary(magic_string).ical_value == base64.b64decode(magic_string)


def test_ical_value_rejects_non_base64_characters():
with pytest.raises(ValueError, match=r"Not valid base 64 encoding\."):
vBinary("!!!!dGV4dA==@@@@").ical_value
"""ical_value property returns the binary value."""
raw_data = b"magic string"
assert vBinary(raw_data).ical_value == raw_data


def test_hash():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
)

JCAL_PAIRS = [
(["attach", {}, "binary", "SGVsbG8gV29ybGQh"], vBinary("SGVsbG8gV29ybGQh")),
(["attach", {}, "binary", "SGVsbG8gV29ybGQh"], vBinary(b"Hello World!")),
(["x-non-smoking", {}, "boolean", True], vBoolean(True)),
(["x-non-smoking", {}, "boolean", False], vBoolean(False)),
(
Expand Down
7 changes: 3 additions & 4 deletions src/icalendar/tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,7 @@ class DummyValue:

def test_create_with_vBinary():
"""Test creating an Image from a vBinary property."""
b64data = base64.b64encode(TRANSPARENT_PIXEL).decode("ascii")
vbin = vBinary(b64data, params={"FMTTYPE": "image/png"})
vbin = vBinary(TRANSPARENT_PIXEL, params={"FMTTYPE": "image/png"})
image = Image.from_property_value(vbin)
assert image.uri is None
assert image.data == TRANSPARENT_PIXEL
Expand All @@ -147,7 +146,7 @@ def test_create_with_vUri():


def test_create_image_with_vText_as_uri():
"""Test that creating an image with vText but VALUE URI or BINARY raises TypeError."""
"""Test that creating an image with vText and VALUE=URI works."""
img = Image.from_property_value(
vText("http://example.com/image.png", params={"VALUE": "URI"})
)
Expand All @@ -159,7 +158,7 @@ def test_create_image_with_vText_as_uri():


def test_create_image_with_vText_as_binary():
"""Test that creating an image with vText but VALUE URI or BINARY raises TypeError."""
"""Test that creating an image with vText and VALUE=BINARY works."""
b64data = base64.b64encode(TRANSPARENT_PIXEL).decode("ascii")
img = Image.from_property_value(vText(b64data, params={"VALUE": "BINARY"}))
assert img.uri is None
Expand Down