mirror of
https://github.com/python/cpython.git
synced 2026-10-28 10:25:05 +08:00
gh-152674: Avoid quadratic behavior in xml.etree.ElementPath index predicates (GH-152676)
Co-authored-by: Alexis <alexis.challande@trailofbits.com> Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
This commit is contained in:
3 files changed
+49
-5
No files matched your search
@@ -3477,6 +3477,37 @@ class ElementFindTest(unittest.TestCase):
|
||||
self.assertRaisesRegex(SyntaxError, 'XPath', e.find, './tag[last()-0]')
|
||||
self.assertRaisesRegex(SyntaxError, 'XPath', e.find, './tag[last()+1]')
|
||||
|
||||
def test_find_xpath_index_no_quadratic_complexity(self):
|
||||
class CountingElement(ET.Element):
|
||||
findall_calls = 0
|
||||
def findall(self, *args, **kwargs):
|
||||
type(self).findall_calls += 1
|
||||
return super().findall(*args, **kwargs)
|
||||
|
||||
def work(n, pattern):
|
||||
root = CountingElement("root")
|
||||
for _ in range(n):
|
||||
ET.SubElement(root, "a")
|
||||
CountingElement.findall_calls = 0
|
||||
root.findall(pattern)
|
||||
return CountingElement.findall_calls
|
||||
|
||||
for pattern in [".//a[1]", ".//a[last()]"]:
|
||||
w1 = work(1024, pattern)
|
||||
w2 = work(2048, pattern)
|
||||
w3 = work(4096, pattern)
|
||||
|
||||
self.assertGreater(w1, 0)
|
||||
r1 = w2 / w1
|
||||
r2 = w3 / w2
|
||||
# Doubling N must not ~double the parent.findall calls.
|
||||
# Linear-in-N call counts indicate the cache is missing.
|
||||
self.assertLess(
|
||||
max(r1, r2), 1.5,
|
||||
msg=f"Possible quadratic behavior on {pattern!r}: "
|
||||
f"calls={w1, w2, w3} ratios={r1, r2}",
|
||||
)
|
||||
|
||||
def test_findall(self):
|
||||
e = ET.XML(SAMPLE_XML)
|
||||
e[2] = ET.XML(SAMPLE_SECTION)
|
||||
|
||||
@@ -324,15 +324,22 @@ def prepare_predicate(next, token):
|
||||
index = -1
|
||||
def select(context, result):
|
||||
parent_map = get_parent_map(context)
|
||||
cache = {}
|
||||
for elem in result:
|
||||
try:
|
||||
parent = parent_map[elem]
|
||||
except KeyError:
|
||||
continue
|
||||
key = (parent, elem.tag)
|
||||
if key not in cache:
|
||||
# FIXME: what if the selector is "*" ?
|
||||
elems = list(parent.findall(elem.tag))
|
||||
if elems[index] is elem:
|
||||
yield elem
|
||||
except (IndexError, KeyError):
|
||||
pass
|
||||
elems = parent.findall(elem.tag)
|
||||
try:
|
||||
cache[key] = elems[index]
|
||||
except IndexError:
|
||||
cache[key] = None
|
||||
if cache[key] is elem:
|
||||
yield elem
|
||||
return select
|
||||
raise SyntaxError("invalid predicate")
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
The :class:`xml.etree.ElementTree.Element` methods
|
||||
:meth:`~xml.etree.ElementTree.Element.findall`,
|
||||
:meth:`~xml.etree.ElementTree.Element.iterfind` and
|
||||
:meth:`~xml.etree.ElementTree.Element.find` avoid quadratic behavior when
|
||||
using XPath index predicates (``[1]``, ``[last()]``, ``[last()-N]``) on XML
|
||||
documents with many same-tag siblings.
|
||||
Reference in new issue
Block a user