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