Repository navigation
fix quadratic time of the XML declaration regex in vXml #2037
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
debef18
9f55ce6
224e498
5b5e119
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Fixed the quadratic run time of :meth:`Component.to_xcal <icalendar.cal.component.Component.to_xcal>` and :attr:`vXml.element <icalendar.prop.xml.vXml.element>` for an ``XML`` property whose XML declaration contains a long run of whitespace. I used AI to assist me with this change. @shamsManna |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,8 @@ | |
|
|
||
| import base64 | ||
| import re | ||
| import sys | ||
| import time | ||
| import xml.etree.ElementTree as ET | ||
| from io import BytesIO | ||
| from xml.etree.ElementTree import Element, ParseError, fromstring, tostring | ||
|
|
@@ -40,6 +42,7 @@ | |
| ) | ||
| from icalendar.parser.xcal.adapter import ElementAdapter | ||
| from icalendar.parser.xcal.property import XCalPropertyParser | ||
| from icalendar.prop import xml as prop_xml | ||
|
|
||
| XML = '<a xmlns="http://example.com/ns">text</a>' | ||
| XML_BASE64 = base64.b64encode(XML.encode()).decode() | ||
|
|
@@ -269,6 +272,21 @@ def test_xml_property_to_xcal_and_back_is_equal(): | |
| assert str(copy.XML[1]) == '<ns0:x xmlns:ns0="http://example.com/b" ns0:a="1" />' | ||
|
|
||
|
|
||
| @pytest.fixture(params=["py310", "py311"]) | ||
| def declared_encoding(request, monkeypatch): | ||
| """Use the regex for the declared encoding of Python 3.10 and of Python 3.11+. | ||
|
|
||
| Python 3.10 has no possessive quantifiers, so it uses a simpler regex. | ||
| Both are tested where they can be compiled. | ||
| """ | ||
| if request.param == "py310": | ||
| monkeypatch.setattr( | ||
| prop_xml, "_DECLARED_ENCODING", prop_xml._DECLARED_ENCODING_PY310 | ||
| ) | ||
| elif sys.version_info < (3, 11): | ||
| pytest.skip("Possessive quantifiers need Python 3.11 or later.") | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "declaration", | ||
| [ | ||
|
|
@@ -277,16 +295,68 @@ def test_xml_property_to_xcal_and_back_is_equal(): | |
| '<?xml version="1.0" encoding="US-ASCII"?>', | ||
| "<?xml version='1.0' encoding='UTF-16' standalone='yes'?>", | ||
| chr(0xFEFF) + '<?xml version="1.0" encoding="ISO-8859-1"?>', | ||
| '<?xml version="1.0" \n\t\t\t\n encoding="ISO-8859-1"\n\t\t\t\n\n?>', | ||
| '<?xml version="1.0"\tencoding = "ISO-8859-1"\nstandalone="yes"?>', | ||
| ], | ||
| ) | ||
| def test_xml_property_to_xcal_ignores_the_declared_encoding(declaration): | ||
| def test_xml_property_to_xcal_ignores_the_declared_encoding( | ||
| declaration, declared_encoding | ||
| ): | ||
| """The text is already decoded, so the encoding of its declaration is ignored.""" | ||
| event = Event() | ||
| event.add("XML", f'{declaration}<a xmlns="urn:x">café €</a>') | ||
| assert event["XML"].element.text == "café €" | ||
| assert Event.from_xcal(event.to_xcal())[0]["XML"].element.text == "café €" | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("whitespace", [" ", "\t", "\n"]) | ||
| def test_xml_property_to_xcal_with_long_whitespace_in_the_declaration( | ||
| whitespace, declared_encoding | ||
| ): | ||
| """A long run of whitespace in the XML declaration is parsed in linear time. | ||
|
|
||
| The regex that removes the declared encoding used to retry the whitespace | ||
| before ``encoding`` from every position, so this took minutes. | ||
| The bound is generous enough to never flake on slow CI. | ||
| """ | ||
| event = Event() | ||
| event.add( | ||
| "XML", | ||
| '<?xml version="1.0"' + whitespace * 200_000 + '?><a xmlns="urn:x">text</a>', | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How does test the presence of whitespace relative to
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it didn't, that test has no the run before |
||
| ) | ||
| start = time.perf_counter() | ||
| xcal = event.to_xcal() | ||
| assert time.perf_counter() - start < 5 | ||
| assert Event.from_xcal(xcal)[0]["XML"].element.text == "text" | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("whitespace", [" ", "\t", "\n"]) | ||
| def test_xml_property_to_xcal_with_long_whitespace_around_the_encoding( | ||
| whitespace, declared_encoding | ||
| ): | ||
| """Long runs of whitespace around the attributes are parsed in linear time. | ||
|
|
||
| The run before ``version`` is not followed by ``encoding``, and the run | ||
| before ``encoding`` is. The declared encoding is still removed, | ||
| so the text is not decoded again. | ||
| """ | ||
| event = Event() | ||
| event.add( | ||
| "XML", | ||
| "<?xml" | ||
| + whitespace * 200_000 | ||
| + 'version="1.0"' | ||
| + whitespace * 200_000 | ||
| + 'encoding="ISO-8859-1"' | ||
| + whitespace * 200_000 | ||
| + '?><a xmlns="urn:x">café €</a>', | ||
| ) | ||
| start = time.perf_counter() | ||
| xcal = event.to_xcal() | ||
| assert time.perf_counter() - start < 5 | ||
| assert Event.from_xcal(xcal)[0]["XML"].element.text == "café €" | ||
|
|
||
|
|
||
| # icalendar objects -> iCal/jCal | ||
|
|
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think double-quotes need to be escaped. Please check against this test, and let me know what you think.
https://regex101.com/r/mXQ2XG/1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
they don't need escaping in the source. both patterns are in
r"""..."""triple-quoted raw strings, so a single"inside is just a literal quote and doesn't end the string. regex101 asks for\"because your link uses"as the delimiter, which corresponds tor"...".i ran your test string through the compiled pattern on 3.11+ and it strips the encoding together with all the whitespace before it, giving
<?xml version="1.0"\n\t\t\t\n\n?><a xmlns="urn:x">text</a>. a declaration like it is now in the tests.