Compare commits
No commits in common. "rawhide" and "f38" have entirely different histories.
17 changed files with 645 additions and 401 deletions
|
|
@ -1,10 +1,9 @@
|
|||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: David Malcolm <dmalcolm@redhat.com>
|
||||
Date: Wed, 13 Jan 2010 21:25:18 +0000
|
||||
Subject: 00001: Fixup distutils/unixccompiler.py to remove standard library
|
||||
path from rpath
|
||||
Subject: [PATCH] 00001: Fixup distutils/unixccompiler.py to remove standard
|
||||
library path from rpath Was Patch0 in ivazquez' python3000 specfile
|
||||
|
||||
Was Patch0 in ivazquez' python3000 specfile
|
||||
---
|
||||
Lib/distutils/unixccompiler.py | 9 +++++++++
|
||||
1 file changed, 9 insertions(+)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: David Malcolm <dmalcolm@redhat.com>
|
||||
Date: Mon, 18 Jan 2010 17:59:07 +0000
|
||||
Subject: 00111: Don't try to build a libpythonMAJOR.MINOR.a
|
||||
Subject: [PATCH] 00111: Don't try to build a libpythonMAJOR.MINOR.a
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= <miro@hroncok.cz>
|
||||
Date: Wed, 15 Aug 2018 15:36:29 +0200
|
||||
Subject: 00189: Instead of bundled wheels, use our RPM packaged wheels
|
||||
Subject: [PATCH] 00189: Instead of bundled wheels, use our RPM packaged wheels
|
||||
|
||||
We keep them in /usr/share/python-wheels
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ We might eventually pursuit upstream support, but it's low prio
|
|||
1 file changed, 26 insertions(+), 11 deletions(-)
|
||||
|
||||
diff --git a/Lib/ensurepip/__init__.py b/Lib/ensurepip/__init__.py
|
||||
index d61bb089e3..77d7ec5a65 100644
|
||||
index 07065c3cb7..77d7ec5a65 100644
|
||||
--- a/Lib/ensurepip/__init__.py
|
||||
+++ b/Lib/ensurepip/__init__.py
|
||||
@@ -1,3 +1,5 @@
|
||||
|
|
@ -30,7 +30,7 @@ index d61bb089e3..77d7ec5a65 100644
|
|||
|
||||
|
||||
__all__ = ["version", "bootstrap"]
|
||||
-_SETUPTOOLS_VERSION = "79.0.1"
|
||||
-_SETUPTOOLS_VERSION = "58.1.0"
|
||||
-_PIP_VERSION = "23.0.1"
|
||||
+
|
||||
+_WHEEL_DIR = "/usr/share/python-wheels/"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Michal Cyprian <m.cyprian@gmail.com>
|
||||
Date: Mon, 26 Jun 2017 16:32:56 +0200
|
||||
Subject: 00251: Change user install location
|
||||
Subject: [PATCH] 00251: Change user install location
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Lumir Balhar <lbalhar@redhat.com>
|
||||
Date: Tue, 4 Aug 2020 12:04:03 +0200
|
||||
Subject: 00353: Original names for architectures with different names
|
||||
Subject: [PATCH] 00353: Original names for architectures with different names
|
||||
downstream
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Hrn=C4=8Diar?= <thrnciar@redhat.com>
|
||||
Date: Fri, 19 Nov 2021 13:37:16 +0100
|
||||
Subject: 00371: Revert "bpo-1596321: Fix threading._shutdown() for the main
|
||||
thread (GH-28549) (GH-28589)"
|
||||
Subject: [PATCH] 00371: Revert "bpo-1596321: Fix threading._shutdown() for the
|
||||
main thread (GH-28549) (GH-28589)"
|
||||
|
||||
This reverts commit 94d19f606fa18a1c4d2faca1caf2f470a8ce6d46. It
|
||||
introduced regression causing FreeIPA's tests to fail.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: "Erlend E. Aasland" <erlend.aasland@protonmail.com>
|
||||
Date: Sun, 6 Nov 2022 22:39:34 +0100
|
||||
Subject: 00407: gh-99086: Fix implicit int compiler warning in configure check
|
||||
for PTHREAD_SCOPE_SYSTEM
|
||||
Subject: [PATCH] 00407: gh-99086: Fix implicit int compiler warning in
|
||||
configure check for PTHREAD_SCOPE_SYSTEM
|
||||
|
||||
Co-authored-by: Sam James <sam@cmpct.info>
|
||||
---
|
||||
|
|
|
|||
|
|
@ -0,0 +1,500 @@
|
|||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Victor Stinner <vstinner@python.org>
|
||||
Date: Fri, 15 Dec 2023 16:10:40 +0100
|
||||
Subject: [PATCH] 00415: [CVE-2023-27043] gh-102988: Reject malformed addresses
|
||||
in email.parseaddr() (#111116)
|
||||
|
||||
Detect email address parsing errors and return empty tuple to
|
||||
indicate the parsing error (old API). Add an optional 'strict'
|
||||
parameter to getaddresses() and parseaddr() functions. Patch by
|
||||
Thomas Dwyer.
|
||||
|
||||
Co-Authored-By: Thomas Dwyer <github@tomd.tel>
|
||||
---
|
||||
Doc/library/email.utils.rst | 19 +-
|
||||
Lib/email/utils.py | 151 ++++++++++++-
|
||||
Lib/test/test_email/test_email.py | 204 +++++++++++++++++-
|
||||
...-10-20-15-28-08.gh-issue-102988.dStNO7.rst | 8 +
|
||||
4 files changed, 361 insertions(+), 21 deletions(-)
|
||||
create mode 100644 Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst
|
||||
|
||||
diff --git a/Doc/library/email.utils.rst b/Doc/library/email.utils.rst
|
||||
index 4d0e920eb0..104229e9e5 100644
|
||||
--- a/Doc/library/email.utils.rst
|
||||
+++ b/Doc/library/email.utils.rst
|
||||
@@ -60,13 +60,18 @@ of the new API.
|
||||
begins with angle brackets, they are stripped off.
|
||||
|
||||
|
||||
-.. function:: parseaddr(address)
|
||||
+.. function:: parseaddr(address, *, strict=True)
|
||||
|
||||
Parse address -- which should be the value of some address-containing field such
|
||||
as :mailheader:`To` or :mailheader:`Cc` -- into its constituent *realname* and
|
||||
*email address* parts. Returns a tuple of that information, unless the parse
|
||||
fails, in which case a 2-tuple of ``('', '')`` is returned.
|
||||
|
||||
+ If *strict* is true, use a strict parser which rejects malformed inputs.
|
||||
+
|
||||
+ .. versionchanged:: 3.13
|
||||
+ Add *strict* optional parameter and reject malformed inputs by default.
|
||||
+
|
||||
|
||||
.. function:: formataddr(pair, charset='utf-8')
|
||||
|
||||
@@ -84,12 +89,15 @@ of the new API.
|
||||
Added the *charset* option.
|
||||
|
||||
|
||||
-.. function:: getaddresses(fieldvalues)
|
||||
+.. function:: getaddresses(fieldvalues, *, strict=True)
|
||||
|
||||
This method returns a list of 2-tuples of the form returned by ``parseaddr()``.
|
||||
*fieldvalues* is a sequence of header field values as might be returned by
|
||||
- :meth:`Message.get_all <email.message.Message.get_all>`. Here's a simple
|
||||
- example that gets all the recipients of a message::
|
||||
+ :meth:`Message.get_all <email.message.Message.get_all>`.
|
||||
+
|
||||
+ If *strict* is true, use a strict parser which rejects malformed inputs.
|
||||
+
|
||||
+ Here's a simple example that gets all the recipients of a message::
|
||||
|
||||
from email.utils import getaddresses
|
||||
|
||||
@@ -99,6 +107,9 @@ of the new API.
|
||||
resent_ccs = msg.get_all('resent-cc', [])
|
||||
all_recipients = getaddresses(tos + ccs + resent_tos + resent_ccs)
|
||||
|
||||
+ .. versionchanged:: 3.13
|
||||
+ Add *strict* optional parameter and reject malformed inputs by default.
|
||||
+
|
||||
|
||||
.. function:: parsedate(date)
|
||||
|
||||
diff --git a/Lib/email/utils.py b/Lib/email/utils.py
|
||||
index 48d30160aa..7ca7a7c886 100644
|
||||
--- a/Lib/email/utils.py
|
||||
+++ b/Lib/email/utils.py
|
||||
@@ -48,6 +48,7 @@ TICK = "'"
|
||||
specialsre = re.compile(r'[][\\()<>@,:;".]')
|
||||
escapesre = re.compile(r'[\\"]')
|
||||
|
||||
+
|
||||
def _has_surrogates(s):
|
||||
"""Return True if s contains surrogate-escaped binary data."""
|
||||
# This check is based on the fact that unless there are surrogates, utf8
|
||||
@@ -106,12 +107,127 @@ def formataddr(pair, charset='utf-8'):
|
||||
return address
|
||||
|
||||
|
||||
+def _iter_escaped_chars(addr):
|
||||
+ pos = 0
|
||||
+ escape = False
|
||||
+ for pos, ch in enumerate(addr):
|
||||
+ if escape:
|
||||
+ yield (pos, '\\' + ch)
|
||||
+ escape = False
|
||||
+ elif ch == '\\':
|
||||
+ escape = True
|
||||
+ else:
|
||||
+ yield (pos, ch)
|
||||
+ if escape:
|
||||
+ yield (pos, '\\')
|
||||
|
||||
-def getaddresses(fieldvalues):
|
||||
- """Return a list of (REALNAME, EMAIL) for each fieldvalue."""
|
||||
- all = COMMASPACE.join(str(v) for v in fieldvalues)
|
||||
- a = _AddressList(all)
|
||||
- return a.addresslist
|
||||
+
|
||||
+def _strip_quoted_realnames(addr):
|
||||
+ """Strip real names between quotes."""
|
||||
+ if '"' not in addr:
|
||||
+ # Fast path
|
||||
+ return addr
|
||||
+
|
||||
+ start = 0
|
||||
+ open_pos = None
|
||||
+ result = []
|
||||
+ for pos, ch in _iter_escaped_chars(addr):
|
||||
+ if ch == '"':
|
||||
+ if open_pos is None:
|
||||
+ open_pos = pos
|
||||
+ else:
|
||||
+ if start != open_pos:
|
||||
+ result.append(addr[start:open_pos])
|
||||
+ start = pos + 1
|
||||
+ open_pos = None
|
||||
+
|
||||
+ if start < len(addr):
|
||||
+ result.append(addr[start:])
|
||||
+
|
||||
+ return ''.join(result)
|
||||
+
|
||||
+
|
||||
+supports_strict_parsing = True
|
||||
+
|
||||
+def getaddresses(fieldvalues, *, strict=True):
|
||||
+ """Return a list of (REALNAME, EMAIL) or ('','') for each fieldvalue.
|
||||
+
|
||||
+ When parsing fails for a fieldvalue, a 2-tuple of ('', '') is returned in
|
||||
+ its place.
|
||||
+
|
||||
+ If strict is true, use a strict parser which rejects malformed inputs.
|
||||
+ """
|
||||
+
|
||||
+ # If strict is true, if the resulting list of parsed addresses is greater
|
||||
+ # than the number of fieldvalues in the input list, a parsing error has
|
||||
+ # occurred and consequently a list containing a single empty 2-tuple [('',
|
||||
+ # '')] is returned in its place. This is done to avoid invalid output.
|
||||
+ #
|
||||
+ # Malformed input: getaddresses(['alice@example.com <bob@example.com>'])
|
||||
+ # Invalid output: [('', 'alice@example.com'), ('', 'bob@example.com')]
|
||||
+ # Safe output: [('', '')]
|
||||
+
|
||||
+ if not strict:
|
||||
+ all = COMMASPACE.join(str(v) for v in fieldvalues)
|
||||
+ a = _AddressList(all)
|
||||
+ return a.addresslist
|
||||
+
|
||||
+ fieldvalues = [str(v) for v in fieldvalues]
|
||||
+ fieldvalues = _pre_parse_validation(fieldvalues)
|
||||
+ addr = COMMASPACE.join(fieldvalues)
|
||||
+ a = _AddressList(addr)
|
||||
+ result = _post_parse_validation(a.addresslist)
|
||||
+
|
||||
+ # Treat output as invalid if the number of addresses is not equal to the
|
||||
+ # expected number of addresses.
|
||||
+ n = 0
|
||||
+ for v in fieldvalues:
|
||||
+ # When a comma is used in the Real Name part it is not a deliminator.
|
||||
+ # So strip those out before counting the commas.
|
||||
+ v = _strip_quoted_realnames(v)
|
||||
+ # Expected number of addresses: 1 + number of commas
|
||||
+ n += 1 + v.count(',')
|
||||
+ if len(result) != n:
|
||||
+ return [('', '')]
|
||||
+
|
||||
+ return result
|
||||
+
|
||||
+
|
||||
+def _check_parenthesis(addr):
|
||||
+ # Ignore parenthesis in quoted real names.
|
||||
+ addr = _strip_quoted_realnames(addr)
|
||||
+
|
||||
+ opens = 0
|
||||
+ for pos, ch in _iter_escaped_chars(addr):
|
||||
+ if ch == '(':
|
||||
+ opens += 1
|
||||
+ elif ch == ')':
|
||||
+ opens -= 1
|
||||
+ if opens < 0:
|
||||
+ return False
|
||||
+ return (opens == 0)
|
||||
+
|
||||
+
|
||||
+def _pre_parse_validation(email_header_fields):
|
||||
+ accepted_values = []
|
||||
+ for v in email_header_fields:
|
||||
+ if not _check_parenthesis(v):
|
||||
+ v = "('', '')"
|
||||
+ accepted_values.append(v)
|
||||
+
|
||||
+ return accepted_values
|
||||
+
|
||||
+
|
||||
+def _post_parse_validation(parsed_email_header_tuples):
|
||||
+ accepted_values = []
|
||||
+ # The parser would have parsed a correctly formatted domain-literal
|
||||
+ # The existence of an [ after parsing indicates a parsing failure
|
||||
+ for v in parsed_email_header_tuples:
|
||||
+ if '[' in v[1]:
|
||||
+ v = ('', '')
|
||||
+ accepted_values.append(v)
|
||||
+
|
||||
+ return accepted_values
|
||||
|
||||
|
||||
def _format_timetuple_and_zone(timetuple, zone):
|
||||
@@ -202,16 +318,33 @@ def parsedate_to_datetime(data):
|
||||
tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
|
||||
|
||||
|
||||
-def parseaddr(addr):
|
||||
+def parseaddr(addr, *, strict=True):
|
||||
"""
|
||||
Parse addr into its constituent realname and email address parts.
|
||||
|
||||
Return a tuple of realname and email address, unless the parse fails, in
|
||||
which case return a 2-tuple of ('', '').
|
||||
+
|
||||
+ If strict is True, use a strict parser which rejects malformed inputs.
|
||||
"""
|
||||
- addrs = _AddressList(addr).addresslist
|
||||
- if not addrs:
|
||||
- return '', ''
|
||||
+ if not strict:
|
||||
+ addrs = _AddressList(addr).addresslist
|
||||
+ if not addrs:
|
||||
+ return ('', '')
|
||||
+ return addrs[0]
|
||||
+
|
||||
+ if isinstance(addr, list):
|
||||
+ addr = addr[0]
|
||||
+
|
||||
+ if not isinstance(addr, str):
|
||||
+ return ('', '')
|
||||
+
|
||||
+ addr = _pre_parse_validation([addr])[0]
|
||||
+ addrs = _post_parse_validation(_AddressList(addr).addresslist)
|
||||
+
|
||||
+ if not addrs or len(addrs) > 1:
|
||||
+ return ('', '')
|
||||
+
|
||||
return addrs[0]
|
||||
|
||||
|
||||
diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py
|
||||
index 761ea90b78..0c689643de 100644
|
||||
--- a/Lib/test/test_email/test_email.py
|
||||
+++ b/Lib/test/test_email/test_email.py
|
||||
@@ -16,6 +16,7 @@ from unittest.mock import patch
|
||||
|
||||
import email
|
||||
import email.policy
|
||||
+import email.utils
|
||||
|
||||
from email.charset import Charset
|
||||
from email.header import Header, decode_header, make_header
|
||||
@@ -3263,15 +3264,154 @@ Foo
|
||||
[('Al Person', 'aperson@dom.ain'),
|
||||
('Bud Person', 'bperson@dom.ain')])
|
||||
|
||||
+ def test_getaddresses_comma_in_name(self):
|
||||
+ """GH-106669 regression test."""
|
||||
+ self.assertEqual(
|
||||
+ utils.getaddresses(
|
||||
+ [
|
||||
+ '"Bud, Person" <bperson@dom.ain>',
|
||||
+ 'aperson@dom.ain (Al Person)',
|
||||
+ '"Mariusz Felisiak" <to@example.com>',
|
||||
+ ]
|
||||
+ ),
|
||||
+ [
|
||||
+ ('Bud, Person', 'bperson@dom.ain'),
|
||||
+ ('Al Person', 'aperson@dom.ain'),
|
||||
+ ('Mariusz Felisiak', 'to@example.com'),
|
||||
+ ],
|
||||
+ )
|
||||
+
|
||||
+ def test_parsing_errors(self):
|
||||
+ """Test for parsing errors from CVE-2023-27043 and CVE-2019-16056"""
|
||||
+ alice = 'alice@example.org'
|
||||
+ bob = 'bob@example.com'
|
||||
+ empty = ('', '')
|
||||
+
|
||||
+ # Test utils.getaddresses() and utils.parseaddr() on malformed email
|
||||
+ # addresses: default behavior (strict=True) rejects malformed address,
|
||||
+ # and strict=False which tolerates malformed address.
|
||||
+ for invalid_separator, expected_non_strict in (
|
||||
+ ('(', [(f'<{bob}>', alice)]),
|
||||
+ (')', [('', alice), empty, ('', bob)]),
|
||||
+ ('<', [('', alice), empty, ('', bob), empty]),
|
||||
+ ('>', [('', alice), empty, ('', bob)]),
|
||||
+ ('[', [('', f'{alice}[<{bob}>]')]),
|
||||
+ (']', [('', alice), empty, ('', bob)]),
|
||||
+ ('@', [empty, empty, ('', bob)]),
|
||||
+ (';', [('', alice), empty, ('', bob)]),
|
||||
+ (':', [('', alice), ('', bob)]),
|
||||
+ ('.', [('', alice + '.'), ('', bob)]),
|
||||
+ ('"', [('', alice), ('', f'<{bob}>')]),
|
||||
+ ):
|
||||
+ address = f'{alice}{invalid_separator}<{bob}>'
|
||||
+ with self.subTest(address=address):
|
||||
+ self.assertEqual(utils.getaddresses([address]),
|
||||
+ [empty])
|
||||
+ self.assertEqual(utils.getaddresses([address], strict=False),
|
||||
+ expected_non_strict)
|
||||
+
|
||||
+ self.assertEqual(utils.parseaddr([address]),
|
||||
+ empty)
|
||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
||||
+ ('', address))
|
||||
+
|
||||
+ # Comma (',') is treated differently depending on strict parameter.
|
||||
+ # Comma without quotes.
|
||||
+ address = f'{alice},<{bob}>'
|
||||
+ self.assertEqual(utils.getaddresses([address]),
|
||||
+ [('', alice), ('', bob)])
|
||||
+ self.assertEqual(utils.getaddresses([address], strict=False),
|
||||
+ [('', alice), ('', bob)])
|
||||
+ self.assertEqual(utils.parseaddr([address]),
|
||||
+ empty)
|
||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
||||
+ ('', address))
|
||||
+
|
||||
+ # Real name between quotes containing comma.
|
||||
+ address = '"Alice, alice@example.org" <bob@example.com>'
|
||||
+ expected_strict = ('Alice, alice@example.org', 'bob@example.com')
|
||||
+ self.assertEqual(utils.getaddresses([address]), [expected_strict])
|
||||
+ self.assertEqual(utils.getaddresses([address], strict=False), [expected_strict])
|
||||
+ self.assertEqual(utils.parseaddr([address]), expected_strict)
|
||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
||||
+ ('', address))
|
||||
+
|
||||
+ # Valid parenthesis in comments.
|
||||
+ address = 'alice@example.org (Alice)'
|
||||
+ expected_strict = ('Alice', 'alice@example.org')
|
||||
+ self.assertEqual(utils.getaddresses([address]), [expected_strict])
|
||||
+ self.assertEqual(utils.getaddresses([address], strict=False), [expected_strict])
|
||||
+ self.assertEqual(utils.parseaddr([address]), expected_strict)
|
||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
||||
+ ('', address))
|
||||
+
|
||||
+ # Invalid parenthesis in comments.
|
||||
+ address = 'alice@example.org )Alice('
|
||||
+ self.assertEqual(utils.getaddresses([address]), [empty])
|
||||
+ self.assertEqual(utils.getaddresses([address], strict=False),
|
||||
+ [('', 'alice@example.org'), ('', ''), ('', 'Alice')])
|
||||
+ self.assertEqual(utils.parseaddr([address]), empty)
|
||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
||||
+ ('', address))
|
||||
+
|
||||
+ # Two addresses with quotes separated by comma.
|
||||
+ address = '"Jane Doe" <jane@example.net>, "John Doe" <john@example.net>'
|
||||
+ self.assertEqual(utils.getaddresses([address]),
|
||||
+ [('Jane Doe', 'jane@example.net'),
|
||||
+ ('John Doe', 'john@example.net')])
|
||||
+ self.assertEqual(utils.getaddresses([address], strict=False),
|
||||
+ [('Jane Doe', 'jane@example.net'),
|
||||
+ ('John Doe', 'john@example.net')])
|
||||
+ self.assertEqual(utils.parseaddr([address]), empty)
|
||||
+ self.assertEqual(utils.parseaddr([address], strict=False),
|
||||
+ ('', address))
|
||||
+
|
||||
+ # Test email.utils.supports_strict_parsing attribute
|
||||
+ self.assertEqual(email.utils.supports_strict_parsing, True)
|
||||
+
|
||||
def test_getaddresses_nasty(self):
|
||||
- eq = self.assertEqual
|
||||
- eq(utils.getaddresses(['foo: ;']), [('', '')])
|
||||
- eq(utils.getaddresses(
|
||||
- ['[]*-- =~$']),
|
||||
- [('', ''), ('', ''), ('', '*--')])
|
||||
- eq(utils.getaddresses(
|
||||
- ['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>']),
|
||||
- [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')])
|
||||
+ for addresses, expected in (
|
||||
+ (['"Sürname, Firstname" <to@example.com>'],
|
||||
+ [('Sürname, Firstname', 'to@example.com')]),
|
||||
+
|
||||
+ (['foo: ;'],
|
||||
+ [('', '')]),
|
||||
+
|
||||
+ (['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>'],
|
||||
+ [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')]),
|
||||
+
|
||||
+ ([r'Pete(A nice \) chap) <pete(his account)@silly.test(his host)>'],
|
||||
+ [('Pete (A nice ) chap his account his host)', 'pete@silly.test')]),
|
||||
+
|
||||
+ (['(Empty list)(start)Undisclosed recipients :(nobody(I know))'],
|
||||
+ [('', '')]),
|
||||
+
|
||||
+ (['Mary <@machine.tld:mary@example.net>, , jdoe@test . example'],
|
||||
+ [('Mary', 'mary@example.net'), ('', ''), ('', 'jdoe@test.example')]),
|
||||
+
|
||||
+ (['John Doe <jdoe@machine(comment). example>'],
|
||||
+ [('John Doe (comment)', 'jdoe@machine.example')]),
|
||||
+
|
||||
+ (['"Mary Smith: Personal Account" <smith@home.example>'],
|
||||
+ [('Mary Smith: Personal Account', 'smith@home.example')]),
|
||||
+
|
||||
+ (['Undisclosed recipients:;'],
|
||||
+ [('', '')]),
|
||||
+
|
||||
+ ([r'<boss@nil.test>, "Giant; \"Big\" Box" <bob@example.net>'],
|
||||
+ [('', 'boss@nil.test'), ('Giant; "Big" Box', 'bob@example.net')]),
|
||||
+ ):
|
||||
+ with self.subTest(addresses=addresses):
|
||||
+ self.assertEqual(utils.getaddresses(addresses),
|
||||
+ expected)
|
||||
+ self.assertEqual(utils.getaddresses(addresses, strict=False),
|
||||
+ expected)
|
||||
+
|
||||
+ addresses = ['[]*-- =~$']
|
||||
+ self.assertEqual(utils.getaddresses(addresses),
|
||||
+ [('', '')])
|
||||
+ self.assertEqual(utils.getaddresses(addresses, strict=False),
|
||||
+ [('', ''), ('', ''), ('', '*--')])
|
||||
|
||||
def test_getaddresses_embedded_comment(self):
|
||||
"""Test proper handling of a nested comment"""
|
||||
@@ -3460,6 +3600,54 @@ multipart/report
|
||||
m = cls(*constructor, policy=email.policy.default)
|
||||
self.assertIs(m.policy, email.policy.default)
|
||||
|
||||
+ def test_iter_escaped_chars(self):
|
||||
+ self.assertEqual(list(utils._iter_escaped_chars(r'a\\b\"c\\"d')),
|
||||
+ [(0, 'a'),
|
||||
+ (2, '\\\\'),
|
||||
+ (3, 'b'),
|
||||
+ (5, '\\"'),
|
||||
+ (6, 'c'),
|
||||
+ (8, '\\\\'),
|
||||
+ (9, '"'),
|
||||
+ (10, 'd')])
|
||||
+ self.assertEqual(list(utils._iter_escaped_chars('a\\')),
|
||||
+ [(0, 'a'), (1, '\\')])
|
||||
+
|
||||
+ def test_strip_quoted_realnames(self):
|
||||
+ def check(addr, expected):
|
||||
+ self.assertEqual(utils._strip_quoted_realnames(addr), expected)
|
||||
+
|
||||
+ check('"Jane Doe" <jane@example.net>, "John Doe" <john@example.net>',
|
||||
+ ' <jane@example.net>, <john@example.net>')
|
||||
+ check(r'"Jane \"Doe\"." <jane@example.net>',
|
||||
+ ' <jane@example.net>')
|
||||
+
|
||||
+ # special cases
|
||||
+ check(r'before"name"after', 'beforeafter')
|
||||
+ check(r'before"name"', 'before')
|
||||
+ check(r'b"name"', 'b') # single char
|
||||
+ check(r'"name"after', 'after')
|
||||
+ check(r'"name"a', 'a') # single char
|
||||
+ check(r'"name"', '')
|
||||
+
|
||||
+ # no change
|
||||
+ for addr in (
|
||||
+ 'Jane Doe <jane@example.net>, John Doe <john@example.net>',
|
||||
+ 'lone " quote',
|
||||
+ ):
|
||||
+ self.assertEqual(utils._strip_quoted_realnames(addr), addr)
|
||||
+
|
||||
+
|
||||
+ def test_check_parenthesis(self):
|
||||
+ addr = 'alice@example.net'
|
||||
+ self.assertTrue(utils._check_parenthesis(f'{addr} (Alice)'))
|
||||
+ self.assertFalse(utils._check_parenthesis(f'{addr} )Alice('))
|
||||
+ self.assertFalse(utils._check_parenthesis(f'{addr} (Alice))'))
|
||||
+ self.assertFalse(utils._check_parenthesis(f'{addr} ((Alice)'))
|
||||
+
|
||||
+ # Ignore real name between quotes
|
||||
+ self.assertTrue(utils._check_parenthesis(f'")Alice((" {addr}'))
|
||||
+
|
||||
|
||||
# Test the iterator/generators
|
||||
class TestIterators(TestEmailBase):
|
||||
diff --git a/Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst b/Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst
|
||||
new file mode 100644
|
||||
index 0000000000..3d0e9e4078
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst
|
||||
@@ -0,0 +1,8 @@
|
||||
+:func:`email.utils.getaddresses` and :func:`email.utils.parseaddr` now
|
||||
+return ``('', '')`` 2-tuples in more situations where invalid email
|
||||
+addresses are encountered instead of potentially inaccurate values. Add
|
||||
+optional *strict* parameter to these two functions: use ``strict=False`` to
|
||||
+get the old behavior, accept malformed inputs.
|
||||
+``getattr(email.utils, 'supports_strict_parsing', False)`` can be use to check
|
||||
+if the *strict* paramater is available. Patch by Thomas Dwyer and Victor
|
||||
+Stinner to improve the CVE-2023-27043 fix.
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= <miro@hroncok.cz>
|
||||
Date: Tue, 5 Dec 2023 21:02:06 +0100
|
||||
Subject: [PATCH] 00419: gh-112769: test_zlib: Fix comparison of
|
||||
ZLIB_RUNTIME_VERSION with non-int suffix (GH-112771) (GH-112774)
|
||||
|
||||
zlib-ng defines the version as "1.3.0.zlib-ng".
|
||||
(cherry picked from commit d384813ff18b33280a90b6d2011654528a2b6ad1)
|
||||
---
|
||||
Lib/test/test_zlib.py | 28 ++++++++++++++++------------
|
||||
1 file changed, 16 insertions(+), 12 deletions(-)
|
||||
|
||||
diff --git a/Lib/test/test_zlib.py b/Lib/test/test_zlib.py
|
||||
index 02509cdf55..2a9e7e5ed3 100644
|
||||
--- a/Lib/test/test_zlib.py
|
||||
+++ b/Lib/test/test_zlib.py
|
||||
@@ -17,6 +17,20 @@ requires_Decompress_copy = unittest.skipUnless(
|
||||
'requires Decompress.copy()')
|
||||
|
||||
|
||||
+def _zlib_runtime_version_tuple(zlib_version=zlib.ZLIB_RUNTIME_VERSION):
|
||||
+ # Register "1.2.3" as "1.2.3.0"
|
||||
+ # or "1.2.0-linux","1.2.0.f","1.2.0.f-linux"
|
||||
+ v = zlib_version.split('-', 1)[0].split('.')
|
||||
+ if len(v) < 4:
|
||||
+ v.append('0')
|
||||
+ elif not v[-1].isnumeric():
|
||||
+ v[-1] = '0'
|
||||
+ return tuple(map(int, v))
|
||||
+
|
||||
+
|
||||
+ZLIB_RUNTIME_VERSION_TUPLE = _zlib_runtime_version_tuple()
|
||||
+
|
||||
+
|
||||
class VersionTestCase(unittest.TestCase):
|
||||
|
||||
def test_library_version(self):
|
||||
@@ -437,9 +451,8 @@ class CompressObjectTestCase(BaseCompressTestCase, unittest.TestCase):
|
||||
sync_opt = ['Z_NO_FLUSH', 'Z_SYNC_FLUSH', 'Z_FULL_FLUSH',
|
||||
'Z_PARTIAL_FLUSH']
|
||||
|
||||
- ver = tuple(int(v) for v in zlib.ZLIB_RUNTIME_VERSION.split('.'))
|
||||
# Z_BLOCK has a known failure prior to 1.2.5.3
|
||||
- if ver >= (1, 2, 5, 3):
|
||||
+ if ZLIB_RUNTIME_VERSION_TUPLE >= (1, 2, 5, 3):
|
||||
sync_opt.append('Z_BLOCK')
|
||||
|
||||
sync_opt = [getattr(zlib, opt) for opt in sync_opt
|
||||
@@ -768,16 +781,7 @@ class CompressObjectTestCase(BaseCompressTestCase, unittest.TestCase):
|
||||
|
||||
def test_wbits(self):
|
||||
# wbits=0 only supported since zlib v1.2.3.5
|
||||
- # Register "1.2.3" as "1.2.3.0"
|
||||
- # or "1.2.0-linux","1.2.0.f","1.2.0.f-linux"
|
||||
- v = zlib.ZLIB_RUNTIME_VERSION.split('-', 1)[0].split('.')
|
||||
- if len(v) < 4:
|
||||
- v.append('0')
|
||||
- elif not v[-1].isnumeric():
|
||||
- v[-1] = '0'
|
||||
-
|
||||
- v = tuple(map(int, v))
|
||||
- supports_wbits_0 = v >= (1, 2, 3, 5)
|
||||
+ supports_wbits_0 = ZLIB_RUNTIME_VERSION_TUPLE >= (1, 2, 3, 5)
|
||||
|
||||
co = zlib.compressobj(level=1, wbits=15)
|
||||
zlib15 = co.compress(HAMLET_SCENE) + co.flush()
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: "Miss Islington (bot)"
|
||||
<31488909+miss-islington@users.noreply.github.com>
|
||||
Date: Mon, 31 Mar 2025 20:29:04 +0200
|
||||
Subject: 00452: Properly apply exported CFLAGS for dtrace/systemtap builds
|
||||
|
||||
When using --with-dtrace the resulting object file could be missing
|
||||
specific CFLAGS exported by the build system due to the systemtap
|
||||
script using specific defaults.
|
||||
|
||||
Exporting the CC and CFLAGS variables before the dtrace invocation
|
||||
allows us to properly apply CFLAGS exported by the build system
|
||||
even when cross-compiling.
|
||||
|
||||
Co-authored-by: stratakis <cstratak@redhat.com>
|
||||
---
|
||||
Makefile.pre.in | 4 ++--
|
||||
.../next/Build/2025-03-31-19-22-41.gh-issue-131865.PIJy7X.rst | 2 ++
|
||||
2 files changed, 4 insertions(+), 2 deletions(-)
|
||||
create mode 100644 Misc/NEWS.d/next/Build/2025-03-31-19-22-41.gh-issue-131865.PIJy7X.rst
|
||||
|
||||
diff --git a/Makefile.pre.in b/Makefile.pre.in
|
||||
index 568018827b..b401724d92 100644
|
||||
--- a/Makefile.pre.in
|
||||
+++ b/Makefile.pre.in
|
||||
@@ -989,7 +989,7 @@ Python/frozen.o: $(srcdir)/Python/importlib.h $(srcdir)/Python/importlib_externa
|
||||
# an include guard, so we can't use a pipeline to transform its output.
|
||||
Include/pydtrace_probes.h: $(srcdir)/Include/pydtrace.d
|
||||
$(MKDIR_P) Include
|
||||
- $(DTRACE) $(DFLAGS) -o $@ -h -s $<
|
||||
+ CC="$(CC)" CFLAGS="$(CFLAGS)" $(DTRACE) $(DFLAGS) -o $@ -h -s $<
|
||||
: sed in-place edit with POSIX-only tools
|
||||
sed 's/PYTHON_/PyDTrace_/' $@ > $@.tmp
|
||||
mv $@.tmp $@
|
||||
@@ -999,7 +999,7 @@ Python/import.o: $(srcdir)/Include/pydtrace.h
|
||||
Modules/gcmodule.o: $(srcdir)/Include/pydtrace.h
|
||||
|
||||
Python/pydtrace.o: $(srcdir)/Include/pydtrace.d $(DTRACE_DEPS)
|
||||
- $(DTRACE) $(DFLAGS) -o $@ -G -s $< $(DTRACE_DEPS)
|
||||
+ CC="$(CC)" CFLAGS="$(CFLAGS)" $(DTRACE) $(DFLAGS) -o $@ -G -s $< $(DTRACE_DEPS)
|
||||
|
||||
Objects/typeobject.o: Objects/typeslots.inc
|
||||
|
||||
diff --git a/Misc/NEWS.d/next/Build/2025-03-31-19-22-41.gh-issue-131865.PIJy7X.rst b/Misc/NEWS.d/next/Build/2025-03-31-19-22-41.gh-issue-131865.PIJy7X.rst
|
||||
new file mode 100644
|
||||
index 0000000000..a287e0b228
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Build/2025-03-31-19-22-41.gh-issue-131865.PIJy7X.rst
|
||||
@@ -0,0 +1,2 @@
|
||||
+The DTrace build now properly passes the ``CC`` and ``CFLAGS`` variables
|
||||
+to the ``dtrace`` command when utilizing SystemTap on Linux.
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: "Miss Islington (bot)"
|
||||
<31488909+miss-islington@users.noreply.github.com>
|
||||
Date: Mon, 22 Dec 2025 14:48:49 +0100
|
||||
Subject: 00471: CVE-2025-12084
|
||||
|
||||
* gh-142145: Remove quadratic behavior in node ID cache clearing (GH-142146)
|
||||
* gh-142754: Ensure that Element & Attr instances have the ownerDocument attribute (GH-142794)
|
||||
(cherry picked from commit 1cc7551b3f9f71efbc88d96dce90f82de98b2454)
|
||||
(cherry picked from commit 08d8e18ad81cd45bc4a27d6da478b51ea49486e4)
|
||||
(cherry picked from commit 8d2d7bb2e754f8649a68ce4116271a4932f76907)
|
||||
|
||||
Co-authored-by: Jacob Walls <38668450+jacobtylerwalls@users.noreply.github.com>
|
||||
Co-authored-by: Seth Michael Larson <seth@python.org>
|
||||
Co-authored-by: Petr Viktorin <encukou@gmail.com>
|
||||
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
|
||||
Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
|
||||
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
|
||||
Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
|
||||
Co-authored-by: Gregory P. Smith <greg@krypto.org>
|
||||
---
|
||||
Lib/test/test_minidom.py | 33 ++++++++++++++++++-
|
||||
Lib/xml/dom/minidom.py | 11 ++-----
|
||||
...-12-01-09-36-45.gh-issue-142145.tcAUhg.rst | 6 ++++
|
||||
3 files changed, 41 insertions(+), 9 deletions(-)
|
||||
create mode 100644 Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst
|
||||
|
||||
diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py
|
||||
index 97620258d8..9f7f5b240e 100644
|
||||
--- a/Lib/test/test_minidom.py
|
||||
+++ b/Lib/test/test_minidom.py
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import copy
|
||||
import pickle
|
||||
+import time
|
||||
import io
|
||||
from test import support
|
||||
import unittest
|
||||
@@ -9,7 +10,7 @@ import unittest
|
||||
import pyexpat
|
||||
import xml.dom.minidom
|
||||
|
||||
-from xml.dom.minidom import parse, Node, Document, parseString
|
||||
+from xml.dom.minidom import parse, Attr, Node, Document, Element, parseString
|
||||
from xml.dom.minidom import getDOMImplementation
|
||||
from xml.parsers.expat import ExpatError
|
||||
|
||||
@@ -163,6 +164,36 @@ class MinidomTest(unittest.TestCase):
|
||||
self.confirm(dom.documentElement.childNodes[-1].data == "Hello")
|
||||
dom.unlink()
|
||||
|
||||
+ @support.requires_resource('cpu')
|
||||
+ def testAppendChildNoQuadraticComplexity(self):
|
||||
+ impl = getDOMImplementation()
|
||||
+
|
||||
+ newdoc = impl.createDocument(None, "some_tag", None)
|
||||
+ top_element = newdoc.documentElement
|
||||
+ children = [newdoc.createElement(f"child-{i}") for i in range(1, 2 ** 15 + 1)]
|
||||
+ element = top_element
|
||||
+
|
||||
+ start = time.monotonic()
|
||||
+ for child in children:
|
||||
+ element.appendChild(child)
|
||||
+ element = child
|
||||
+ end = time.monotonic()
|
||||
+
|
||||
+ # This example used to take at least 30 seconds.
|
||||
+ # Conservative assertion due to the wide variety of systems and
|
||||
+ # build configs timing based tests wind up run under.
|
||||
+ # A --with-address-sanitizer --with-pydebug build on a rpi5 still
|
||||
+ # completes this loop in <0.5 seconds.
|
||||
+ self.assertLess(end - start, 4)
|
||||
+
|
||||
+ def testSetAttributeNodeWithoutOwnerDocument(self):
|
||||
+ # regression test for gh-142754
|
||||
+ elem = Element("test")
|
||||
+ attr = Attr("id")
|
||||
+ attr.value = "test-id"
|
||||
+ elem.setAttributeNode(attr)
|
||||
+ self.assertEqual(elem.getAttribute("id"), "test-id")
|
||||
+
|
||||
def testAppendChildFragment(self):
|
||||
dom, orig, c1, c2, c3, frag = self._create_fragment_test_nodes()
|
||||
dom.documentElement.appendChild(frag)
|
||||
diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py
|
||||
index d09ef5e7d0..e4e8b42996 100644
|
||||
--- a/Lib/xml/dom/minidom.py
|
||||
+++ b/Lib/xml/dom/minidom.py
|
||||
@@ -292,13 +292,6 @@ def _append_child(self, node):
|
||||
childNodes.append(node)
|
||||
node.parentNode = self
|
||||
|
||||
-def _in_document(node):
|
||||
- # return True iff node is part of a document tree
|
||||
- while node is not None:
|
||||
- if node.nodeType == Node.DOCUMENT_NODE:
|
||||
- return True
|
||||
- node = node.parentNode
|
||||
- return False
|
||||
|
||||
def _write_data(writer, data):
|
||||
"Writes datachars to writer."
|
||||
@@ -355,6 +348,7 @@ class Attr(Node):
|
||||
def __init__(self, qName, namespaceURI=EMPTY_NAMESPACE, localName=None,
|
||||
prefix=None):
|
||||
self.ownerElement = None
|
||||
+ self.ownerDocument = None
|
||||
self._name = qName
|
||||
self.namespaceURI = namespaceURI
|
||||
self._prefix = prefix
|
||||
@@ -678,6 +672,7 @@ class Element(Node):
|
||||
|
||||
def __init__(self, tagName, namespaceURI=EMPTY_NAMESPACE, prefix=None,
|
||||
localName=None):
|
||||
+ self.ownerDocument = None
|
||||
self.parentNode = None
|
||||
self.tagName = self.nodeName = tagName
|
||||
self.prefix = prefix
|
||||
@@ -1537,7 +1532,7 @@ def _clear_id_cache(node):
|
||||
if node.nodeType == Node.DOCUMENT_NODE:
|
||||
node._id_cache.clear()
|
||||
node._id_search_stack = None
|
||||
- elif _in_document(node):
|
||||
+ elif node.ownerDocument:
|
||||
node.ownerDocument._id_cache.clear()
|
||||
node.ownerDocument._id_search_stack= None
|
||||
|
||||
diff --git a/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst b/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst
|
||||
new file mode 100644
|
||||
index 0000000000..05c7df35d1
|
||||
--- /dev/null
|
||||
+++ b/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst
|
||||
@@ -0,0 +1,6 @@
|
||||
+Remove quadratic behavior in ``xml.minidom`` node ID cache clearing. In order
|
||||
+to do this without breaking existing users, we also add the *ownerDocument*
|
||||
+attribute to :mod:`xml.dom.minidom` elements and attributes created by directly
|
||||
+instantiating the ``Element`` or ``Attr`` class. Note that this way of creating
|
||||
+nodes is not supported; creator functions like
|
||||
+:py:meth:`xml.dom.Document.documentElement` should be used instead.
|
||||
50
plan.fmf
50
plan.fmf
|
|
@ -1,50 +0,0 @@
|
|||
execute:
|
||||
how: tmt
|
||||
|
||||
provision:
|
||||
hardware:
|
||||
memory: '>= 3 GB'
|
||||
|
||||
environment:
|
||||
pybasever: '3.9'
|
||||
|
||||
discover:
|
||||
- name: tests_python
|
||||
how: shell
|
||||
url: https://src.fedoraproject.org/tests/python.git
|
||||
tests:
|
||||
- name: smoke
|
||||
path: /smoke
|
||||
test: "VERSION=${pybasever} ./venv.sh"
|
||||
- name: debugsmoke
|
||||
path: /smoke
|
||||
test: "PYTHON=python${pybasever}d TOX=false VERSION=${pybasever} INSTALL_OR_SKIP=true ./venv.sh"
|
||||
- name: selftest
|
||||
path: /selftest
|
||||
test: VERSION=${pybasever} X="-x test_wsgiref" ./parallel.sh
|
||||
- name: marshalparser
|
||||
path: /marshalparser
|
||||
test: "VERSION=${pybasever} SAMPLE=10 ./test_marshalparser_compatibility.sh"
|
||||
|
||||
prepare:
|
||||
- name: Install dependencies
|
||||
how: install
|
||||
package:
|
||||
- gcc # for extension building in venv and selftest
|
||||
- gdb # for test_gdb
|
||||
- "python${pybasever}" # the test subject
|
||||
- "python${pybasever}-devel" # for extension building in venv and selftest
|
||||
- "python${pybasever}-tkinter" # for selftest
|
||||
- "python${pybasever}-test" # for selftest
|
||||
- python3-tox # for venv tests
|
||||
- glibc-all-langpacks # for locale tests
|
||||
- marshalparser # for testing compatibility (magic numbers) with marshalparser
|
||||
- rpm # for debugging marshalparser
|
||||
- dnf # for upgrade
|
||||
- name: Update packages
|
||||
how: shell
|
||||
script: dnf upgrade -y
|
||||
- name: rpm_qa
|
||||
order: 100
|
||||
how: shell
|
||||
script: rpm -qa | sort | tee $TMT_PLAN_DATA/rpmqa.txt
|
||||
169
python3.9.spec
169
python3.9.spec
|
|
@ -13,11 +13,11 @@ URL: https://www.python.org/
|
|||
|
||||
# WARNING When rebasing to a new Python version,
|
||||
# remember to update the python3-docs package as well
|
||||
%global general_version %{pybasever}.25
|
||||
%global general_version %{pybasever}.19
|
||||
#global prerel ...
|
||||
%global upstream_version %{general_version}%{?prerel}
|
||||
Version: %{general_version}%{?prerel:~%{prerel}}
|
||||
Release: 3%{?dist}
|
||||
Release: 2%{?dist}
|
||||
License: Python
|
||||
|
||||
|
||||
|
|
@ -40,10 +40,9 @@ License: Python
|
|||
%endif
|
||||
|
||||
# Flat package, i.e. no separate subpackages
|
||||
# Default (in Fedora >= 44): disabled
|
||||
# Default (in Fedora < 44): enabled when this is not the main Python
|
||||
# Default (in Fedora): if this is a main Python, it is not a flatpackage
|
||||
# Not supported: Combination of flatpackage enabled and main_python enabled
|
||||
%if %{with main_python} || 0%{?fedora} >= 44
|
||||
%if %{with main_python}
|
||||
%bcond_with flatpackage
|
||||
%else
|
||||
%bcond_without flatpackage
|
||||
|
|
@ -238,7 +237,6 @@ BuildRequires: libnsl2-devel
|
|||
BuildRequires: libtirpc-devel
|
||||
BuildRequires: libGL-devel
|
||||
BuildRequires: libuuid-devel
|
||||
BuildRequires: libxcrypt-devel
|
||||
BuildRequires: libX11-devel
|
||||
BuildRequires: make
|
||||
BuildRequires: ncurses-devel
|
||||
|
|
@ -251,9 +249,9 @@ BuildRequires: sqlite-devel
|
|||
BuildRequires: gdb
|
||||
|
||||
BuildRequires: tar
|
||||
BuildRequires: tcl-devel < 1:9
|
||||
BuildRequires: tcl-devel
|
||||
BuildRequires: tix-devel
|
||||
BuildRequires: tk-devel < 1:9
|
||||
BuildRequires: tk-devel
|
||||
BuildRequires: tzdata
|
||||
|
||||
%if %{with valgrind}
|
||||
|
|
@ -263,7 +261,6 @@ BuildRequires: valgrind-devel
|
|||
BuildRequires: xz-devel
|
||||
BuildRequires: zlib-devel
|
||||
|
||||
BuildRequires: systemtap-sdt-devel
|
||||
BuildRequires: /usr/bin/dtrace
|
||||
|
||||
# workaround http://bugs.python.org/issue19804 (test_uuid requires ifconfig)
|
||||
|
|
@ -272,9 +269,6 @@ BuildRequires: /usr/sbin/ifconfig
|
|||
%if %{with rpmwheels}
|
||||
BuildRequires: python-setuptools-wheel
|
||||
BuildRequires: python-pip-wheel
|
||||
%else
|
||||
# For %%python_wheel_inject_sbom
|
||||
BuildRequires: python-rpm-macros
|
||||
%endif
|
||||
|
||||
%if %{without bootstrap}
|
||||
|
|
@ -307,7 +301,6 @@ Source11: idle3.appdata.xml
|
|||
|
||||
# 00001 # d06a8853cf4bae9e115f45e1d531d2dc152c5cc8
|
||||
# Fixup distutils/unixccompiler.py to remove standard library path from rpath
|
||||
#
|
||||
# Was Patch0 in ivazquez' python3000 specfile
|
||||
Patch1: 00001-rpath.patch
|
||||
|
||||
|
|
@ -319,7 +312,7 @@ Patch1: 00001-rpath.patch
|
|||
# See https://bugzilla.redhat.com/show_bug.cgi?id=556092
|
||||
Patch111: 00111-no-static-lib.patch
|
||||
|
||||
# 00189 # 0c6dd5d318a22bbe89e09e1cd5513eaaca549aa5
|
||||
# 00189 # 60517f098bd1525ad454adf7252b60a3d6b0f8ba
|
||||
# Instead of bundled wheels, use our RPM packaged wheels
|
||||
#
|
||||
# We keep them in /usr/share/python-wheels
|
||||
|
|
@ -332,7 +325,7 @@ Patch189: 00189-use-rpm-wheels.patch
|
|||
# When the bundled setuptools/pip wheel is updated, the patch no longer applies cleanly.
|
||||
# In such cases, the patch needs to be amended and the versions updated here:
|
||||
%global pip_version 23.0.1
|
||||
%global setuptools_version 79.0.1
|
||||
%global setuptools_version 58.1.0
|
||||
|
||||
# 00251 # 1b1047c14ff98eae6d355b4aac4df3e388813f62
|
||||
# Change user install location
|
||||
|
|
@ -391,24 +384,20 @@ Patch371: 00371-revert-bpo-1596321-fix-threading-_shutdown-for-the-main-thread-g
|
|||
# gh-99086: Fix implicit int compiler warning in configure check for PTHREAD_SCOPE_SYSTEM
|
||||
Patch407: 00407-gh-99086-fix-implicit-int-compiler-warning-in-configure-check-for-pthread_scope_system.patch
|
||||
|
||||
# 00452 # eb11d070c5af7d1b5e47f4e02186152d08eaf793
|
||||
# Properly apply exported CFLAGS for dtrace/systemtap builds
|
||||
# 00415 # 512c60eb23a8d7b26d74824a6d7bbefb6feefb65
|
||||
# [CVE-2023-27043] gh-102988: Reject malformed addresses in email.parseaddr() (#111116)
|
||||
#
|
||||
# When using --with-dtrace the resulting object file could be missing
|
||||
# specific CFLAGS exported by the build system due to the systemtap
|
||||
# script using specific defaults.
|
||||
#
|
||||
# Exporting the CC and CFLAGS variables before the dtrace invocation
|
||||
# allows us to properly apply CFLAGS exported by the build system
|
||||
# even when cross-compiling.
|
||||
Patch452: 00452-properly-apply-exported-cflags-for-dtrace-systemtap-builds.patch
|
||||
# Detect email address parsing errors and return empty tuple to
|
||||
# indicate the parsing error (old API). Add an optional 'strict'
|
||||
# parameter to getaddresses() and parseaddr() functions. Patch by
|
||||
# Thomas Dwyer.
|
||||
Patch415: 00415-cve-2023-27043-gh-102988-reject-malformed-addresses-in-email-parseaddr-111116.patch
|
||||
|
||||
# 00471 # fc5f344f7e15c13dbf41824a1b7a82d92205f79d
|
||||
# CVE-2025-12084
|
||||
# 00419 # f13682530cc7e4daec2e40acd56508846fdd3aad
|
||||
# gh-112769: test_zlib: Fix comparison of ZLIB_RUNTIME_VERSION with non-int suffix (GH-112771) (GH-112774)
|
||||
#
|
||||
# * gh-142145: Remove quadratic behavior in node ID cache clearing (GH-142146)
|
||||
# * gh-142754: Ensure that Element & Attr instances have the ownerDocument attribute (GH-142794)
|
||||
Patch471: 00471-cve-2025-12084.patch
|
||||
# zlib-ng defines the version as "1.3.0.zlib-ng".
|
||||
Patch419: 00419-gh-112769-test_zlib-fix-comparison-of-zlib_runtime_version-with-non-int-suffix-gh-112771-gh-112774.patch
|
||||
|
||||
# (New patches go here ^^^)
|
||||
#
|
||||
|
|
@ -478,18 +467,9 @@ Obsoletes: platform-python < %{pybasever}
|
|||
Provides: python%{pyshortver} = %{version}-%{release}
|
||||
Obsoletes: python%{pyshortver} < %{version}-%{release}
|
||||
|
||||
# https://docs.fedoraproject.org/en-US/packaging-guidelines/#_one_to_many_replacement
|
||||
Obsoletes: %{pkgname} < 3.9.24-2
|
||||
|
||||
%if %{with main_python}
|
||||
# Packages with Python modules in standard locations automatically
|
||||
# depend on python(abi). Provide that here.
|
||||
Provides: python(abi) = %{pybasever}
|
||||
%else
|
||||
# We exclude the `python(abi)` Provides
|
||||
%global __requires_exclude ^python\\(abi\\) = 3\\..+
|
||||
%global __provides_exclude ^python\\(abi\\) = 3\\..+
|
||||
%endif
|
||||
|
||||
Requires: %{pkgname}-libs%{?_isa} = %{version}-%{release}
|
||||
|
||||
|
|
@ -604,8 +584,6 @@ Conflicts: python-libs < 3
|
|||
# (We explicitly conflict with python-libs and not python2-libs, so only the
|
||||
# old Python 2 builds that still provided unversioned Python are handled.)
|
||||
|
||||
# https://docs.fedoraproject.org/en-US/packaging-guidelines/#_one_to_many_replacement
|
||||
Obsoletes: %{pkgname} < 3.9.24-2
|
||||
|
||||
%description -n %{pkgname}-libs
|
||||
This package contains runtime libraries for use by Python:
|
||||
|
|
@ -625,7 +603,6 @@ Requires: (python3-rpm-macros if rpm-build)
|
|||
Requires: (pyproject-rpm-macros if rpm-build)
|
||||
|
||||
%if %{without bootstrap}
|
||||
%if %{with main_python}
|
||||
# This is not "API" (packages that need setuptools should still BuildRequire it)
|
||||
# However some packages apparently can build both with and without setuptools
|
||||
# producing egg-info as file or directory (depending on setuptools presence).
|
||||
|
|
@ -634,7 +611,6 @@ Requires: (pyproject-rpm-macros if rpm-build)
|
|||
# See https://bugzilla.redhat.com/show_bug.cgi?id=1623914
|
||||
# See https://fedoraproject.org/wiki/Packaging:Directory_Replacement
|
||||
Requires: (%{pkgname}-setuptools if rpm-build)
|
||||
%endif
|
||||
|
||||
Requires: (python3-rpm-generators if rpm-build)
|
||||
%endif
|
||||
|
|
@ -654,9 +630,6 @@ Provides: platform-python-devel%{?_isa} = %{version}-%{release}
|
|||
Obsoletes: platform-python-devel < %{pybasever}
|
||||
%endif
|
||||
|
||||
# https://docs.fedoraproject.org/en-US/packaging-guidelines/#_one_to_many_replacement
|
||||
Obsoletes: %{pkgname} < 3.9.24-2
|
||||
|
||||
%description -n %{pkgname}-devel
|
||||
This package contains the header files and configuration needed to compile
|
||||
Python extension modules (typically written in C or C++), to embed Python
|
||||
|
|
@ -681,9 +654,6 @@ Obsoletes: %{pkgname}-tools < %{version}-%{release}
|
|||
# In Fedora 31, /usr/bin/idle was moved here from Python 2.
|
||||
Conflicts: python-tools < 3
|
||||
|
||||
# https://docs.fedoraproject.org/en-US/packaging-guidelines/#_one_to_many_replacement
|
||||
Obsoletes: %{pkgname} < 3.9.24-2
|
||||
|
||||
%description -n %{pkgname}-idle
|
||||
IDLE is Python’s Integrated Development and Learning Environment.
|
||||
|
||||
|
|
@ -705,9 +675,6 @@ Requires: %{pkgname} = %{version}-%{release}
|
|||
# (We don't provide python3-turtledemo, that's not too useful when imported.)
|
||||
%py_provides %{pkgname}-turtle
|
||||
|
||||
# https://docs.fedoraproject.org/en-US/packaging-guidelines/#_one_to_many_replacement
|
||||
Obsoletes: %{pkgname} < 3.9.24-2
|
||||
|
||||
%description -n %{pkgname}-tkinter
|
||||
The Tkinter (Tk interface) library is a graphical user interface toolkit for
|
||||
the Python programming language.
|
||||
|
|
@ -718,9 +685,6 @@ Summary: The self-test suite for the main python3 package
|
|||
Requires: %{pkgname} = %{version}-%{release}
|
||||
Requires: %{pkgname}-libs%{?_isa} = %{version}-%{release}
|
||||
|
||||
# https://docs.fedoraproject.org/en-US/packaging-guidelines/#_one_to_many_replacement
|
||||
Obsoletes: %{pkgname} < 3.9.24-2
|
||||
|
||||
%description -n %{pkgname}-test
|
||||
The self-test suite for the Python interpreter.
|
||||
|
||||
|
|
@ -771,6 +735,11 @@ The debug runtime additionally supports debug builds of C-API extensions
|
|||
|
||||
%else # with flatpackage
|
||||
|
||||
# We'll not provide this, on purpose
|
||||
# No package in Fedora shall ever depend on flatpackage via this
|
||||
%global __requires_exclude ^python\\(abi\\) = 3\\..$
|
||||
%global __provides_exclude ^python\\(abi\\) = 3\\..$
|
||||
|
||||
# Python interpreter packages used to be named (or provide) name pythonXY (e.g.
|
||||
# python39). However, to align it with the executable names and to prepare for
|
||||
# Python 3.10, they were renamed to pythonX.Y (e.g. python3.9, python3.10). We
|
||||
|
|
@ -803,16 +772,6 @@ Requires: tzdata
|
|||
# Other subpackages (like -debug) also need this, but they all depend on -libs.
|
||||
Requires: expat >= 2.6
|
||||
|
||||
# Provides of the subpackages contained in flatpackage
|
||||
Provides: %{pkgname}-libs = %{version}-%{release}
|
||||
Provides: %{pkgname}-devel = %{version}-%{release}
|
||||
Provides: %{pkgname}-idle = %{version}-%{release}
|
||||
Provides: %{pkgname}-tkinter = %{version}-%{release}
|
||||
Provides: %{pkgname}-test = %{version}-%{release}
|
||||
%if %{with debug_build}
|
||||
Provides: %{pkgname}-debug = %{version}-%{release}
|
||||
%endif
|
||||
|
||||
# The description for the flat package (SRPM and built)
|
||||
%description
|
||||
Python %{pybasever} package for developers.
|
||||
|
|
@ -1269,11 +1228,6 @@ for file in %{buildroot}%{pylibdir}/pydoc_data/topics.py $(grep --include='*.py'
|
|||
rm ${directory}/{__pycache__/${module}.cpython-%{pyshortver}.opt-?.pyc,${module}.py}
|
||||
done
|
||||
|
||||
%if %{without rpmwheels}
|
||||
# Inject SBOM into the installed wheels (if the macro is available)
|
||||
%{?python_wheel_inject_sbom:%python_wheel_inject_sbom %{buildroot}%{pylibdir}/ensurepip/_bundled/*.whl}
|
||||
%endif
|
||||
|
||||
# ======================================================
|
||||
# Checks for packaging issues
|
||||
# ======================================================
|
||||
|
|
@ -1344,8 +1298,6 @@ CheckPython() {
|
|||
# package: rpmbuild requires /usr/bin/pythonX.Y to be installed
|
||||
# test_gdb on arm on Fedora 33:
|
||||
# https://bugzilla.redhat.com/show_bug.cgi?id=1846390
|
||||
# test_sendfile_close_peer_in_the_middle_of_receiving:
|
||||
# https://github.com/python/cpython/issues/120226
|
||||
LD_LIBRARY_PATH=$ConfDir $ConfDir/python -m test.regrtest \
|
||||
-wW --slowest -j0 --timeout=1800 \
|
||||
%if %{with bootstrap}
|
||||
|
|
@ -1359,9 +1311,6 @@ CheckPython() {
|
|||
-x test_gdb \
|
||||
%endif
|
||||
%endif
|
||||
%ifarch ppc64le
|
||||
-i test_sendfile_close_peer_in_the_middle_of_receiving \
|
||||
%endif
|
||||
|
||||
echo FINISHED: CHECKING OF PYTHON FOR CONFIGURATION: $ConfName
|
||||
|
||||
|
|
@ -1541,10 +1490,6 @@ CheckPython optimized
|
|||
%dir %{pylibdir}/site-packages/
|
||||
%dir %{pylibdir}/site-packages/__pycache__/
|
||||
%{pylibdir}/site-packages/README.txt
|
||||
|
||||
%exclude %{pylibdir}/_sysconfigdata_d_linux_%{platform_triplet}.py
|
||||
%exclude %{pylibdir}/__pycache__/_sysconfigdata_d_linux_%{platform_triplet}%{bytecode_suffixes}
|
||||
|
||||
%{pylibdir}/*.py
|
||||
%dir %{pylibdir}/__pycache__/
|
||||
%{pylibdir}/__pycache__/*%{bytecode_suffixes}
|
||||
|
|
@ -1873,9 +1818,6 @@ CheckPython optimized
|
|||
%{dynload_dir}/_testinternalcapi.%{SOABI_debug}.so
|
||||
%{dynload_dir}/_testmultiphase.%{SOABI_debug}.so
|
||||
|
||||
%{pylibdir}/_sysconfigdata_d_linux_%{platform_triplet}.py
|
||||
%{pylibdir}/__pycache__/_sysconfigdata_d_linux_%{platform_triplet}%{bytecode_suffixes}
|
||||
|
||||
%endif # with debug_build
|
||||
|
||||
# We put the debug-gdb.py file inside /usr/lib/debug to avoid noise from ldconfig
|
||||
|
|
@ -1899,69 +1841,6 @@ CheckPython optimized
|
|||
# ======================================================
|
||||
|
||||
%changelog
|
||||
* Wed Jan 14 2026 Lumír Balhar <lbalhar@redhat.com> - 3.9.25-3
|
||||
- Security fix for CVE-2025-12084
|
||||
|
||||
* Mon Nov 10 2025 Tomas Orsava <torsava@redhat.com> - 3.9.25-2
|
||||
- Move _sysconfigdata_d_linux*.py to the debug subpackage
|
||||
|
||||
* Mon Nov 03 2025 Karolina Surma <ksurma@redhat.com> - 3.9.25-1
|
||||
- Update to Python 3.9.25
|
||||
|
||||
* Wed Oct 15 2025 Miro Hrončok <mhroncok@redhat.com> - 3.9.24-2
|
||||
- On Fedora 44+, split this package into multiple subpackages
|
||||
- This mimics newer Python versions
|
||||
|
||||
* Fri Oct 10 2025 Karolina Surma <ksurma@redhat.com> - 3.9.24-1
|
||||
- Update to Python 3.9.24
|
||||
|
||||
* Fri Jul 25 2025 Fedora Release Engineering <releng@fedoraproject.org> - 3.9.23-2
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_43_Mass_Rebuild
|
||||
|
||||
* Wed Jun 04 2025 Tomáš Hrnčiar <thrnciar@redhat.com> - 3.9.23-1
|
||||
- Update to 3.9.23
|
||||
|
||||
* Wed Apr 23 2025 Miro Hrončok <mhroncok@redhat.com> - 3.9.22-2
|
||||
- Add RPM Provides for python3.9-libs, python3.9-devel, python3.9-idle, python3.9-tkinter, python3.9-test
|
||||
|
||||
* Wed Apr 09 2025 Tomáš Hrnčiar <thrnciar@redhat.com> - 3.9.22-1
|
||||
- Update to 3.9.22
|
||||
|
||||
* Mon Mar 31 2025 Charalampos Stratakis <cstratak@redhat.com> - 3.9.21-5
|
||||
- Properly apply exported CFLAGS for dtrace/systemtap builds
|
||||
- Fixes: rhbz#2356304
|
||||
|
||||
* Mon Feb 10 2025 Charalampos Stratakis <cstratak@redhat.com> - 3.9.21-4
|
||||
- Security fix for CVE-2025-0938
|
||||
- Fixes: rhbz#2343278
|
||||
|
||||
* Sat Feb 01 2025 Björn Esser <besser82@fedoraproject.org> - 3.9.21-3
|
||||
- Add explicit BR: libxcrypt-devel
|
||||
|
||||
* Sat Jan 18 2025 Fedora Release Engineering <releng@fedoraproject.org> - 3.9.21-2
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_42_Mass_Rebuild
|
||||
|
||||
* Tue Dec 03 2024 Lumír Balhar <lbalhar@redhat.com> - 3.9.21-1
|
||||
- Update to 3.9.21
|
||||
- Fixes: rhbz#2321662
|
||||
|
||||
* Mon Sep 09 2024 Tomáš Hrnčiar <thrnciar@redhat.com> - 3.9.20-1
|
||||
- Update to 3.9.20
|
||||
|
||||
* Fri Aug 23 2024 Charalampos Stratakis <cstratak@redhat.com> - 3.9.19-6
|
||||
- Security fix for CVE-2024-8088
|
||||
- Fixes: rhbz#2307466
|
||||
|
||||
* Tue Aug 13 2024 Lumír Balhar <lbalhar@redhat.com> - 3.9.19-5
|
||||
- Security fix for CVE-2024-4032 (rhbz#2293397)
|
||||
- Security fix for CVE-2024-6923 (rhbz#2303164)
|
||||
|
||||
* Tue Jul 23 2024 Lumír Balhar <lbalhar@redhat.com> - 3.9.19-4
|
||||
- Require systemtap-sdt-devel for sys/sdt.h
|
||||
|
||||
* Fri Jul 19 2024 Fedora Release Engineering <releng@fedoraproject.org> - 3.9.19-3
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_41_Mass_Rebuild
|
||||
|
||||
* Wed Apr 17 2024 Miro Hrončok <mhroncok@redhat.com> - 3.9.19-2
|
||||
- Require expat >= 2.6 to prevent errors when creating venvs with older expat
|
||||
|
||||
|
|
|
|||
4
sources
4
sources
|
|
@ -1,2 +1,2 @@
|
|||
SHA512 (Python-3.9.25.tar.xz) = 33fd65952cc3ce5df83825aa32a103935815bdd5a016e5fd9896cafb068a3f89b3a6134458a2694e4f0f4f8a9fbe84739b53116264728b32cde0f03ab210cb19
|
||||
SHA512 (Python-3.9.25.tar.xz.asc) = 83f0a0e558aa89a106bdffeeb9b0fa2685fbd7be5c5954f9176c59c6c7023716207b07239f202b3508cbb98ca34572161955f0bfd3732fdb9265721cd6723dbe
|
||||
SHA512 (Python-3.9.19.tar.xz) = 5577830c734e63a70bbc62cd33d263b9aa87c4381b49cb694c3559067c4c682a55506b65ec5514a8e0a5abf6294dc728e909385d449ae1c388e62f83cea9bb89
|
||||
SHA512 (Python-3.9.19.tar.xz.asc) = f7f4946243dfc56de2c84f50276b088d347f17054f50e3331d1e312e2a8e2c6ed1b4b4a807202b51137fd2af3fc9218cafa42ed348a954ace896d9a432e2defd
|
||||
|
|
|
|||
4
tests/provision.fmf
Normal file
4
tests/provision.fmf
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
standard-inventory-qcow2:
|
||||
qemu:
|
||||
m: 3G # Amount of VM memory
|
||||
37
tests/tests.yml
Normal file
37
tests/tests.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
- hosts: localhost
|
||||
tags:
|
||||
- classic
|
||||
tasks:
|
||||
- dnf:
|
||||
name: "*"
|
||||
state: latest
|
||||
|
||||
- hosts: localhost
|
||||
roles:
|
||||
- role: standard-test-basic
|
||||
tags:
|
||||
- classic
|
||||
repositories:
|
||||
- repo: "https://src.fedoraproject.org/tests/python.git"
|
||||
dest: "python"
|
||||
tests:
|
||||
- rpm_qa:
|
||||
run: rpm -qa
|
||||
- smoke:
|
||||
dir: python/smoke
|
||||
run: VERSION=3.9 ./venv.sh
|
||||
- selftest:
|
||||
dir: python/selftest
|
||||
run: VERSION=3.9 X="-x test_wsgiref" ./parallel.sh
|
||||
- marshalparser:
|
||||
dir: python/marshalparser
|
||||
run: VERSION=3.9 SAMPLE=10 test_marshalparser_compatibility.sh
|
||||
required_packages:
|
||||
- gcc # for extension building in venv and selftest
|
||||
- gdb # for test_gdb
|
||||
- python3.9 # the test subject
|
||||
- python3-tox # for venv tests
|
||||
- glibc-all-langpacks # for locale tests
|
||||
- marshalparser # for testing compatibility (magic numbers) with marshalparser
|
||||
- rpm # for debugging
|
||||
Loading…
Add table
Add a link
Reference in a new issue