1"""BINARY values from :rfc:`5545`."""
2
3import base64
4import binascii
5from typing import ClassVar
6
7from icalendar.compatibility import Self, deprecate_for_version_8
8from icalendar.error import JCalParsingError
9from icalendar.parser import Parameters
10from icalendar.parser_tools import to_unicode
11
12
13class vBinary:
14 """Binary property values are Base64 encoded."""
15
16 default_value: ClassVar[str] = "BINARY"
17 params: Parameters
18 bytes: bytes
19 """The raw binary value of the BINARY property.
20
21 This is the authoritative storage and round-trips losslessly, including
22 for non-UTF-8 data. Use this instead of the deprecated :attr:`obj`.
23 """
24
25 def __init__(self, obj: str | bytes, params: dict[str, str] | None = None) -> None:
26 if isinstance(obj, str):
27 self.bytes = obj.encode("utf-8")
28 else:
29 self.bytes = obj
30 self.params = Parameters(encoding="BASE64", value="BINARY")
31 if params:
32 self.params.update(params)
33
34 def __repr__(self) -> str:
35 return f"vBinary({self.to_ical()})"
36
37 def to_ical(self) -> bytes:
38 return base64.b64encode(self.bytes)
39
40 @staticmethod
41 def from_ical(ical: str | bytes) -> bytes:
42 try:
43 return base64.b64decode(ical, validate=True)
44 except (binascii.Error, ValueError) as e:
45 raise ValueError("Not valid base 64 encoding.") from e
46
47 @property
48 def base64data(self) -> str:
49 """The Base64-encoded string view of this value.
50
51 This is the same string that :meth:`to_ical` produces, exposed as
52 a plain :class:`str` instead of :class:`bytes` so you don't need to
53 call :func:`base64.b64encode` yourself. See :issue:`1550`.
54
55 Returns:
56 The Base64-encoded representation of the stored value.
57 """
58 return self.to_ical().decode("ascii")
59
60 @base64data.setter
61 def base64data(self, value: str) -> None:
62 """Set this value from a Base64-encoded string.
63
64 The decoded raw bytes are stored in :attr:`bytes`, so non-UTF-8
65 payloads round-trip losslessly.
66
67 Parameters:
68 value: A Base64-encoded string.
69
70 Raises:
71 ValueError: If ``value`` isn't valid Base64.
72 """
73 self.bytes = self.from_ical(value)
74
75 @property
76 @deprecate_for_version_8
77 def obj(self) -> str:
78 """Deprecated string view of the value.
79
80 .. deprecated:: 7.1.3
81 Use :attr:`bytes` for the raw binary value. ``obj`` decodes
82 :attr:`bytes` as text and is lossy for non-UTF-8 data. It will
83 be removed in icalendar 8.
84 """
85 return to_unicode(self.bytes)
86
87 @obj.setter
88 @deprecate_for_version_8
89 def obj(self, value: str | bytes) -> None:
90 self.bytes = value.encode("utf-8") if isinstance(value, str) else value
91
92 def __eq__(self, other: object) -> bool:
93 """self == other"""
94 return isinstance(other, vBinary) and self.bytes == other.bytes
95
96 def __hash__(self) -> int:
97 """Hash of the vBinary object."""
98 return hash(self.bytes)
99
100 @classmethod
101 def examples(cls) -> list[Self]:
102 """Examples of vBinary."""
103 return [cls("VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4")]
104
105 from icalendar.param import VALUE
106
107 def to_jcal(self, name: str) -> list:
108 """The jCal representation of this property according to :rfc:`7265`."""
109 params = self.params.to_jcal()
110 if params.get("encoding") == "BASE64":
111 # BASE64 is the only allowed encoding
112 del params["encoding"]
113 return [name, params, self.VALUE.lower(), self.base64data]
114
115 @property
116 def ical_value(self) -> bytes:
117 """The raw ``bytes`` value of the BINARY property.
118
119 .. versionadded:: 7.1.0
120
121 .. versionchanged:: 7.1.3
122 Returns the raw stored bytes. Previously the stored value was
123 Base64-decoded, which raised :class:`ValueError` for non-Base64
124 input. See :pr:`1356`.
125 """
126 return self.bytes
127
128 @classmethod
129 def from_jcal(cls, jcal_property: list) -> Self:
130 """Parse jCal from :rfc:`7265` to a vBinary.
131
132 Parameters:
133 jcal_property: The jCal property to parse.
134
135 Raises:
136 ~error.JCalParsingError: If the provided jCal is invalid.
137 """
138 JCalParsingError.validate_property(jcal_property, cls)
139 JCalParsingError.validate_value_type(jcal_property[3], str, cls, 3)
140 return cls(
141 cls.from_ical(jcal_property[3]),
142 params=Parameters.from_jcal_property(jcal_property),
143 )
144
145
146__all__ = ["vBinary"]