Skip to content
Open
Show file tree
Hide file tree
Changes from all 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/1888.bugfix.1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed :meth:`vCalAddress.to_xcal() <icalendar.prop.cal_address.vCalAddress.to_xcal>` from writing characters that are disallowed in XML. The value now goes through the same filter as the text and URI types, and that filter also removes ``U+FFFE`` and ``U+FFFF``. I used AI (Claude Opus 5.5, via Claude Code) to find this bug, write the fix, and write its tests; I reviewed and validated the output. @alhudz
18 changes: 16 additions & 2 deletions src/icalendar/parser/xcal/string.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,21 @@ def xsd_float_to_string(f: float) -> str:
return "-INF"


_VALID_XML_CHARACTERS = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f]")
_INVALID_XML_CHARACTERS = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\ufffe\uffff]")
"""Match the characters that XML 1.0 disallows in a document.

`XML 1.0, section 2.2, Characters <https://www.w3.org/TR/REC-xml/#charsets>`_,
or Char, allows only ``#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] |
[#x10000-#x10FFFF]``. This pattern matches the characters outside of it that a
Python string can contain, apart from surrogates:

- the ``C0`` control characters ``#x0`` to ``#x1F``, except tab, line feed and
carriage return, and
- the noncharacters ``#xFFFE`` and ``#xFFFF``.

Lone surrogates ``#xD800`` to ``#xDFFF`` are also outside Char, but are not
matched here. They can't be encoded as UTF-8, and ``to_ical()`` rejects them.
"""


def _to_valid_xml_string(string: str) -> str:
Expand All @@ -49,7 +63,7 @@ def _to_valid_xml_string(string: str) -> str:
`~icalendar.prop.text._strip_unsafe_text_chars`,
`Issue 1888 <https://github.com/collective/icalendar/issues/1888>`_,
"""
return _VALID_XML_CHARACTERS.sub("", string)
return _INVALID_XML_CHARACTERS.sub("", string)


__all__ = ["_to_valid_xml_string", "string_to_xsd_float", "xsd_float_to_string"]
3 changes: 2 additions & 1 deletion src/icalendar/prop/cal_address.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from icalendar.error import JCalParsingError
from icalendar.parser import Parameters
from icalendar.parser.xcal.base import XCalParser
from icalendar.parser.xcal.string import _to_valid_xml_string
from icalendar.parser.xcal.wrapper import from_xcal_wrapper
from icalendar.parser_tools import DEFAULT_ENCODING, to_unicode

Expand Down Expand Up @@ -317,7 +318,7 @@ def to_xcal(self, element: Element) -> None:
"""The xCal representation of this property according to :rfc:`6321`."""
self.params.to_xcal(element)
element = SubElement(element, "cal-address")
element.text = self.ical_value
element.text = _to_valid_xml_string(self.ical_value)


__all__ = ["vCalAddress"]
9 changes: 7 additions & 2 deletions src/icalendar/tests/rfc_6321_xcal/test_text_escape.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import pytest

from icalendar.config import parse_xml
from icalendar.prop.cal_address import vCalAddress
from icalendar.prop.text import vText
from icalendar.prop.uid import vUid
from icalendar.prop.unknown import vUnknown
Expand All @@ -39,8 +40,9 @@ def round_trip(xml: Element) -> Element:

# #x9 | #xA | #xD | [#x20-#xD7FF]
VALID_CHARACTERS_BELOW_SPACE = "\x09\x0a\x0d"
INVALID_CHARACTERS = "".join(
chr(i) for i in range(0x20) if chr(i) not in VALID_CHARACTERS_BELOW_SPACE
INVALID_CHARACTERS = (
"".join(chr(i) for i in range(0x20) if chr(i) not in VALID_CHARACTERS_BELOW_SPACE)
+ "\ufffe\uffff"
Comment thread
niccokunzmann marked this conversation as resolved.
Resolved
)

VALID_CHARACTERS = "".join(
Expand Down Expand Up @@ -107,6 +109,9 @@ def test_xml_round_trips(text):
assert round_tripped.text == newline_escape(text)


@pytest.mark.parametrize(
"xcal_text_type", [vText, vUnknown, vUid, vUri, vXmlReference, vCalAddress]
)
@pytest.mark.parametrize("text", INVALID_CHARACTERS)
@pytest.mark.parametrize("wrap", ["", "asdasdasdasd"])
def test_invalid_xml_characters_are_replaced(xcal_text_type, text, wrap):
Expand Down
Loading