Compare commits
No commits in common. 'c8' and 'c9-beta' have entirely different histories.
@ -1 +1 @@
|
|||||||
SOURCES/Python-3.12.6.tar.xz
|
SOURCES/Python-3.12.5.tar.xz
|
||||||
|
@ -1 +1 @@
|
|||||||
6d2bbe1603b01764c541608938766233bf56f780 SOURCES/Python-3.12.6.tar.xz
|
d9b83c17a717e1cbd3ab6bd14cfe3e508e6d87b2 SOURCES/Python-3.12.5.tar.xz
|
||||||
|
@ -0,0 +1,483 @@
|
|||||||
|
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 | 187 +++++++++++++++++-
|
||||||
|
...-10-20-15-28-08.gh-issue-102988.dStNO7.rst | 8 +
|
||||||
|
4 files changed, 344 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 6ba42491d6..6bd45200d8 100644
|
||||||
|
--- a/Doc/library/email.utils.rst
|
||||||
|
+++ b/Doc/library/email.utils.rst
|
||||||
|
@@ -58,13 +58,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')
|
||||||
|
|
||||||
|
@@ -82,12 +87,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
|
||||||
|
|
||||||
|
@@ -97,6 +105,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 1de547a011..e53abc8b84 100644
|
||||||
|
--- a/Lib/email/utils.py
|
||||||
|
+++ b/Lib/email/utils.py
|
||||||
|
@@ -48,6 +48,7 @@
|
||||||
|
specialsre = re.compile(r'[][\\()<>@,:;".]')
|
||||||
|
escapesre = re.compile(r'[\\"]')
|
||||||
|
|
||||||
|
+
|
||||||
|
def _has_surrogates(s):
|
||||||
|
"""Return True if s may contain 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):
|
||||||
|
@@ -205,16 +321,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 fc8d87974e..ef8aa0d53c 100644
|
||||||
|
--- a/Lib/test/test_email/test_email.py
|
||||||
|
+++ b/Lib/test/test_email/test_email.py
|
||||||
|
@@ -16,6 +16,7 @@
|
||||||
|
|
||||||
|
import email
|
||||||
|
import email.policy
|
||||||
|
+import email.utils
|
||||||
|
|
||||||
|
from email.charset import Charset
|
||||||
|
from email.generator import Generator, DecodedGenerator, BytesGenerator
|
||||||
|
@@ -3352,15 +3353,137 @@ def test_getaddresses_comma_in_name(self):
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
+ 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"""
|
||||||
|
@@ -3551,6 +3674,54 @@ def test_mime_classes_policy_argument(self):
|
||||||
|
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,121 @@
|
|||||||
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||||
|
From: "Miss Islington (bot)"
|
||||||
|
<31488909+miss-islington@users.noreply.github.com>
|
||||||
|
Date: Mon, 12 Aug 2024 02:35:17 +0200
|
||||||
|
Subject: [PATCH] 00436: [CVE-2024-8088] gh-122905: Sanitize names in
|
||||||
|
zipfile.Path.
|
||||||
|
|
||||||
|
---
|
||||||
|
Lib/test/test_zipfile/_path/test_path.py | 17 +++++
|
||||||
|
Lib/zipfile/_path/__init__.py | 64 ++++++++++++++++++-
|
||||||
|
...-08-11-14-08-04.gh-issue-122905.7tDsxA.rst | 1 +
|
||||||
|
3 files changed, 81 insertions(+), 1 deletion(-)
|
||||||
|
create mode 100644 Misc/NEWS.d/next/Library/2024-08-11-14-08-04.gh-issue-122905.7tDsxA.rst
|
||||||
|
|
||||||
|
diff --git a/Lib/test/test_zipfile/_path/test_path.py b/Lib/test/test_zipfile/_path/test_path.py
|
||||||
|
index 06d5aab69b..90885dbbe3 100644
|
||||||
|
--- a/Lib/test/test_zipfile/_path/test_path.py
|
||||||
|
+++ b/Lib/test/test_zipfile/_path/test_path.py
|
||||||
|
@@ -577,3 +577,20 @@ def test_getinfo_missing(self, alpharep):
|
||||||
|
zipfile.Path(alpharep)
|
||||||
|
with self.assertRaises(KeyError):
|
||||||
|
alpharep.getinfo('does-not-exist')
|
||||||
|
+
|
||||||
|
+ def test_malformed_paths(self):
|
||||||
|
+ """
|
||||||
|
+ Path should handle malformed paths.
|
||||||
|
+ """
|
||||||
|
+ data = io.BytesIO()
|
||||||
|
+ zf = zipfile.ZipFile(data, "w")
|
||||||
|
+ zf.writestr("/one-slash.txt", b"content")
|
||||||
|
+ zf.writestr("//two-slash.txt", b"content")
|
||||||
|
+ zf.writestr("../parent.txt", b"content")
|
||||||
|
+ zf.filename = ''
|
||||||
|
+ root = zipfile.Path(zf)
|
||||||
|
+ assert list(map(str, root.iterdir())) == [
|
||||||
|
+ 'one-slash.txt',
|
||||||
|
+ 'two-slash.txt',
|
||||||
|
+ 'parent.txt',
|
||||||
|
+ ]
|
||||||
|
diff --git a/Lib/zipfile/_path/__init__.py b/Lib/zipfile/_path/__init__.py
|
||||||
|
index 78c413563b..42f9fded21 100644
|
||||||
|
--- a/Lib/zipfile/_path/__init__.py
|
||||||
|
+++ b/Lib/zipfile/_path/__init__.py
|
||||||
|
@@ -83,7 +83,69 @@ def __setstate__(self, state):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
-class CompleteDirs(InitializedState, zipfile.ZipFile):
|
||||||
|
+class SanitizedNames:
|
||||||
|
+ """
|
||||||
|
+ ZipFile mix-in to ensure names are sanitized.
|
||||||
|
+ """
|
||||||
|
+
|
||||||
|
+ def namelist(self):
|
||||||
|
+ return list(map(self._sanitize, super().namelist()))
|
||||||
|
+
|
||||||
|
+ @staticmethod
|
||||||
|
+ def _sanitize(name):
|
||||||
|
+ r"""
|
||||||
|
+ Ensure a relative path with posix separators and no dot names.
|
||||||
|
+
|
||||||
|
+ Modeled after
|
||||||
|
+ https://github.com/python/cpython/blob/bcc1be39cb1d04ad9fc0bd1b9193d3972835a57c/Lib/zipfile/__init__.py#L1799-L1813
|
||||||
|
+ but provides consistent cross-platform behavior.
|
||||||
|
+
|
||||||
|
+ >>> san = SanitizedNames._sanitize
|
||||||
|
+ >>> san('/foo/bar')
|
||||||
|
+ 'foo/bar'
|
||||||
|
+ >>> san('//foo.txt')
|
||||||
|
+ 'foo.txt'
|
||||||
|
+ >>> san('foo/.././bar.txt')
|
||||||
|
+ 'foo/bar.txt'
|
||||||
|
+ >>> san('foo../.bar.txt')
|
||||||
|
+ 'foo../.bar.txt'
|
||||||
|
+ >>> san('\\foo\\bar.txt')
|
||||||
|
+ 'foo/bar.txt'
|
||||||
|
+ >>> san('D:\\foo.txt')
|
||||||
|
+ 'D/foo.txt'
|
||||||
|
+ >>> san('\\\\server\\share\\file.txt')
|
||||||
|
+ 'server/share/file.txt'
|
||||||
|
+ >>> san('\\\\?\\GLOBALROOT\\Volume3')
|
||||||
|
+ '?/GLOBALROOT/Volume3'
|
||||||
|
+ >>> san('\\\\.\\PhysicalDrive1\\root')
|
||||||
|
+ 'PhysicalDrive1/root'
|
||||||
|
+
|
||||||
|
+ Retain any trailing slash.
|
||||||
|
+ >>> san('abc/')
|
||||||
|
+ 'abc/'
|
||||||
|
+
|
||||||
|
+ Raises a ValueError if the result is empty.
|
||||||
|
+ >>> san('../..')
|
||||||
|
+ Traceback (most recent call last):
|
||||||
|
+ ...
|
||||||
|
+ ValueError: Empty filename
|
||||||
|
+ """
|
||||||
|
+
|
||||||
|
+ def allowed(part):
|
||||||
|
+ return part and part not in {'..', '.'}
|
||||||
|
+
|
||||||
|
+ # Remove the drive letter.
|
||||||
|
+ # Don't use ntpath.splitdrive, because that also strips UNC paths
|
||||||
|
+ bare = re.sub('^([A-Z]):', r'\1', name, flags=re.IGNORECASE)
|
||||||
|
+ clean = bare.replace('\\', '/')
|
||||||
|
+ parts = clean.split('/')
|
||||||
|
+ joined = '/'.join(filter(allowed, parts))
|
||||||
|
+ if not joined:
|
||||||
|
+ raise ValueError("Empty filename")
|
||||||
|
+ return joined + '/' * name.endswith('/')
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+class CompleteDirs(InitializedState, SanitizedNames, zipfile.ZipFile):
|
||||||
|
"""
|
||||||
|
A ZipFile subclass that ensures that implied directories
|
||||||
|
are always included in the namelist.
|
||||||
|
diff --git a/Misc/NEWS.d/next/Library/2024-08-11-14-08-04.gh-issue-122905.7tDsxA.rst b/Misc/NEWS.d/next/Library/2024-08-11-14-08-04.gh-issue-122905.7tDsxA.rst
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000000..1be44c906c
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/Misc/NEWS.d/next/Library/2024-08-11-14-08-04.gh-issue-122905.7tDsxA.rst
|
||||||
|
@@ -0,0 +1 @@
|
||||||
|
+:class:`zipfile.Path` objects now sanitize names from the zipfile.
|
@ -0,0 +1,18 @@
|
|||||||
|
-----BEGIN PGP SIGNATURE-----
|
||||||
|
|
||||||
|
iQKTBAABCgB9FiEEcWlgX2LHUTVtBUomqCHmgOX6YwUFAmayiFtfFIAAAAAALgAo
|
||||||
|
aXNzdWVyLWZwckBub3RhdGlvbnMub3BlbnBncC5maWZ0aGhvcnNlbWFuLm5ldDcx
|
||||||
|
Njk2MDVGNjJDNzUxMzU2RDA1NEEyNkE4MjFFNjgwRTVGQTYzMDUACgkQqCHmgOX6
|
||||||
|
YwUr4g//VyVs9tvbtiSp8pGe8f1gYErEw54r124sL/CBuNii8Irts1j5ymGxcm+l
|
||||||
|
hshPK5UlqRnhd5dCJWFTvLTXa5Ko2R1L3JyyxfGd1hmDuMhrWsDHijI0R7L/mGM5
|
||||||
|
6X2LTaadBVNvk8HaNKvR8SEWvo68rdnOuYElFA9ir7uqwjO26ZWz9FfH80YDGwo8
|
||||||
|
Blef2NYw8rNhiaZMFV0HYV7D+YyUAZnFNfW8M7Fd4oskUyj1tD9J89T9FFLYN09d
|
||||||
|
BcCIf+EdiEfqRpKxH89bW2g52kDrm4jYGONtpyF8eruyS3YwYSbvbuWioBYKmlxC
|
||||||
|
s51mieXz6G325GTZnmPxLek3ywPv6Gil9y0wH3fIr2BsWsmXust4LBpjDGt56Fy6
|
||||||
|
seokGBg8xzsBSk3iEqNoFmNsy/QOiuCcDejX4XqBDNodOlETQPJb07TkTI2iOmg9
|
||||||
|
NG4Atiz1HvGVxK68UuK9IIcNHyaWUmH8h4VQFGvc6KV6feP5Nm21Y12PZ5XIqJBO
|
||||||
|
Y8M/VJIJ5koaNPQfnBbbI5YBkUr4BVpIXIpY5LM/L5sUo2C3R7hMi0VGK88HGfSQ
|
||||||
|
KV4JmZgf6RMBNmrWY12sryS1QQ6q3P110GTUGQWB3sxxNbhmfcrK+4viqHc83yDz
|
||||||
|
ifmk33HuqaQGU7OzUMHeNcoCJIPo3H1FpoHOn9wLLCtA1pT+as4=
|
||||||
|
=t0Rk
|
||||||
|
-----END PGP SIGNATURE-----
|
@ -1,18 +0,0 @@
|
|||||||
-----BEGIN PGP SIGNATURE-----
|
|
||||||
|
|
||||||
iQKTBAABCgB9FiEEcWlgX2LHUTVtBUomqCHmgOX6YwUFAmbbZv1fFIAAAAAALgAo
|
|
||||||
aXNzdWVyLWZwckBub3RhdGlvbnMub3BlbnBncC5maWZ0aGhvcnNlbWFuLm5ldDcx
|
|
||||||
Njk2MDVGNjJDNzUxMzU2RDA1NEEyNkE4MjFFNjgwRTVGQTYzMDUACgkQqCHmgOX6
|
|
||||||
YwV51g//WpQjF/Rt19lgaWojZ3qDkvmTM2kpvfDGLe9Tkm1fWYzji4TLS3TnzGEp
|
|
||||||
dw2K6ApqGX4aO9AKMBRdfiFyhaDp0ENlBzSspvyzVT4LsRxiWXqyJ1qZB9mui/S8
|
|
||||||
k5pw2qygaS4gYEAOLrVEwmQ52pig1wMAouSmRuopVk5DGYUN6Wir7RZMrYynsd6P
|
|
||||||
6HYqpZby2L1fKlcj2xYY44niuL5a+I8ucWN9qOBzRLCuzq20lVoII817vORjCqa7
|
|
||||||
ZUMKrDXDlzHnISNqZyyX37/oi6a8UdNm0o8V9yDJLiBu9+Dy3OueoIguuzimk4hq
|
|
||||||
ZnepBoCcr2YAxIsXvwl2qfQBOCjJ5WAZ/wzA/eQMo9Jn1TYRBwuMC1MmP0ylt7Me
|
|
||||||
/pS57bGuulkfPv9pMto7qc2lNpotBmAsfGJAJMczeEmyo5tAXnJUBE94JRmiLaR/
|
|
||||||
zwPmJB3O9uEQhEa8+cjx4+9bK6+YvAXkb9x92Wn70u+IaalPj8CRA7B45hy1KYHT
|
|
||||||
5s/ndwFFWThxqxH61oqjPvlZW5PWBC83yi/KovhgDWNL2G9CTusKevMqX/LMUAKz
|
|
||||||
M3mJU/24vUu9bJNxB2qsa3UeP8hbb6WN5LLQyxRsXPjVQ9iTeMWrQkz1mRGIK2Ec
|
|
||||||
OFbYH2SGa/RELds6MZWzvZuXIefCoyuc51WsXnc4LFr2ZGv4dAI=
|
|
||||||
=CK1W
|
|
||||||
-----END PGP SIGNATURE-----
|
|
@ -1,171 +0,0 @@
|
|||||||
'''Script to perform import of each module given to %%py_check_import
|
|
||||||
'''
|
|
||||||
import argparse
|
|
||||||
import importlib
|
|
||||||
import fnmatch
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import site
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
def read_modules_files(file_paths):
|
|
||||||
'''Read module names from the files (modules must be newline separated).
|
|
||||||
|
|
||||||
Return the module names list or, if no files were provided, an empty list.
|
|
||||||
'''
|
|
||||||
|
|
||||||
if not file_paths:
|
|
||||||
return []
|
|
||||||
|
|
||||||
modules = []
|
|
||||||
for file in file_paths:
|
|
||||||
file_contents = file.read_text()
|
|
||||||
modules.extend(file_contents.split())
|
|
||||||
return modules
|
|
||||||
|
|
||||||
|
|
||||||
def read_modules_from_cli(argv):
|
|
||||||
'''Read module names from command-line arguments (space or comma separated).
|
|
||||||
|
|
||||||
Return the module names list.
|
|
||||||
'''
|
|
||||||
|
|
||||||
if not argv:
|
|
||||||
return []
|
|
||||||
|
|
||||||
# %%py3_check_import allows to separate module list with comma or whitespace,
|
|
||||||
# we need to unify the output to a list of particular elements
|
|
||||||
modules_as_str = ' '.join(argv)
|
|
||||||
modules = re.split(r'[\s,]+', modules_as_str)
|
|
||||||
# Because of shell expansion in some less typical cases it may happen
|
|
||||||
# that a trailing space will occur at the end of the list.
|
|
||||||
# Remove the empty items from the list before passing it further
|
|
||||||
modules = [m for m in modules if m]
|
|
||||||
return modules
|
|
||||||
|
|
||||||
|
|
||||||
def filter_top_level_modules_only(modules):
|
|
||||||
'''Filter out entries with nested modules (containing dot) ie. 'foo.bar'.
|
|
||||||
|
|
||||||
Return the list of top-level modules.
|
|
||||||
'''
|
|
||||||
|
|
||||||
return [module for module in modules if '.' not in module]
|
|
||||||
|
|
||||||
|
|
||||||
def any_match(text, globs):
|
|
||||||
'''Return True if any of given globs fnmatchcase's the given text.'''
|
|
||||||
|
|
||||||
return any(fnmatch.fnmatchcase(text, g) for g in globs)
|
|
||||||
|
|
||||||
|
|
||||||
def exclude_unwanted_module_globs(globs, modules):
|
|
||||||
'''Filter out entries which match the either of the globs given as argv.
|
|
||||||
|
|
||||||
Return the list of filtered modules.
|
|
||||||
'''
|
|
||||||
|
|
||||||
return [m for m in modules if not any_match(m, globs)]
|
|
||||||
|
|
||||||
|
|
||||||
def read_modules_from_all_args(args):
|
|
||||||
'''Return a joined list of modules from all given command-line arguments.
|
|
||||||
'''
|
|
||||||
|
|
||||||
modules = read_modules_files(args.filename)
|
|
||||||
modules.extend(read_modules_from_cli(args.modules))
|
|
||||||
if args.exclude:
|
|
||||||
modules = exclude_unwanted_module_globs(args.exclude, modules)
|
|
||||||
|
|
||||||
if args.top_level:
|
|
||||||
modules = filter_top_level_modules_only(modules)
|
|
||||||
|
|
||||||
# Error when someone accidentally managed to filter out everything
|
|
||||||
if len(modules) == 0:
|
|
||||||
raise ValueError('No modules to check were left')
|
|
||||||
|
|
||||||
return modules
|
|
||||||
|
|
||||||
|
|
||||||
def import_modules(modules):
|
|
||||||
'''Procedure to perform import check for each module name from the given list of modules.
|
|
||||||
'''
|
|
||||||
|
|
||||||
for module in modules:
|
|
||||||
print('Check import:', module, file=sys.stderr)
|
|
||||||
importlib.import_module(module)
|
|
||||||
|
|
||||||
|
|
||||||
def argparser():
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description='Generate list of all importable modules for import check.'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'modules', nargs='*',
|
|
||||||
help=('Add modules to check the import (space or comma separated).'),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-f', '--filename', action='append', type=Path,
|
|
||||||
help='Add importable module names list from file.',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-t', '--top-level', action='store_true',
|
|
||||||
help='Check only top-level modules.',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'-e', '--exclude', action='append',
|
|
||||||
help='Provide modules globs to be excluded from the check.',
|
|
||||||
)
|
|
||||||
return parser
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def remove_unwanteds_from_sys_path():
|
|
||||||
'''Remove cwd and this script's parent from sys.path for the import test.
|
|
||||||
Bring the original contents back after import is done (or failed)
|
|
||||||
'''
|
|
||||||
|
|
||||||
cwd_absolute = Path.cwd().absolute()
|
|
||||||
this_file_parent = Path(__file__).parent.absolute()
|
|
||||||
old_sys_path = list(sys.path)
|
|
||||||
for path in old_sys_path:
|
|
||||||
if Path(path).absolute() in (cwd_absolute, this_file_parent):
|
|
||||||
sys.path.remove(path)
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
sys.path = old_sys_path
|
|
||||||
|
|
||||||
|
|
||||||
def addsitedirs_from_environ():
|
|
||||||
'''Load directories from the _PYTHONSITE environment variable (separated by :)
|
|
||||||
and load the ones already present in sys.path via site.addsitedir()
|
|
||||||
to handle .pth files in them.
|
|
||||||
|
|
||||||
This is needed to properly import old-style namespace packages with nspkg.pth files.
|
|
||||||
See https://bugzilla.redhat.com/2018551 for a more detailed rationale.'''
|
|
||||||
for path in os.getenv('_PYTHONSITE', '').split(':'):
|
|
||||||
if path in sys.path:
|
|
||||||
site.addsitedir(path)
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
|
|
||||||
cli_args = argparser().parse_args(argv)
|
|
||||||
|
|
||||||
if not cli_args.modules and not cli_args.filename:
|
|
||||||
raise ValueError('No modules to check were provided')
|
|
||||||
|
|
||||||
modules = read_modules_from_all_args(cli_args)
|
|
||||||
|
|
||||||
with remove_unwanteds_from_sys_path():
|
|
||||||
addsitedirs_from_environ()
|
|
||||||
import_modules(modules)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,91 +0,0 @@
|
|||||||
%__python3 /usr/bin/python3.12
|
|
||||||
%python3_pkgversion 3.12
|
|
||||||
|
|
||||||
# The following are macros from macros.python3 in Fedora that are newer/different than those in the python3-rpm-macros package in RHEL 8.
|
|
||||||
# These macros overwrite/supercede some of the macros in the python3-rpm-macros package in RHEL.
|
|
||||||
|
|
||||||
# nb: $RPM_BUILD_ROOT is not set when the macros are expanded (at spec parse time)
|
|
||||||
# so we set it manually (to empty string), making our Python prefer the correct install scheme location
|
|
||||||
# platbase/base is explicitly set to %%{_prefix} to support custom values, such as /app for flatpaks
|
|
||||||
%python3_sitelib %(RPM_BUILD_ROOT= %{__python3} -Ic "import sysconfig; print(sysconfig.get_path('purelib', vars={'platbase': '%{_prefix}', 'base': '%{_prefix}'}))")
|
|
||||||
%python3_sitearch %(RPM_BUILD_ROOT= %{__python3} -Ic "import sysconfig; print(sysconfig.get_path('platlib', vars={'platbase': '%{_prefix}', 'base': '%{_prefix}'}))")
|
|
||||||
%python3_version %(RPM_BUILD_ROOT= %{__python3} -Ic "import sys; sys.stdout.write('{0.major}.{0.minor}'.format(sys.version_info))")
|
|
||||||
%python3_version_nodots %(RPM_BUILD_ROOT= %{__python3} -Ic "import sys; sys.stdout.write('{0.major}{0.minor}'.format(sys.version_info))")
|
|
||||||
%python3_platform %(RPM_BUILD_ROOT= %{__python3} -Ic "import sysconfig; print(sysconfig.get_platform())")
|
|
||||||
%python3_platform_triplet %(RPM_BUILD_ROOT= %{__python3} -Ic "import sysconfig; print(sysconfig.get_config_var('MULTIARCH'))")
|
|
||||||
%python3_ext_suffix %(RPM_BUILD_ROOT= %{__python3} -Ic "import sysconfig; print(sysconfig.get_config_var('EXT_SUFFIX'))")
|
|
||||||
%python3_cache_tag %(RPM_BUILD_ROOT= %{__python3} -Ic "import sys; print(sys.implementation.cache_tag)")
|
|
||||||
|
|
||||||
%_py3_shebang_s s
|
|
||||||
%_py3_shebang_P %(RPM_BUILD_ROOT= %{__python3} -Ic "import sys; print('P' if hasattr(sys.flags, 'safe_path') else '')")
|
|
||||||
%py3_shbang_opts -%{?_py3_shebang_s}%{?_py3_shebang_P}
|
|
||||||
|
|
||||||
%py3_shebang_fix %{expand:\\\
|
|
||||||
if [ -z "%{?py3_shebang_flags}" ]; then
|
|
||||||
shebang_flags="-k"
|
|
||||||
else
|
|
||||||
shebang_flags="-ka%{py3_shebang_flags}"
|
|
||||||
fi
|
|
||||||
%{__python3} -B %{_rpmconfigdir}/redhat/pathfix_py3_12.py -pni %{__python3} $shebang_flags}
|
|
||||||
|
|
||||||
%py3_install() %{expand:\\\
|
|
||||||
CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\
|
|
||||||
%{__python3} %{py_setup} %{?py_setup_args} install -O1 --skip-build --root %{buildroot} --prefix %{_prefix} %{?*}
|
|
||||||
rm -rfv %{buildroot}%{_bindir}/__pycache__
|
|
||||||
}
|
|
||||||
|
|
||||||
%py3_install_egg() %{expand:\\\
|
|
||||||
mkdir -p %{buildroot}%{python3_sitelib}
|
|
||||||
%{__python3} -m easy_install -m --prefix %{buildroot}%{_prefix} -Z dist/*-py%{python3_version}.egg %{?*}
|
|
||||||
rm -rfv %{buildroot}%{_bindir}/__pycache__
|
|
||||||
}
|
|
||||||
|
|
||||||
%py3_install_wheel() %{expand:\\\
|
|
||||||
%{__python3} -m pip install -I dist/%{1} --root %{buildroot} --prefix %{_prefix} --no-deps --no-index --no-warn-script-location
|
|
||||||
rm -rfv %{buildroot}%{_bindir}/__pycache__
|
|
||||||
for distinfo in %{buildroot}%{python3_sitelib}/*.dist-info %{buildroot}%{python3_sitearch}/*.dist-info; do
|
|
||||||
if [ -f ${distinfo}/direct_url.json ]; then
|
|
||||||
rm -fv ${distinfo}/direct_url.json
|
|
||||||
sed -i '/direct_url.json/d' ${distinfo}/RECORD
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# With $PATH and $PYTHONPATH set to the %%buildroot,
|
|
||||||
# try to import the Python 3 module(s) given as command-line args or read from file (-f).
|
|
||||||
# Respect the custom values of %%py3_shebang_flags or set nothing if it's undefined.
|
|
||||||
# Filter and check import on only top-level modules using -t flag.
|
|
||||||
# Exclude unwanted modules by passing their globs to -e option.
|
|
||||||
# Useful as a smoke test in %%check when running tests is not feasible.
|
|
||||||
# Use spaces or commas as separators if providing list directly.
|
|
||||||
# Use newlines as separators if providing list in a file.
|
|
||||||
%py3_check_import(e:tf:) %{expand:\\\
|
|
||||||
PATH="%{buildroot}%{_bindir}:$PATH"\\\
|
|
||||||
PYTHONPATH="${PYTHONPATH:-%{buildroot}%{python3_sitearch}:%{buildroot}%{python3_sitelib}}"\\\
|
|
||||||
_PYTHONSITE="%{buildroot}%{python3_sitearch}:%{buildroot}%{python3_sitelib}"\\\
|
|
||||||
PYTHONDONTWRITEBYTECODE=1\\\
|
|
||||||
%{lua:
|
|
||||||
local command = "%{__python3} "
|
|
||||||
if rpm.expand("%{?py3_shebang_flags}") ~= "" then
|
|
||||||
command = command .. "-%{py3_shebang_flags}"
|
|
||||||
end
|
|
||||||
command = command .. " %{_rpmconfigdir}/redhat/import_all_modules_py3_12.py "
|
|
||||||
-- handle multiline arguments correctly, see https://bugzilla.redhat.com/2018809
|
|
||||||
local args=rpm.expand('%{?**}'):gsub("[%s\\\\]*%s+", " ")
|
|
||||||
print(command .. args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Environment variables used by %%pytest, %%tox or standalone, e.g.:
|
|
||||||
# %%{py3_test_envvars} %%{python3} -m unittest
|
|
||||||
%py3_test_envvars %{expand:\\\
|
|
||||||
CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}"\\\
|
|
||||||
PATH="%{buildroot}%{_bindir}:$PATH"\\\
|
|
||||||
PYTHONPATH="${PYTHONPATH:-%{buildroot}%{python3_sitearch}:%{buildroot}%{python3_sitelib}}"\\\
|
|
||||||
PYTHONDONTWRITEBYTECODE=1\\\
|
|
||||||
%{?__pytest_addopts:PYTEST_ADDOPTS="${PYTEST_ADDOPTS:-} %{__pytest_addopts}"}\\\
|
|
||||||
PYTEST_XDIST_AUTO_NUM_WORKERS=%{_smp_build_ncpus}}
|
|
||||||
|
|
||||||
# This is intended for Python 3 only, hence also no Python version in the name.
|
|
||||||
%__pytest /usr/bin/pytest-%{python3_version}
|
|
||||||
%pytest %py3_test_envvars %__pytest
|
|
@ -1,199 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from stat import *
|
|
||||||
import getopt
|
|
||||||
|
|
||||||
err = sys.stderr.write
|
|
||||||
dbg = err
|
|
||||||
rep = sys.stdout.write
|
|
||||||
|
|
||||||
new_interpreter = None
|
|
||||||
preserve_timestamps = False
|
|
||||||
create_backup = True
|
|
||||||
keep_flags = False
|
|
||||||
add_flags = b''
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
global new_interpreter
|
|
||||||
global preserve_timestamps
|
|
||||||
global create_backup
|
|
||||||
global keep_flags
|
|
||||||
global add_flags
|
|
||||||
|
|
||||||
usage = ('usage: %s -i /interpreter -p -n -k -a file-or-directory ...\n' %
|
|
||||||
sys.argv[0])
|
|
||||||
try:
|
|
||||||
opts, args = getopt.getopt(sys.argv[1:], 'i:a:kpn')
|
|
||||||
except getopt.error as msg:
|
|
||||||
err(str(msg) + '\n')
|
|
||||||
err(usage)
|
|
||||||
sys.exit(2)
|
|
||||||
for o, a in opts:
|
|
||||||
if o == '-i':
|
|
||||||
new_interpreter = a.encode()
|
|
||||||
if o == '-p':
|
|
||||||
preserve_timestamps = True
|
|
||||||
if o == '-n':
|
|
||||||
create_backup = False
|
|
||||||
if o == '-k':
|
|
||||||
keep_flags = True
|
|
||||||
if o == '-a':
|
|
||||||
add_flags = a.encode()
|
|
||||||
if b' ' in add_flags:
|
|
||||||
err("-a option doesn't support whitespaces")
|
|
||||||
sys.exit(2)
|
|
||||||
if not new_interpreter or not new_interpreter.startswith(b'/') or \
|
|
||||||
not args:
|
|
||||||
err('-i option or file-or-directory missing\n')
|
|
||||||
err(usage)
|
|
||||||
sys.exit(2)
|
|
||||||
bad = 0
|
|
||||||
for arg in args:
|
|
||||||
if os.path.isdir(arg):
|
|
||||||
if recursedown(arg): bad = 1
|
|
||||||
elif os.path.islink(arg):
|
|
||||||
err(arg + ': will not process symbolic links\n')
|
|
||||||
bad = 1
|
|
||||||
else:
|
|
||||||
if fix(arg): bad = 1
|
|
||||||
sys.exit(bad)
|
|
||||||
|
|
||||||
|
|
||||||
def ispython(name):
|
|
||||||
return name.endswith('.py')
|
|
||||||
|
|
||||||
|
|
||||||
def recursedown(dirname):
|
|
||||||
dbg('recursedown(%r)\n' % (dirname,))
|
|
||||||
bad = 0
|
|
||||||
try:
|
|
||||||
names = os.listdir(dirname)
|
|
||||||
except OSError as msg:
|
|
||||||
err('%s: cannot list directory: %r\n' % (dirname, msg))
|
|
||||||
return 1
|
|
||||||
names.sort()
|
|
||||||
subdirs = []
|
|
||||||
for name in names:
|
|
||||||
if name in (os.curdir, os.pardir): continue
|
|
||||||
fullname = os.path.join(dirname, name)
|
|
||||||
if os.path.islink(fullname): pass
|
|
||||||
elif os.path.isdir(fullname):
|
|
||||||
subdirs.append(fullname)
|
|
||||||
elif ispython(name):
|
|
||||||
if fix(fullname): bad = 1
|
|
||||||
for fullname in subdirs:
|
|
||||||
if recursedown(fullname): bad = 1
|
|
||||||
return bad
|
|
||||||
|
|
||||||
|
|
||||||
def fix(filename):
|
|
||||||
## dbg('fix(%r)\n' % (filename,))
|
|
||||||
try:
|
|
||||||
f = open(filename, 'rb')
|
|
||||||
except IOError as msg:
|
|
||||||
err('%s: cannot open: %r\n' % (filename, msg))
|
|
||||||
return 1
|
|
||||||
with f:
|
|
||||||
line = f.readline()
|
|
||||||
fixed = fixline(line)
|
|
||||||
if line == fixed:
|
|
||||||
rep(filename+': no change\n')
|
|
||||||
return
|
|
||||||
head, tail = os.path.split(filename)
|
|
||||||
tempname = os.path.join(head, '@' + tail)
|
|
||||||
try:
|
|
||||||
g = open(tempname, 'wb')
|
|
||||||
except IOError as msg:
|
|
||||||
err('%s: cannot create: %r\n' % (tempname, msg))
|
|
||||||
return 1
|
|
||||||
with g:
|
|
||||||
rep(filename + ': updating\n')
|
|
||||||
g.write(fixed)
|
|
||||||
BUFSIZE = 8*1024
|
|
||||||
while 1:
|
|
||||||
buf = f.read(BUFSIZE)
|
|
||||||
if not buf: break
|
|
||||||
g.write(buf)
|
|
||||||
|
|
||||||
# Finishing touch -- move files
|
|
||||||
|
|
||||||
mtime = None
|
|
||||||
atime = None
|
|
||||||
# First copy the file's mode to the temp file
|
|
||||||
try:
|
|
||||||
statbuf = os.stat(filename)
|
|
||||||
mtime = statbuf.st_mtime
|
|
||||||
atime = statbuf.st_atime
|
|
||||||
os.chmod(tempname, statbuf[ST_MODE] & 0o7777)
|
|
||||||
except OSError as msg:
|
|
||||||
err('%s: warning: chmod failed (%r)\n' % (tempname, msg))
|
|
||||||
# Then make a backup of the original file as filename~
|
|
||||||
if create_backup:
|
|
||||||
try:
|
|
||||||
os.rename(filename, filename + '~')
|
|
||||||
except OSError as msg:
|
|
||||||
err('%s: warning: backup failed (%r)\n' % (filename, msg))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
os.remove(filename)
|
|
||||||
except OSError as msg:
|
|
||||||
err('%s: warning: removing failed (%r)\n' % (filename, msg))
|
|
||||||
# Now move the temp file to the original file
|
|
||||||
try:
|
|
||||||
os.rename(tempname, filename)
|
|
||||||
except OSError as msg:
|
|
||||||
err('%s: rename failed (%r)\n' % (filename, msg))
|
|
||||||
return 1
|
|
||||||
if preserve_timestamps:
|
|
||||||
if atime and mtime:
|
|
||||||
try:
|
|
||||||
os.utime(filename, (atime, mtime))
|
|
||||||
except OSError as msg:
|
|
||||||
err('%s: reset of timestamp failed (%r)\n' % (filename, msg))
|
|
||||||
return 1
|
|
||||||
# Return success
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
def parse_shebang(shebangline):
|
|
||||||
shebangline = shebangline.rstrip(b'\n')
|
|
||||||
start = shebangline.find(b' -')
|
|
||||||
if start == -1:
|
|
||||||
return b''
|
|
||||||
return shebangline[start:]
|
|
||||||
|
|
||||||
|
|
||||||
def populate_flags(shebangline):
|
|
||||||
old_flags = b''
|
|
||||||
if keep_flags:
|
|
||||||
old_flags = parse_shebang(shebangline)
|
|
||||||
if old_flags:
|
|
||||||
old_flags = old_flags[2:]
|
|
||||||
if not (old_flags or add_flags):
|
|
||||||
return b''
|
|
||||||
# On Linux, the entire string following the interpreter name
|
|
||||||
# is passed as a single argument to the interpreter.
|
|
||||||
# e.g. "#! /usr/bin/python3 -W Error -s" runs "/usr/bin/python3 "-W Error -s"
|
|
||||||
# so shebang should have single '-' where flags are given and
|
|
||||||
# flag might need argument for that reasons adding new flags is
|
|
||||||
# between '-' and original flags
|
|
||||||
# e.g. #! /usr/bin/python3 -sW Error
|
|
||||||
return b' -' + add_flags + old_flags
|
|
||||||
|
|
||||||
|
|
||||||
def fixline(line):
|
|
||||||
if not line.startswith(b'#!'):
|
|
||||||
return line
|
|
||||||
|
|
||||||
if b"python" not in line:
|
|
||||||
return line
|
|
||||||
|
|
||||||
flags = populate_flags(line)
|
|
||||||
return b'#! ' + new_interpreter + flags + b'\n'
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
Loading…
Reference in new issue