diff --git a/Lib/email/header.py b/Lib/email/header.py index ea1f84ea964ca60..9937b6d4f3a0cf1 100644 --- a/Lib/email/header.py +++ b/Lib/email/header.py @@ -546,24 +546,31 @@ class _Accumulator(list): def __init__(self, initial_size=0): self._initial_size = initial_size + # The value of len(): _initial_size plus the length of all the + # parts, kept up to date by push(), pop(), pop_from() and reset(). + # Summing the parts on every call makes folding quadratic. + self._len = initial_size super().__init__() def push(self, fws, string): self.append((fws, string)) + self._len += len(fws) + len(string) def pop_from(self, i=0): popped = self[i:] self[i:] = [] + self._len -= sum(len(fws) + len(part) for fws, part in popped) return popped def pop(self): if self.part_count()==0: return ('', '') - return super().pop() + fws, part = super().pop() + self._len -= len(fws) + len(part) + return fws, part def __len__(self): - return sum((len(fws)+len(part) for fws, part in self), - self._initial_size) + return self._len def __str__(self): return EMPTYSTRING.join((EMPTYSTRING.join((fws, part)) @@ -574,6 +581,7 @@ def reset(self, startval=None): startval = [] self[:] = startval self._initial_size = 0 + self._len = sum(len(fws) + len(part) for fws, part in self) def is_onlyws(self): return self._initial_size==0 and (not self or str(self).isspace()) diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py index 2a5794a5b66fc5b..a5954a269fa6409 100644 --- a/Lib/test/test_email/test_email.py +++ b/Lib/test/test_email/test_email.py @@ -1290,6 +1290,35 @@ def test_no_split_long_header(self): h = Header('x' * 80) eq(h.encode(), 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') + def test_unfolded_long_header(self): + eq = self.ndiffAssertEqual + value = ' '.join(f'' for i in range(2000)) + h = Header(value, header_name='References') + eq(h.encode(maxlinelen=0), value) + msg = Message() + msg['References'] = value + eq(msg.as_string(), f'References: {value}\n\n') + + def test_accumulator_length(self): + from email.header import _Accumulator + acc = _Accumulator(10) + self.assertEqual(len(acc), 10) + acc.push(' ', 'abc') + acc.push('\t', 'de') + acc.push('', 'f') + self.assertEqual(len(acc), 10 + 4 + 3 + 1) + self.assertEqual(acc.pop(), ('', 'f')) + self.assertEqual(len(acc), 10 + 4 + 3) + self.assertEqual(acc.pop_from(1), [('\t', 'de')]) + self.assertEqual(len(acc), 10 + 4) + acc.reset([(' ', 'gh'), ('', 'ijk')]) + self.assertEqual(len(acc), 6) + self.assertEqual(acc.part_count(), 2) + acc.reset() + self.assertEqual(len(acc), 0) + self.assertEqual(acc.pop(), ('', '')) + self.assertEqual(len(acc), 0) + def test_splitting_multiple_long_lines(self): eq = self.ndiffAssertEqual hstr = """\ diff --git a/Misc/NEWS.d/next/Library/2026-11-04-00-40-27.gh-issue-161284.o6JvHQ.rst b/Misc/NEWS.d/next/Library/2026-11-04-00-40-27.gh-issue-161284.o6JvHQ.rst new file mode 100644 index 000000000000000..f0732224da0a6e4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-11-04-00-40-27.gh-issue-161284.o6JvHQ.rst @@ -0,0 +1,2 @@ +Fix quadratic time complexity in :meth:`email.header.Header.encode` when +folding long headers. This also affects :meth:`email.message.Message.as_string`.