diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 51fbea6..ffa2c56 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -103,7 +103,7 @@ jobs: - uses: actions/checkout@v4.1.7 - name: Set up Python - uses: actions/setup-python@v5.1.1 + uses: actions/setup-python@v5.2.0 with: # Appending -dev ensures that we can always build the dev release. # It's a no-op for versions that have been published. @@ -115,7 +115,7 @@ jobs: make ${{ matrix.target }} BUILD_NUMBER=${{ needs.config.outputs.BUILD_NUMBER }} - name: Upload build artefacts - uses: actions/upload-artifact@v4.3.5 + uses: actions/upload-artifact@v4.4.0 with: name: Python-${{ needs.config.outputs.PYTHON_VER }}-${{ matrix.target }}-support.${{ needs.config.outputs.BUILD_NUMBER }}.tar.gz path: dist/Python-${{ needs.config.outputs.PYTHON_VER }}-${{ matrix.target }}-support.${{ needs.config.outputs.BUILD_NUMBER }}.tar.gz @@ -125,20 +125,15 @@ jobs: with: repository: beeware/Python-support-testbed path: Python-support-testbed - # TODO - remove the py3.13 reference option. - ref: py3.13-support - name: Install dependencies if: matrix.run-tests run: | - # TODO - Revert to the development version of Briefcase # Use the development version of Briefcase - # python -m pip install git+https://github.com/beeware/briefcase.git - python -m pip install git+https://github.com/freakboy3742/briefcase.git@version-bumps + python -m pip install git+https://github.com/beeware/briefcase.git - name: Run support testbed check if: matrix.run-tests timeout-minutes: 10 working-directory: Python-support-testbed - # TODO - remove the template_branch option. - run: briefcase run ${{ matrix.target }} Xcode --test ${{ matrix.briefcase-run-args }} -C support_package=\'../dist/Python-${{ needs.config.outputs.PYTHON_VER }}-${{ matrix.target }}-support.${{ needs.config.outputs.BUILD_NUMBER }}.tar.gz\' -C template_branch=\'framework-lib\' + run: briefcase run ${{ matrix.target }} Xcode --test ${{ matrix.briefcase-run-args }} -C support_package=\'../dist/Python-${{ needs.config.outputs.PYTHON_VER }}-${{ matrix.target }}-support.${{ needs.config.outputs.BUILD_NUMBER }}.tar.gz\' diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index e1b09da..e90bf31 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python environment - uses: actions/setup-python@v5.1.1 + uses: actions/setup-python@v5.2.0 with: python-version: "3.X" diff --git a/Makefile b/Makefile index 1748922..7185743 100644 --- a/Makefile +++ b/Makefile @@ -28,8 +28,8 @@ PYTHON_VER=$(basename $(PYTHON_VERSION)) # https://github.com/beeware/cpython-apple-source-deps/releases BZIP2_VERSION=1.0.8-1 LIBFFI_VERSION=3.4.6-1 -OPENSSL_VERSION=3.0.14-1 -XZ_VERSION=5.4.7-1 +OPENSSL_VERSION=3.0.15-1 +XZ_VERSION=5.6.2-1 # Supported OS OS_LIST=macOS iOS tvOS watchOS @@ -151,7 +151,7 @@ downloads/bzip2-$(BZIP2_VERSION)-$(target).tar.gz: $$(BZIP2_LIB-$(target)): downloads/bzip2-$(BZIP2_VERSION)-$(target).tar.gz @echo ">>> Install BZip2 for $(target)" mkdir -p $$(BZIP2_INSTALL-$(target)) - cd $$(BZIP2_INSTALL-$(target)) && tar zxvf $(PROJECT_DIR)/downloads/bzip2-$(BZIP2_VERSION)-$(target).tar.gz + cd $$(BZIP2_INSTALL-$(target)) && tar zxvf $(PROJECT_DIR)/downloads/bzip2-$(BZIP2_VERSION)-$(target).tar.gz --exclude="*.dylib" # Ensure the target is marked as clean. touch $$(BZIP2_LIB-$(target)) @@ -171,7 +171,7 @@ downloads/xz-$(XZ_VERSION)-$(target).tar.gz: $$(XZ_LIB-$(target)): downloads/xz-$(XZ_VERSION)-$(target).tar.gz @echo ">>> Install XZ for $(target)" mkdir -p $$(XZ_INSTALL-$(target)) - cd $$(XZ_INSTALL-$(target)) && tar zxvf $(PROJECT_DIR)/downloads/xz-$(XZ_VERSION)-$(target).tar.gz + cd $$(XZ_INSTALL-$(target)) && tar zxvf $(PROJECT_DIR)/downloads/xz-$(XZ_VERSION)-$(target).tar.gz --exclude="*.dylib" # Ensure the target is marked as clean. touch $$(XZ_LIB-$(target)) @@ -191,7 +191,7 @@ downloads/openssl-$(OPENSSL_VERSION)-$(target).tar.gz: $$(OPENSSL_SSL_LIB-$(target)): downloads/openssl-$(OPENSSL_VERSION)-$(target).tar.gz @echo ">>> Install OpenSSL for $(target)" mkdir -p $$(OPENSSL_INSTALL-$(target)) - cd $$(OPENSSL_INSTALL-$(target)) && tar zxvf $(PROJECT_DIR)/downloads/openssl-$(OPENSSL_VERSION)-$(target).tar.gz + cd $$(OPENSSL_INSTALL-$(target)) && tar zxvf $(PROJECT_DIR)/downloads/openssl-$(OPENSSL_VERSION)-$(target).tar.gz --exclude="*.dylib" # Ensure the target is marked as clean. touch $$(OPENSSL_SSL_LIB-$(target)) @@ -216,7 +216,7 @@ downloads/libffi-$(LIBFFI_VERSION)-$(target).tar.gz: $$(LIBFFI_LIB-$(target)): downloads/libffi-$(LIBFFI_VERSION)-$(target).tar.gz @echo ">>> Install libFFI for $(target)" mkdir -p $$(LIBFFI_INSTALL-$(target)) - cd $$(LIBFFI_INSTALL-$(target)) && tar zxvf $(PROJECT_DIR)/downloads/libffi-$(LIBFFI_VERSION)-$(target).tar.gz + cd $$(LIBFFI_INSTALL-$(target)) && tar zxvf $(PROJECT_DIR)/downloads/libffi-$(LIBFFI_VERSION)-$(target).tar.gz --exclude="*.dylib" # Ensure the target is marked as clean. touch $$(LIBFFI_LIB-$(target)) diff --git a/README.rst b/README.rst index 9aa8070..0716242 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ Python Apple Support This is a meta-package for building a version of Python that can be embedded into a macOS, iOS, tvOS or watchOS project. -**This branch builds a packaged version of Python 3.10.13**. +**This branch builds a packaged version of Python 3.10**. Other Python versions are available by cloning other branches of the main repository: diff --git a/patch/Python/Python.patch b/patch/Python/Python.patch index eddc4c9..0cd79de 100644 --- a/patch/Python/Python.patch +++ b/patch/Python/Python.patch @@ -1,3 +1,129 @@ +diff --git a/Doc/library/asyncio.rst b/Doc/library/asyncio.rst +index 8a6526ed902..242206d6734 100644 +--- a/Doc/library/asyncio.rst ++++ b/Doc/library/asyncio.rst +@@ -56,6 +56,10 @@ + * :ref:`bridge ` callback-based libraries and code + with async/await syntax. + ++.. _asyncio-cli: ++ ++.. rubric:: asyncio REPL ++ + You can experiment with an ``asyncio`` concurrent context in the REPL: + + .. code-block:: pycon +@@ -68,6 +72,11 @@ + >>> await asyncio.sleep(10, result='hello') + 'hello' + ++.. audit-event:: cpython.run_stdin "" "" ++ ++.. versionchanged:: 3.10.15 (also 3.9.20, and 3.8.20) ++ Emits audit events. ++ + .. We use the "rubric" directive here to avoid creating + the "Reference" subsection in the TOC. + +diff --git a/Doc/library/email.errors.rst b/Doc/library/email.errors.rst +index 194a98696f4..f737f0282c5 100644 +--- a/Doc/library/email.errors.rst ++++ b/Doc/library/email.errors.rst +@@ -59,6 +59,12 @@ + :class:`~email.mime.image.MIMEImage`). + + ++.. exception:: HeaderWriteError() ++ ++ Raised when an error occurs when the :mod:`~email.generator` outputs ++ headers. ++ ++ + Here is the list of the defects that the :class:`~email.parser.FeedParser` + can find while parsing messages. Note that the defects are added to the message + where the problem was found, so for example, if a message nested inside a +diff --git a/Doc/library/email.policy.rst b/Doc/library/email.policy.rst +index bf53b9520fc..eba43b5169d 100644 +--- a/Doc/library/email.policy.rst ++++ b/Doc/library/email.policy.rst +@@ -229,6 +229,24 @@ + + .. versionadded:: 3.6 + ++ ++ .. attribute:: verify_generated_headers ++ ++ If ``True`` (the default), the generator will raise ++ :exc:`~email.errors.HeaderWriteError` instead of writing a header ++ that is improperly folded or delimited, such that it would ++ be parsed as multiple headers or joined with adjacent data. ++ Such headers can be generated by custom header classes or bugs ++ in the ``email`` module. ++ ++ As it's a security feature, this defaults to ``True`` even in the ++ :class:`~email.policy.Compat32` policy. ++ For backwards compatible, but unsafe, behavior, it must be set to ++ ``False`` explicitly. ++ ++ .. versionadded:: 3.10.15 ++ ++ + The following :class:`Policy` method is intended to be called by code using + the email library to create policy instances with custom settings: + +diff --git a/Doc/library/email.utils.rst b/Doc/library/email.utils.rst +index 0e266b6a457..65948fb3d11 100644 +--- a/Doc/library/email.utils.rst ++++ b/Doc/library/email.utils.rst +@@ -60,13 +60,18 @@ + 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.10.15 ++ Add *strict* optional parameter and reject malformed inputs by default. ++ + + .. function:: formataddr(pair, charset='utf-8') + +@@ -84,12 +89,15 @@ + 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 `. Here's a simple +- example that gets all the recipients of a message:: ++ :meth:`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 @@ + resent_ccs = msg.get_all('resent-cc', []) + all_recipients = getaddresses(tos + ccs + resent_tos + resent_ccs) + ++ .. versionchanged:: 3.10.15 ++ Add *strict* optional parameter and reject malformed inputs by default. ++ + + .. function:: parsedate(date) + diff --git a/Doc/library/importlib.rst b/Doc/library/importlib.rst index 21c19903464..61baa722359 100644 --- a/Doc/library/importlib.rst @@ -72,8 +198,70 @@ index 21c19903464..61baa722359 100644 :mod:`importlib.util` -- Utility code for importers --------------------------------------------------- +diff --git a/Doc/library/ipaddress.rst b/Doc/library/ipaddress.rst +index 9c2dff55703..6f4520e32da 100644 +--- a/Doc/library/ipaddress.rst ++++ b/Doc/library/ipaddress.rst +@@ -188,18 +188,53 @@ + + .. attribute:: is_private + +- ``True`` if the address is allocated for private networks. See ++ ``True`` if the address is defined as not globally reachable by + iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_ +- (for IPv6). ++ (for IPv6) with the following exceptions: ++ ++ * ``is_private`` is ``False`` for the shared address space (``100.64.0.0/10``) ++ * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the ++ semantics of the underlying IPv4 addresses and the following condition holds ++ (see :attr:`IPv6Address.ipv4_mapped`):: ++ ++ address.is_private == address.ipv4_mapped.is_private ++ ++ ``is_private`` has value opposite to :attr:`is_global`, except for the shared address space ++ (``100.64.0.0/10`` range) where they are both ``False``. ++ ++ .. versionchanged:: 3.10.15 ++ ++ Fixed some false positives and false negatives. ++ ++ * ``192.0.0.0/24`` is considered private with the exception of ``192.0.0.9/32`` and ++ ``192.0.0.10/32`` (previously: only the ``192.0.0.0/29`` sub-range was considered private). ++ * ``64:ff9b:1::/48`` is considered private. ++ * ``2002::/16`` is considered private. ++ * There are exceptions within ``2001::/23`` (otherwise considered private): ``2001:1::1/128``, ++ ``2001:1::2/128``, ``2001:3::/32``, ``2001:4:112::/48``, ``2001:20::/28``, ``2001:30::/28``. ++ The exceptions are not considered private. + + .. attribute:: is_global + +- ``True`` if the address is allocated for public networks. See ++ ``True`` if the address is defined as globally reachable by + iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_ +- (for IPv6). ++ (for IPv6) with the following exception: ++ ++ For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the ++ semantics of the underlying IPv4 addresses and the following condition holds ++ (see :attr:`IPv6Address.ipv4_mapped`):: ++ ++ address.is_global == address.ipv4_mapped.is_global ++ ++ ``is_global`` has value opposite to :attr:`is_private`, except for the shared address space ++ (``100.64.0.0/10`` range) where they are both ``False``. + + .. versionadded:: 3.4 + ++ .. versionchanged:: 3.10.15 ++ ++ Fixed some false positives and false negatives, see :attr:`is_private` for details. ++ + .. attribute:: is_unspecified + + ``True`` if the address is unspecified. See :RFC:`5735` (for IPv4) diff --git a/Doc/library/os.rst b/Doc/library/os.rst -index 28990c216af..ac963c8f516 100644 +index 28990c216af..79db1891a1a 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -710,6 +710,11 @@ @@ -88,6 +276,27 @@ index 28990c216af..ac963c8f516 100644 .. availability:: recent flavors of Unix. .. versionchanged:: 3.3 +@@ -2065,6 +2070,10 @@ + platform-dependent. On some platforms, they are ignored and you should call + :func:`chmod` explicitly to set them. + ++ On Windows, a *mode* of ``0o700`` is specifically handled to apply access ++ control to the new directory such that only the current user and ++ administrators have access. Other values of *mode* are ignored. ++ + This function can also support :ref:`paths relative to directory descriptors + `. + +@@ -2079,6 +2088,9 @@ + .. versionchanged:: 3.6 + Accepts a :term:`path-like object`. + ++ .. versionchanged:: 3.10.15 ++ Windows now handles a *mode* of ``0o700``. ++ + + .. function:: makedirs(name, mode=0o777, exist_ok=False) + diff --git a/Doc/library/platform.rst b/Doc/library/platform.rst index dc2d871b47d..57dc421bb6c 100644 --- a/Doc/library/platform.rst @@ -144,6 +353,36 @@ index dc2d871b47d..57dc421bb6c 100644 Unix Platforms -------------- +diff --git a/Doc/library/subprocess.rst b/Doc/library/subprocess.rst +index fc66663546b..7eb9f304240 100644 +--- a/Doc/library/subprocess.rst ++++ b/Doc/library/subprocess.rst +@@ -741,8 +741,8 @@ + Security Considerations + ----------------------- + +-Unlike some other popen functions, this implementation will never +-implicitly call a system shell. This means that all characters, ++Unlike some other popen functions, this library will not ++implicitly choose to call a system shell. This means that all characters, + including shell metacharacters, can safely be passed to child processes. + If the shell is invoked explicitly, via ``shell=True``, it is the application's + responsibility to ensure that all whitespace and metacharacters are +@@ -751,6 +751,14 @@ + vulnerabilities. On :ref:`some platforms `, it is possible + to use :func:`shlex.quote` for this escaping. + ++On Windows, batch files (:file:`*.bat` or :file:`*.cmd`) may be launched by the ++operating system in a system shell regardless of the arguments passed to this ++library. This could result in arguments being parsed according to shell rules, ++but without any escaping added by Python. If you are intentionally launching a ++batch file with arguments from untrusted sources, consider passing ++``shell=True`` to allow Python to escape special characters. See :gh:`114539` ++for additional discussion. ++ + + Popen Objects + ------------- diff --git a/Doc/library/urllib.parse.rst b/Doc/library/urllib.parse.rst index 1e85602951c..8f5af980dee 100644 --- a/Doc/library/urllib.parse.rst @@ -210,6 +449,22 @@ index c0990882e58..e5cc007d2b8 100644 Here are some simple examples:: url = 'https://docs.python.org/' +diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst +index 303b884db21..4df6bf3c1d8 100644 +--- a/Doc/using/cmdline.rst ++++ b/Doc/using/cmdline.rst +@@ -626,6 +626,11 @@ + This variable can also be modified by Python code using :data:`os.environ` + to force inspect mode on program termination. + ++ .. audit-event:: cpython.run_stdin "" "" ++ ++ .. versionchanged:: 3.10.15 (also 3.9.20, and 3.8.20) ++ Emits audit events. ++ + + .. envvar:: PYTHONUNBUFFERED + diff --git a/Doc/using/configure.rst b/Doc/using/configure.rst index ab33e0a06f5..6fe63e9ce2d 100644 --- a/Doc/using/configure.rst @@ -323,6 +578,93 @@ index 9ae0270eaee..2bb14d88dc9 100644 Other Resources =============== +diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst +index 43da72aece9..177781ac288 100644 +--- a/Doc/whatsnew/3.10.rst ++++ b/Doc/whatsnew/3.10.rst +@@ -1247,6 +1247,13 @@ + and :data:`~os.O_NOFOLLOW_ANY` for macOS. + (Contributed by Dong-hee Na in :issue:`43106`.) + ++As of 3.10.15, :func:`os.mkdir` and :func:`os.makedirs` on Windows now support ++passing a *mode* value of ``0o700`` to apply access control to the new ++directory. This implicitly affects :func:`tempfile.mkdtemp` and is a ++mitigation for CVE-2024-4030. Other values for *mode* continue to be ++ignored. ++(Contributed by Steve Dower in :gh:`118486`.) ++ + os.path + ------- + +@@ -1399,6 +1406,14 @@ + module names. + (Contributed by Victor Stinner in :issue:`42955`.) + ++tempfile ++-------- ++ ++As of 3.10.15 on Windows, the default mode ``0o700`` used by ++:func:`tempfile.mkdtemp` now limits access to the new directory due to ++changes to :func:`os.mkdir`. This is a mitigation for CVE-2024-4030. ++(Contributed by Steve Dower in :gh:`118486`.) ++ + _thread + ------- + +@@ -2348,3 +2363,34 @@ + :exc:`DeprecationWarning`. + In Python 3.14, the default will switch to ``'data'``. + (Contributed by Petr Viktorin in :pep:`706`.) ++ ++Notable changes in 3.10.15 ++========================== ++ ++ipaddress ++--------- ++ ++* Fixed ``is_global`` and ``is_private`` behavior in ``IPv4Address``, ++ ``IPv6Address``, ``IPv4Network`` and ``IPv6Network``. ++ ++email ++----- ++ ++* Headers with embedded newlines are now quoted on output. ++ ++ The :mod:`~email.generator` will now refuse to serialize (write) headers ++ that are improperly folded or delimited, such that they would be parsed as ++ multiple headers or joined with adjacent data. ++ If you need to turn this safety feature off, ++ set :attr:`~email.policy.Policy.verify_generated_headers`. ++ (Contributed by Bas Bloemsaat and Petr Viktorin in :gh:`121650`.) ++ ++* :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. ++ An optional *strict* parameter was added to these two functions: ++ use ``strict=False`` to get the old behavior, accepting malformed inputs. ++ ``getattr(email.utils, 'supports_strict_parsing', False)`` can be used to ++ check if the *strict* paramater is available. ++ (Contributed by Thomas Dwyer and Victor Stinner for :gh:`102988` to improve ++ the CVE-2023-27043 fix.) +diff --git a/Include/patchlevel.h b/Include/patchlevel.h +index 61bf1c087db..6d50b2fda26 100644 +--- a/Include/patchlevel.h ++++ b/Include/patchlevel.h +@@ -18,12 +18,12 @@ + /*--start constants--*/ + #define PY_MAJOR_VERSION 3 + #define PY_MINOR_VERSION 10 +-#define PY_MICRO_VERSION 14 ++#define PY_MICRO_VERSION 15 + #define PY_RELEASE_LEVEL PY_RELEASE_LEVEL_FINAL + #define PY_RELEASE_SERIAL 0 + + /* Version as a string */ +-#define PY_VERSION "3.10.14" ++#define PY_VERSION "3.10.15" + /*--end constants--*/ + + /* Version as a single 4-byte hex number, e.g. 0x010502B2 == 1.5.2b2. --- /dev/null +++ b/Lib/_ios_support.py @@ -0,0 +1,71 @@ @@ -397,6 +739,44 @@ index 9ae0270eaee..2bb14d88dc9 100644 + model = objc.objc_msgSend(device_model, SEL_UTF8String).decode() + + return system, release, model, is_simulator +diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py +index 18bb87a5bc4..73330f4ac3f 100644 +--- a/Lib/asyncio/__main__.py ++++ b/Lib/asyncio/__main__.py +@@ -90,6 +90,8 @@ + + + if __name__ == '__main__': ++ sys.audit("cpython.run_stdin") ++ + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + +diff --git a/Lib/asyncio/windows_events.py b/Lib/asyncio/windows_events.py +index 3204c7c9be9..5aed825588b 100644 +--- a/Lib/asyncio/windows_events.py ++++ b/Lib/asyncio/windows_events.py +@@ -323,13 +323,13 @@ + if self._self_reading_future is not None: + ov = self._self_reading_future._ov + self._self_reading_future.cancel() +- # self_reading_future was just cancelled so if it hasn't been +- # finished yet, it never will be (it's possible that it has +- # already finished and its callback is waiting in the queue, +- # where it could still happen if the event loop is restarted). +- # Unregister it otherwise IocpProactor.close will wait for it +- # forever +- if ov is not None: ++ # self_reading_future always uses IOCP, so even though it's ++ # been cancelled, we need to make sure that the IOCP message ++ # is received so that the kernel is not holding on to the ++ # memory, possibly causing memory corruption later. Only ++ # unregister it if IO is complete in all respects. Otherwise ++ # we need another _poll() later to complete the IO. ++ if ov is not None and not ov.pending: + self._proactor._unregister(ov) + self._self_reading_future = None + diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index 4afa4ebd422..dca2081bea4 100644 --- a/Lib/ctypes/__init__.py @@ -563,6 +943,349 @@ index 2ce5c5b64d6..e927f4af938 100644 try: import pwd os.environ['HOME'] = pwd.getpwuid(os.getuid())[5] +diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py +index e637e6df066..e1b99d5b417 100644 +--- a/Lib/email/_header_value_parser.py ++++ b/Lib/email/_header_value_parser.py +@@ -92,6 +92,8 @@ + ASPECIALS = TSPECIALS | set("*'%") + ATTRIBUTE_ENDS = ASPECIALS | WSP + EXTENDED_ATTRIBUTE_ENDS = ATTRIBUTE_ENDS - set('%') ++NLSET = {'\n', '\r'} ++SPECIALSNL = SPECIALS | NLSET + + def quote_string(value): + return '"'+str(value).replace('\\', '\\\\').replace('"', r'\"')+'"' +@@ -2778,9 +2780,13 @@ + wrap_as_ew_blocked -= 1 + continue + tstr = str(part) +- if part.token_type == 'ptext' and set(tstr) & SPECIALS: +- # Encode if tstr contains special characters. +- want_encoding = True ++ if not want_encoding: ++ if part.token_type == 'ptext': ++ # Encode if tstr contains special characters. ++ want_encoding = not SPECIALSNL.isdisjoint(tstr) ++ else: ++ # Encode if tstr contains newlines. ++ want_encoding = not NLSET.isdisjoint(tstr) + try: + tstr.encode(encoding) + charset = encoding +diff --git a/Lib/email/_policybase.py b/Lib/email/_policybase.py +index c9cbadd2a80..d1f48211f90 100644 +--- a/Lib/email/_policybase.py ++++ b/Lib/email/_policybase.py +@@ -157,6 +157,13 @@ + message_factory -- the class to use to create new message objects. + If the value is None, the default is Message. + ++ verify_generated_headers ++ -- if true, the generator verifies that each header ++ they are properly folded, so that a parser won't ++ treat it as multiple headers, start-of-body, or ++ part of another header. ++ This is a check against custom Header & fold() ++ implementations. + """ + + raise_on_defect = False +@@ -165,6 +172,7 @@ + max_line_length = 78 + mangle_from_ = False + message_factory = None ++ verify_generated_headers = True + + def handle_defect(self, obj, defect): + """Based on policy, either raise defect or call register_defect. +diff --git a/Lib/email/errors.py b/Lib/email/errors.py +index 3ad00565549..02aa5eced6a 100644 +--- a/Lib/email/errors.py ++++ b/Lib/email/errors.py +@@ -29,6 +29,10 @@ + """An illegal charset was given.""" + + ++class HeaderWriteError(MessageError): ++ """Error while writing headers.""" ++ ++ + # These are parsing defects which the parser was able to work around. + class MessageDefect(ValueError): + """Base class for a message defect.""" +diff --git a/Lib/email/generator.py b/Lib/email/generator.py +index c9b121624e0..89224ae41cb 100644 +--- a/Lib/email/generator.py ++++ b/Lib/email/generator.py +@@ -14,12 +14,14 @@ + from copy import deepcopy + from io import StringIO, BytesIO + from email.utils import _has_surrogates ++from email.errors import HeaderWriteError + + UNDERSCORE = '_' + NL = '\n' # XXX: no longer used by the code below. + + NLCRE = re.compile(r'\r\n|\r|\n') + fcre = re.compile(r'^From ', re.MULTILINE) ++NEWLINE_WITHOUT_FWSP = re.compile(r'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]') + + + +@@ -223,7 +225,16 @@ + + def _write_headers(self, msg): + for h, v in msg.raw_items(): +- self.write(self.policy.fold(h, v)) ++ folded = self.policy.fold(h, v) ++ if self.policy.verify_generated_headers: ++ linesep = self.policy.linesep ++ if not folded.endswith(self.policy.linesep): ++ raise HeaderWriteError( ++ f'folded header does not end with {linesep!r}: {folded!r}') ++ if NEWLINE_WITHOUT_FWSP.search(folded.removesuffix(linesep)): ++ raise HeaderWriteError( ++ f'folded header contains newline: {folded!r}') ++ self.write(folded) + # A blank line always separates headers from body + self.write(self._NL) + +diff --git a/Lib/email/utils.py b/Lib/email/utils.py +index cfdfeb3f1a8..9522341fabe 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 contains surrogate-escaped binary data.""" + # This check is based on the fact that unless there are surrogates, utf8 +@@ -106,12 +107,127 @@ + 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 _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) + +-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 ++ ++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 ']) ++ # 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 @@ + 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/http/cookies.py b/Lib/http/cookies.py +index 35ac2dc6ae2..2c1f021d0ab 100644 +--- a/Lib/http/cookies.py ++++ b/Lib/http/cookies.py +@@ -184,8 +184,13 @@ + return '"' + str.translate(_Translator) + '"' + + +-_OctalPatt = re.compile(r"\\[0-3][0-7][0-7]") +-_QuotePatt = re.compile(r"[\\].") ++_unquote_sub = re.compile(r'\\(?:([0-3][0-7][0-7])|(.))').sub ++ ++def _unquote_replace(m): ++ if m[1]: ++ return chr(int(m[1], 8)) ++ else: ++ return m[2] + + def _unquote(str): + # If there aren't any doublequotes, +@@ -205,30 +210,7 @@ + # \012 --> \n + # \" --> " + # +- i = 0 +- n = len(str) +- res = [] +- while 0 <= i < n: +- o_match = _OctalPatt.search(str, i) +- q_match = _QuotePatt.search(str, i) +- if not o_match and not q_match: # Neither matched +- res.append(str[i:]) +- break +- # else: +- j = k = -1 +- if o_match: +- j = o_match.start(0) +- if q_match: +- k = q_match.start(0) +- if q_match and (not o_match or k < j): # QuotePatt matched +- res.append(str[i:k]) +- res.append(str[k+1]) +- i = k + 2 +- else: # OctalPatt matched +- res.append(str[i:j]) +- res.append(chr(int(str[j+1:j+4], 8))) +- i = j + 4 +- return _nulljoin(res) ++ return _unquote_sub(_unquote_replace, str) + + # The _getdate() routine is used to set the expiration time in the cookie's HTTP + # header. By default, _getdate() returns the current time in the appropriate diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index 49bcaea78d7..94052fc72e4 100644 --- a/Lib/importlib/_bootstrap_external.py @@ -698,6 +1421,173 @@ index 2999a6019e0..d28a98539d5 100644 raise OSError('source code not available') module = getmodule(object, file) +diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py +index 756f1bc38c9..94b1761b5f7 100644 +--- a/Lib/ipaddress.py ++++ b/Lib/ipaddress.py +@@ -1323,18 +1323,41 @@ + @property + @functools.lru_cache() + def is_private(self): +- """Test if this address is allocated for private networks. ++ """``True`` if the address is defined as not globally reachable by ++ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_ ++ (for IPv6) with the following exceptions: + +- Returns: +- A boolean, True if the address is reserved per +- iana-ipv4-special-registry. ++ * ``is_private`` is ``False`` for ``100.64.0.0/10`` ++ * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the ++ semantics of the underlying IPv4 addresses and the following condition holds ++ (see :attr:`IPv6Address.ipv4_mapped`):: ++ ++ address.is_private == address.ipv4_mapped.is_private + ++ ``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10`` ++ IPv4 range where they are both ``False``. + """ +- return any(self in net for net in self._constants._private_networks) ++ return ( ++ any(self in net for net in self._constants._private_networks) ++ and all(self not in net for net in self._constants._private_networks_exceptions) ++ ) + + @property + @functools.lru_cache() + def is_global(self): ++ """``True`` if the address is defined as globally reachable by ++ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_ ++ (for IPv6) with the following exception: ++ ++ For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the ++ semantics of the underlying IPv4 addresses and the following condition holds ++ (see :attr:`IPv6Address.ipv4_mapped`):: ++ ++ address.is_global == address.ipv4_mapped.is_global ++ ++ ``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10`` ++ IPv4 range where they are both ``False``. ++ """ + return self not in self._constants._public_network and not self.is_private + + @property +@@ -1538,13 +1561,15 @@ + + _public_network = IPv4Network('100.64.0.0/10') + ++ # Not globally reachable address blocks listed on ++ # https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml + _private_networks = [ + IPv4Network('0.0.0.0/8'), + IPv4Network('10.0.0.0/8'), + IPv4Network('127.0.0.0/8'), + IPv4Network('169.254.0.0/16'), + IPv4Network('172.16.0.0/12'), +- IPv4Network('192.0.0.0/29'), ++ IPv4Network('192.0.0.0/24'), + IPv4Network('192.0.0.170/31'), + IPv4Network('192.0.2.0/24'), + IPv4Network('192.168.0.0/16'), +@@ -1555,6 +1580,11 @@ + IPv4Network('255.255.255.255/32'), + ] + ++ _private_networks_exceptions = [ ++ IPv4Network('192.0.0.9/32'), ++ IPv4Network('192.0.0.10/32'), ++ ] ++ + _reserved_network = IPv4Network('240.0.0.0/4') + + _unspecified_address = IPv4Address('0.0.0.0') +@@ -1996,27 +2026,42 @@ + @property + @functools.lru_cache() + def is_private(self): +- """Test if this address is allocated for private networks. ++ """``True`` if the address is defined as not globally reachable by ++ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_ ++ (for IPv6) with the following exceptions: + +- Returns: +- A boolean, True if the address is reserved per +- iana-ipv6-special-registry, or is ipv4_mapped and is +- reserved in the iana-ipv4-special-registry. ++ * ``is_private`` is ``False`` for ``100.64.0.0/10`` ++ * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the ++ semantics of the underlying IPv4 addresses and the following condition holds ++ (see :attr:`IPv6Address.ipv4_mapped`):: ++ ++ address.is_private == address.ipv4_mapped.is_private + ++ ``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10`` ++ IPv4 range where they are both ``False``. + """ + ipv4_mapped = self.ipv4_mapped + if ipv4_mapped is not None: + return ipv4_mapped.is_private +- return any(self in net for net in self._constants._private_networks) ++ return ( ++ any(self in net for net in self._constants._private_networks) ++ and all(self not in net for net in self._constants._private_networks_exceptions) ++ ) + + @property + def is_global(self): +- """Test if this address is allocated for public networks. ++ """``True`` if the address is defined as globally reachable by ++ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_ ++ (for IPv6) with the following exception: + +- Returns: +- A boolean, true if the address is not reserved per +- iana-ipv6-special-registry. ++ For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the ++ semantics of the underlying IPv4 addresses and the following condition holds ++ (see :attr:`IPv6Address.ipv4_mapped`):: ++ ++ address.is_global == address.ipv4_mapped.is_global + ++ ``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10`` ++ IPv4 range where they are both ``False``. + """ + return not self.is_private + +@@ -2257,19 +2302,31 @@ + + _multicast_network = IPv6Network('ff00::/8') + ++ # Not globally reachable address blocks listed on ++ # https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml + _private_networks = [ + IPv6Network('::1/128'), + IPv6Network('::/128'), + IPv6Network('::ffff:0:0/96'), ++ IPv6Network('64:ff9b:1::/48'), + IPv6Network('100::/64'), + IPv6Network('2001::/23'), +- IPv6Network('2001:2::/48'), + IPv6Network('2001:db8::/32'), +- IPv6Network('2001:10::/28'), ++ # IANA says N/A, let's consider it not globally reachable to be safe ++ IPv6Network('2002::/16'), + IPv6Network('fc00::/7'), + IPv6Network('fe80::/10'), + ] + ++ _private_networks_exceptions = [ ++ IPv6Network('2001:1::1/128'), ++ IPv6Network('2001:1::2/128'), ++ IPv6Network('2001:3::/32'), ++ IPv6Network('2001:4:112::/48'), ++ IPv6Network('2001:20::/28'), ++ IPv6Network('2001:30::/28'), ++ ] ++ + _reserved_networks = [ + IPv6Network('::/8'), IPv6Network('100::/8'), + IPv6Network('200::/7'), IPv6Network('400::/6'), diff --git a/Lib/lib2to3/tests/test_parser.py b/Lib/lib2to3/tests/test_parser.py index 90a1b34b5ff..2e07663de43 100644 --- a/Lib/lib2to3/tests/test_parser.py @@ -894,6 +1784,17 @@ index 6a820c90a1a..28bd77fcf3d 100755 if system == 'Windows': # MS platforms +diff --git a/Lib/pydoc_data/topics.py b/Lib/pydoc_data/topics.py +index f09f28b2c43..ccb2a7b1b42 100644 +--- a/Lib/pydoc_data/topics.py ++++ b/Lib/pydoc_data/topics.py +@@ -1,5 +1,5 @@ + # -*- coding: utf-8 -*- +-# Autogenerated by Sphinx on Tue Mar 19 22:44:19 2024 ++# Autogenerated by Sphinx on Sat Sep 7 01:19:53 2024 + topics = {'assert': 'The "assert" statement\n' + '**********************\n' + '\n' diff --git a/Lib/site.py b/Lib/site.py index 5302037e0bf..35bf9b03d35 100644 --- a/Lib/site.py @@ -909,6 +1810,130 @@ index 5302037e0bf..35bf9b03d35 100644 return None def joinuser(*args): +diff --git a/Lib/socket.py b/Lib/socket.py +index 379662903cf..ecaf73cf307 100644 +--- a/Lib/socket.py ++++ b/Lib/socket.py +@@ -589,16 +589,65 @@ + return socket(0, 0, 0, info) + __all__.append("fromshare") + +-if hasattr(_socket, "socketpair"): ++# Origin: https://gist.github.com/4325783, by Geert Jansen. Public domain. ++# This is used if _socket doesn't natively provide socketpair. It's ++# always defined so that it can be patched in for testing purposes. ++def _fallback_socketpair(family=AF_INET, type=SOCK_STREAM, proto=0): ++ if family == AF_INET: ++ host = _LOCALHOST ++ elif family == AF_INET6: ++ host = _LOCALHOST_V6 ++ else: ++ raise ValueError("Only AF_INET and AF_INET6 socket address families " ++ "are supported") ++ if type != SOCK_STREAM: ++ raise ValueError("Only SOCK_STREAM socket type is supported") ++ if proto != 0: ++ raise ValueError("Only protocol zero is supported") ++ ++ # We create a connected TCP socket. Note the trick with ++ # setblocking(False) that prevents us from having to create a thread. ++ lsock = socket(family, type, proto) ++ try: ++ lsock.bind((host, 0)) ++ lsock.listen() ++ # On IPv6, ignore flow_info and scope_id ++ addr, port = lsock.getsockname()[:2] ++ csock = socket(family, type, proto) ++ try: ++ csock.setblocking(False) ++ try: ++ csock.connect((addr, port)) ++ except (BlockingIOError, InterruptedError): ++ pass ++ csock.setblocking(True) ++ ssock, _ = lsock.accept() ++ except: ++ csock.close() ++ raise ++ finally: ++ lsock.close() + +- def socketpair(family=None, type=SOCK_STREAM, proto=0): +- """socketpair([family[, type[, proto]]]) -> (socket object, socket object) ++ # Authenticating avoids using a connection from something else ++ # able to connect to {host}:{port} instead of us. ++ # We expect only AF_INET and AF_INET6 families. ++ try: ++ if ( ++ ssock.getsockname() != csock.getpeername() ++ or csock.getsockname() != ssock.getpeername() ++ ): ++ raise ConnectionError("Unexpected peer connection") ++ except: ++ # getsockname() and getpeername() can fail ++ # if either socket isn't connected. ++ ssock.close() ++ csock.close() ++ raise + +- Create a pair of socket objects from the sockets returned by the platform +- socketpair() function. +- The arguments are the same as for socket() except the default family is +- AF_UNIX if defined on the platform; otherwise, the default is AF_INET. +- """ ++ return (ssock, csock) ++ ++if hasattr(_socket, "socketpair"): ++ def socketpair(family=None, type=SOCK_STREAM, proto=0): + if family is None: + try: + family = AF_UNIX +@@ -610,44 +659,7 @@ + return a, b + + else: +- +- # Origin: https://gist.github.com/4325783, by Geert Jansen. Public domain. +- def socketpair(family=AF_INET, type=SOCK_STREAM, proto=0): +- if family == AF_INET: +- host = _LOCALHOST +- elif family == AF_INET6: +- host = _LOCALHOST_V6 +- else: +- raise ValueError("Only AF_INET and AF_INET6 socket address families " +- "are supported") +- if type != SOCK_STREAM: +- raise ValueError("Only SOCK_STREAM socket type is supported") +- if proto != 0: +- raise ValueError("Only protocol zero is supported") +- +- # We create a connected TCP socket. Note the trick with +- # setblocking(False) that prevents us from having to create a thread. +- lsock = socket(family, type, proto) +- try: +- lsock.bind((host, 0)) +- lsock.listen() +- # On IPv6, ignore flow_info and scope_id +- addr, port = lsock.getsockname()[:2] +- csock = socket(family, type, proto) +- try: +- csock.setblocking(False) +- try: +- csock.connect((addr, port)) +- except (BlockingIOError, InterruptedError): +- pass +- csock.setblocking(True) +- ssock, _ = lsock.accept() +- except: +- csock.close() +- raise +- finally: +- lsock.close() +- return (ssock, csock) ++ socketpair = _fallback_socketpair + __all__.append("socketpair") + + socketpair.__doc__ = """socketpair([family[, type[, proto]]]) -> (socket object, socket object) diff --git a/Lib/sqlite3/test/dbapi.py b/Lib/sqlite3/test/dbapi.py index a764c82b0d3..8b23cd3f733 100644 --- a/Lib/sqlite3/test/dbapi.py @@ -1099,6 +2124,168 @@ index daf9f000060..5ddead7bbc6 100644 return f"{osname}-{release}-{machine}" +diff --git a/Lib/tarfile.py b/Lib/tarfile.py +index 495349f08f9..3ab6811d633 100755 +--- a/Lib/tarfile.py ++++ b/Lib/tarfile.py +@@ -841,6 +841,9 @@ + # Sentinel for replace() defaults, meaning "don't change the attribute" + _KEEP = object() + ++# Header length is digits followed by a space. ++_header_length_prefix_re = re.compile(br"([0-9]{1,20}) ") ++ + class TarInfo(object): + """Informational class which holds the details about an + archive member given by a tar header block. +@@ -1410,41 +1413,59 @@ + else: + pax_headers = tarfile.pax_headers.copy() + +- # Check if the pax header contains a hdrcharset field. This tells us +- # the encoding of the path, linkpath, uname and gname fields. Normally, +- # these fields are UTF-8 encoded but since POSIX.1-2008 tar +- # implementations are allowed to store them as raw binary strings if +- # the translation to UTF-8 fails. +- match = re.search(br"\d+ hdrcharset=([^\n]+)\n", buf) +- if match is not None: +- pax_headers["hdrcharset"] = match.group(1).decode("utf-8") +- +- # For the time being, we don't care about anything other than "BINARY". +- # The only other value that is currently allowed by the standard is +- # "ISO-IR 10646 2000 UTF-8" in other words UTF-8. +- hdrcharset = pax_headers.get("hdrcharset") +- if hdrcharset == "BINARY": +- encoding = tarfile.encoding +- else: +- encoding = "utf-8" +- + # Parse pax header information. A record looks like that: + # "%d %s=%s\n" % (length, keyword, value). length is the size + # of the complete record including the length field itself and +- # the newline. keyword and value are both UTF-8 encoded strings. +- regex = re.compile(br"(\d+) ([^=]+)=") ++ # the newline. + pos = 0 +- while True: +- match = regex.match(buf, pos) +- if not match: +- break ++ encoding = None ++ raw_headers = [] ++ while len(buf) > pos and buf[pos] != 0x00: ++ if not (match := _header_length_prefix_re.match(buf, pos)): ++ raise InvalidHeaderError("invalid header") ++ try: ++ length = int(match.group(1)) ++ except ValueError: ++ raise InvalidHeaderError("invalid header") ++ # Headers must be at least 5 bytes, shortest being '5 x=\n'. ++ # Value is allowed to be empty. ++ if length < 5: ++ raise InvalidHeaderError("invalid header") ++ if pos + length > len(buf): ++ raise InvalidHeaderError("invalid header") + +- length, keyword = match.groups() +- length = int(length) +- if length == 0: ++ header_value_end_offset = match.start(1) + length - 1 # Last byte of the header ++ keyword_and_value = buf[match.end(1) + 1:header_value_end_offset] ++ raw_keyword, equals, raw_value = keyword_and_value.partition(b"=") ++ ++ # Check the framing of the header. The last character must be '\n' (0x0A) ++ if not raw_keyword or equals != b"=" or buf[header_value_end_offset] != 0x0A: + raise InvalidHeaderError("invalid header") +- value = buf[match.end(2) + 1:match.start(1) + length - 1] ++ raw_headers.append((length, raw_keyword, raw_value)) ++ ++ # Check if the pax header contains a hdrcharset field. This tells us ++ # the encoding of the path, linkpath, uname and gname fields. Normally, ++ # these fields are UTF-8 encoded but since POSIX.1-2008 tar ++ # implementations are allowed to store them as raw binary strings if ++ # the translation to UTF-8 fails. For the time being, we don't care about ++ # anything other than "BINARY". The only other value that is currently ++ # allowed by the standard is "ISO-IR 10646 2000 UTF-8" in other words UTF-8. ++ # Note that we only follow the initial 'hdrcharset' setting to preserve ++ # the initial behavior of the 'tarfile' module. ++ if raw_keyword == b"hdrcharset" and encoding is None: ++ if raw_value == b"BINARY": ++ encoding = tarfile.encoding ++ else: # This branch ensures only the first 'hdrcharset' header is used. ++ encoding = "utf-8" ++ ++ pos += length + ++ # If no explicit hdrcharset is set, we use UTF-8 as a default. ++ if encoding is None: ++ encoding = "utf-8" ++ ++ # After parsing the raw headers we can decode them to text. ++ for length, raw_keyword, raw_value in raw_headers: + # Normally, we could just use "utf-8" as the encoding and "strict" + # as the error handler, but we better not take the risk. For + # example, GNU tar <= 1.23 is known to store filenames it cannot +@@ -1452,17 +1473,16 @@ + # hdrcharset=BINARY header). + # We first try the strict standard encoding, and if that fails we + # fall back on the user's encoding and error handler. +- keyword = self._decode_pax_field(keyword, "utf-8", "utf-8", ++ keyword = self._decode_pax_field(raw_keyword, "utf-8", "utf-8", + tarfile.errors) + if keyword in PAX_NAME_FIELDS: +- value = self._decode_pax_field(value, encoding, tarfile.encoding, ++ value = self._decode_pax_field(raw_value, encoding, tarfile.encoding, + tarfile.errors) + else: +- value = self._decode_pax_field(value, "utf-8", "utf-8", ++ value = self._decode_pax_field(raw_value, "utf-8", "utf-8", + tarfile.errors) + + pax_headers[keyword] = value +- pos += length + + # Fetch the next header. + try: +@@ -1477,7 +1497,7 @@ + + elif "GNU.sparse.size" in pax_headers: + # GNU extended sparse format version 0.0. +- self._proc_gnusparse_00(next, pax_headers, buf) ++ self._proc_gnusparse_00(next, raw_headers) + + elif pax_headers.get("GNU.sparse.major") == "1" and pax_headers.get("GNU.sparse.minor") == "0": + # GNU extended sparse format version 1.0. +@@ -1499,15 +1519,24 @@ + + return next + +- def _proc_gnusparse_00(self, next, pax_headers, buf): ++ def _proc_gnusparse_00(self, next, raw_headers): + """Process a GNU tar extended sparse header, version 0.0. + """ + offsets = [] +- for match in re.finditer(br"\d+ GNU.sparse.offset=(\d+)\n", buf): +- offsets.append(int(match.group(1))) + numbytes = [] +- for match in re.finditer(br"\d+ GNU.sparse.numbytes=(\d+)\n", buf): +- numbytes.append(int(match.group(1))) ++ for _, keyword, value in raw_headers: ++ if keyword == b"GNU.sparse.offset": ++ try: ++ offsets.append(int(value.decode())) ++ except ValueError: ++ raise InvalidHeaderError("invalid header") ++ ++ elif keyword == b"GNU.sparse.numbytes": ++ try: ++ numbytes.append(int(value.decode())) ++ except ValueError: ++ raise InvalidHeaderError("invalid header") ++ + next.sparse = list(zip(offsets, numbytes)) + + def _proc_gnusparse_01(self, next, pax_headers): diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index f474a756b33..21e792a8642 100644 --- a/Lib/test/datetimetester.py @@ -1484,6 +2671,88 @@ index 01c1214c7f7..7e16e2a369d 100644 def test_create_unix_server_ssl_bool(self): coro = self.loop.create_unix_server(lambda: None, path='spam', +diff --git a/Lib/test/test_asyncio/test_windows_events.py b/Lib/test/test_asyncio/test_windows_events.py +index 46eb7ecf7c4..daf0f1ec1b2 100644 +--- a/Lib/test/test_asyncio/test_windows_events.py ++++ b/Lib/test/test_asyncio/test_windows_events.py +@@ -36,7 +36,23 @@ + self.trans.close() + + +-class ProactorLoopCtrlC(test_utils.TestCase): ++class WindowsEventsTestCase(test_utils.TestCase): ++ def _unraisablehook(self, unraisable): ++ # Storing unraisable.object can resurrect an object which is being ++ # finalized. Storing unraisable.exc_value creates a reference cycle. ++ self._unraisable = unraisable ++ print(unraisable) ++ ++ def setUp(self): ++ self._prev_unraisablehook = sys.unraisablehook ++ self._unraisable = None ++ sys.unraisablehook = self._unraisablehook ++ ++ def tearDown(self): ++ sys.unraisablehook = self._prev_unraisablehook ++ self.assertIsNone(self._unraisable) ++ ++class ProactorLoopCtrlC(WindowsEventsTestCase): + + def test_ctrl_c(self): + +@@ -58,7 +74,7 @@ + thread.join() + + +-class ProactorMultithreading(test_utils.TestCase): ++class ProactorMultithreading(WindowsEventsTestCase): + def test_run_from_nonmain_thread(self): + finished = False + +@@ -79,7 +95,7 @@ + self.assertTrue(finished) + + +-class ProactorTests(test_utils.TestCase): ++class ProactorTests(WindowsEventsTestCase): + + def setUp(self): + super().setUp() +@@ -290,8 +306,32 @@ + + return "done" + +- +-class WinPolicyTests(test_utils.TestCase): ++ def test_loop_restart(self): ++ # We're fishing for the "RuntimeError: <_overlapped.Overlapped object at XXX> ++ # still has pending operation at deallocation, the process may crash" error ++ stop = threading.Event() ++ def threadMain(): ++ while not stop.is_set(): ++ self.loop.call_soon_threadsafe(lambda: None) ++ time.sleep(0.01) ++ thr = threading.Thread(target=threadMain) ++ ++ # In 10 60-second runs of this test prior to the fix: ++ # time in seconds until failure: (none), 15.0, 6.4, (none), 7.6, 8.3, 1.7, 22.2, 23.5, 8.3 ++ # 10 seconds had a 50% failure rate but longer would be more costly ++ end_time = time.time() + 10 # Run for 10 seconds ++ self.loop.call_soon(thr.start) ++ while not self._unraisable: # Stop if we got an unraisable exc ++ self.loop.stop() ++ self.loop.run_forever() ++ if time.time() >= end_time: ++ break ++ ++ stop.set() ++ thr.join() ++ ++ ++class WinPolicyTests(WindowsEventsTestCase): + + def test_selector_win_policy(self): + async def main(): diff --git a/Lib/test/test_asyncio/utils.py b/Lib/test/test_asyncio/utils.py index 0b9cde6878f..3f4c90e2ba6 100644 --- a/Lib/test/test_asyncio/utils.py @@ -1782,6 +3051,367 @@ index 8a436ad123b..4fa7571fe43 100644 def trace(self, script_file, subcommand=None): command = self.generate_trace_command(script_file, subcommand) stdout, _ = subprocess.Popen(command, +diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py +index 8b16cca9bf5..5b19bb38f6d 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 +@@ -3288,15 +3289,154 @@ + [('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" ', ++ 'aperson@dom.ain (Al Person)', ++ '"Mariusz Felisiak" ', ++ ] ++ ), ++ [ ++ ('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" ' ++ 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" , "John Doe" ' ++ 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 R. Mastaler', 'jason@dom.ain')]) ++ for addresses, expected in ( ++ (['"Sürname, Firstname" '], ++ [('Sürname, Firstname', 'to@example.com')]), ++ ++ (['foo: ;'], ++ [('', '')]), ++ ++ (['foo: ;', '"Jason R. Mastaler" '], ++ [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')]), ++ ++ ([r'Pete(A nice \) chap) '], ++ [('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 '], ++ [('John Doe (comment)', 'jdoe@machine.example')]), ++ ++ (['"Mary Smith: Personal Account" '], ++ [('Mary Smith: Personal Account', 'smith@home.example')]), ++ ++ (['Undisclosed recipients:;'], ++ [('', '')]), ++ ++ ([r', "Giant; \"Big\" Box" '], ++ [('', '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""" +@@ -3485,6 +3625,54 @@ + 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" , "John Doe" ', ++ ' , ') ++ check(r'"Jane \"Doe\"." ', ++ ' ') ++ ++ # 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 , John Doe ', ++ '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/Lib/test/test_email/test_generator.py b/Lib/test/test_email/test_generator.py +index 89e7edeb63a..d29400f0ed1 100644 +--- a/Lib/test/test_email/test_generator.py ++++ b/Lib/test/test_email/test_generator.py +@@ -6,6 +6,7 @@ + from email.generator import Generator, BytesGenerator + from email.headerregistry import Address + from email import policy ++import email.errors + from test.test_email import TestEmailBase, parameterize + + +@@ -216,6 +217,44 @@ + g.flatten(msg) + self.assertEqual(s.getvalue(), self.typ(expected)) + ++ def test_keep_encoded_newlines(self): ++ msg = self.msgmaker(self.typ(textwrap.dedent("""\ ++ To: nobody ++ Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com ++ ++ None ++ """))) ++ expected = textwrap.dedent("""\ ++ To: nobody ++ Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com ++ ++ None ++ """) ++ s = self.ioclass() ++ g = self.genclass(s, policy=self.policy.clone(max_line_length=80)) ++ g.flatten(msg) ++ self.assertEqual(s.getvalue(), self.typ(expected)) ++ ++ def test_keep_long_encoded_newlines(self): ++ msg = self.msgmaker(self.typ(textwrap.dedent("""\ ++ To: nobody ++ Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com ++ ++ None ++ """))) ++ expected = textwrap.dedent("""\ ++ To: nobody ++ Subject: Bad subject ++ =?utf-8?q?=0A?=Bcc: ++ injection@example.com ++ ++ None ++ """) ++ s = self.ioclass() ++ g = self.genclass(s, policy=self.policy.clone(max_line_length=30)) ++ g.flatten(msg) ++ self.assertEqual(s.getvalue(), self.typ(expected)) ++ + + class TestGenerator(TestGeneratorBase, TestEmailBase): + +@@ -224,6 +263,29 @@ + ioclass = io.StringIO + typ = str + ++ def test_verify_generated_headers(self): ++ """gh-121650: by default the generator prevents header injection""" ++ class LiteralHeader(str): ++ name = 'Header' ++ def fold(self, **kwargs): ++ return self ++ ++ for text in ( ++ 'Value\r\nBad Injection\r\n', ++ 'NoNewLine' ++ ): ++ with self.subTest(text=text): ++ message = message_from_string( ++ "Header: Value\r\n\r\nBody", ++ policy=self.policy, ++ ) ++ ++ del message['Header'] ++ message['Header'] = LiteralHeader(text) ++ ++ with self.assertRaises(email.errors.HeaderWriteError): ++ message.as_string() ++ + + class TestBytesGenerator(TestGeneratorBase, TestEmailBase): + +diff --git a/Lib/test/test_email/test_policy.py b/Lib/test/test_email/test_policy.py +index e87c2755494..ff1ddf7d7a8 100644 +--- a/Lib/test/test_email/test_policy.py ++++ b/Lib/test/test_email/test_policy.py +@@ -26,6 +26,7 @@ + 'raise_on_defect': False, + 'mangle_from_': True, + 'message_factory': None, ++ 'verify_generated_headers': True, + } + # These default values are the ones set on email.policy.default. + # If any of these defaults change, the docs must be updated. +@@ -277,6 +278,31 @@ + with self.assertRaises(email.errors.HeaderParseError): + policy.fold("Subject", subject) + ++ def test_verify_generated_headers(self): ++ """Turning protection off allows header injection""" ++ policy = email.policy.default.clone(verify_generated_headers=False) ++ for text in ( ++ 'Header: Value\r\nBad: Injection\r\n', ++ 'Header: NoNewLine' ++ ): ++ with self.subTest(text=text): ++ message = email.message_from_string( ++ "Header: Value\r\n\r\nBody", ++ policy=policy, ++ ) ++ class LiteralHeader(str): ++ name = 'Header' ++ def fold(self, **kwargs): ++ return self ++ ++ del message['Header'] ++ message['Header'] = LiteralHeader(text) ++ ++ self.assertEqual( ++ message.as_string(), ++ f"{text}\nBody", ++ ) ++ + # XXX: Need subclassing tests. + # For adding subclassed objects, make sure the usual rules apply (subclass + # wins), but that the order still works (right overrides left). diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 8c343f37210..f13b5a1745b 100644 --- a/Lib/test/test_embed.py @@ -3656,6 +5286,62 @@ index f86e767ac0e..14434981e95 100644 @create_and_remove_directory(TEMPDIR) def test_compress_stdin_outfile(self): args = sys.executable, '-m', 'gzip' +diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py +index 6072c7e15e9..644e75cd5b7 100644 +--- a/Lib/test/test_http_cookies.py ++++ b/Lib/test/test_http_cookies.py +@@ -5,6 +5,7 @@ + import unittest + from http import cookies + import pickle ++from test import support + + + class CookieTests(unittest.TestCase): +@@ -58,6 +59,43 @@ + for k, v in sorted(case['dict'].items()): + self.assertEqual(C[k].value, v) + ++ def test_unquote(self): ++ cases = [ ++ (r'a="b=\""', 'b="'), ++ (r'a="b=\\"', 'b=\\'), ++ (r'a="b=\="', 'b=='), ++ (r'a="b=\n"', 'b=n'), ++ (r'a="b=\042"', 'b="'), ++ (r'a="b=\134"', 'b=\\'), ++ (r'a="b=\377"', 'b=\xff'), ++ (r'a="b=\400"', 'b=400'), ++ (r'a="b=\42"', 'b=42'), ++ (r'a="b=\\042"', 'b=\\042'), ++ (r'a="b=\\134"', 'b=\\134'), ++ (r'a="b=\\\""', 'b=\\"'), ++ (r'a="b=\\\042"', 'b=\\"'), ++ (r'a="b=\134\""', 'b=\\"'), ++ (r'a="b=\134\042"', 'b=\\"'), ++ ] ++ for encoded, decoded in cases: ++ with self.subTest(encoded): ++ C = cookies.SimpleCookie() ++ C.load(encoded) ++ self.assertEqual(C['a'].value, decoded) ++ ++ @support.requires_resource('cpu') ++ def test_unquote_large(self): ++ n = 10**6 ++ for encoded in r'\\', r'\134': ++ with self.subTest(encoded): ++ data = 'a="b=' + encoded*n + ';"' ++ C = cookies.SimpleCookie() ++ C.load(data) ++ value = C['a'].value ++ self.assertEqual(value[:3], 'b=\\') ++ self.assertEqual(value[-2:], '\\;') ++ self.assertEqual(len(value), n + 3) ++ + def test_load(self): + C = cookies.SimpleCookie() + C.load('Customer="WILE_E_COYOTE"; Version=1; Path=/acme') diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index 10a50fe97ec..ae51668668c 100644 --- a/Lib/test/test_httpservers.py @@ -3998,6 +5684,83 @@ index 8dae85ac4f5..54359c36f15 100644 support.requires( 'largefile', 'test requires %s bytes and a long time to run' % self.LARGE) +diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py +index f1ef87f7d58..86f3bb504a2 100644 +--- a/Lib/test/test_ipaddress.py ++++ b/Lib/test/test_ipaddress.py +@@ -2263,6 +2263,10 @@ + self.assertEqual(True, ipaddress.ip_address( + '172.31.255.255').is_private) + self.assertEqual(False, ipaddress.ip_address('172.32.0.0').is_private) ++ self.assertFalse(ipaddress.ip_address('192.0.0.0').is_global) ++ self.assertTrue(ipaddress.ip_address('192.0.0.9').is_global) ++ self.assertTrue(ipaddress.ip_address('192.0.0.10').is_global) ++ self.assertFalse(ipaddress.ip_address('192.0.0.255').is_global) + + self.assertEqual(True, + ipaddress.ip_address('169.254.100.200').is_link_local) +@@ -2278,6 +2282,40 @@ + self.assertEqual(False, ipaddress.ip_address('128.0.0.0').is_loopback) + self.assertEqual(True, ipaddress.ip_network('0.0.0.0').is_unspecified) + ++ def testPrivateNetworks(self): ++ self.assertEqual(True, ipaddress.ip_network("0.0.0.0/0").is_private) ++ self.assertEqual(False, ipaddress.ip_network("1.0.0.0/8").is_private) ++ ++ self.assertEqual(True, ipaddress.ip_network("0.0.0.0/8").is_private) ++ self.assertEqual(True, ipaddress.ip_network("10.0.0.0/8").is_private) ++ self.assertEqual(True, ipaddress.ip_network("127.0.0.0/8").is_private) ++ self.assertEqual(True, ipaddress.ip_network("169.254.0.0/16").is_private) ++ self.assertEqual(True, ipaddress.ip_network("172.16.0.0/12").is_private) ++ self.assertEqual(True, ipaddress.ip_network("192.0.0.0/29").is_private) ++ self.assertEqual(False, ipaddress.ip_network("192.0.0.9/32").is_private) ++ self.assertEqual(True, ipaddress.ip_network("192.0.0.170/31").is_private) ++ self.assertEqual(True, ipaddress.ip_network("192.0.2.0/24").is_private) ++ self.assertEqual(True, ipaddress.ip_network("192.168.0.0/16").is_private) ++ self.assertEqual(True, ipaddress.ip_network("198.18.0.0/15").is_private) ++ self.assertEqual(True, ipaddress.ip_network("198.51.100.0/24").is_private) ++ self.assertEqual(True, ipaddress.ip_network("203.0.113.0/24").is_private) ++ self.assertEqual(True, ipaddress.ip_network("240.0.0.0/4").is_private) ++ self.assertEqual(True, ipaddress.ip_network("255.255.255.255/32").is_private) ++ ++ self.assertEqual(False, ipaddress.ip_network("::/0").is_private) ++ self.assertEqual(False, ipaddress.ip_network("::ff/128").is_private) ++ ++ self.assertEqual(True, ipaddress.ip_network("::1/128").is_private) ++ self.assertEqual(True, ipaddress.ip_network("::/128").is_private) ++ self.assertEqual(True, ipaddress.ip_network("::ffff:0:0/96").is_private) ++ self.assertEqual(True, ipaddress.ip_network("100::/64").is_private) ++ self.assertEqual(True, ipaddress.ip_network("2001:2::/48").is_private) ++ self.assertEqual(False, ipaddress.ip_network("2001:3::/48").is_private) ++ self.assertEqual(True, ipaddress.ip_network("2001:db8::/32").is_private) ++ self.assertEqual(True, ipaddress.ip_network("2001:10::/28").is_private) ++ self.assertEqual(True, ipaddress.ip_network("fc00::/7").is_private) ++ self.assertEqual(True, ipaddress.ip_network("fe80::/10").is_private) ++ + def testReservedIpv6(self): + + self.assertEqual(True, ipaddress.ip_network('ffff::').is_multicast) +@@ -2351,6 +2389,20 @@ + self.assertEqual(True, ipaddress.ip_address('0::0').is_unspecified) + self.assertEqual(False, ipaddress.ip_address('::1').is_unspecified) + ++ self.assertFalse(ipaddress.ip_address('64:ff9b:1::').is_global) ++ self.assertFalse(ipaddress.ip_address('2001::').is_global) ++ self.assertTrue(ipaddress.ip_address('2001:1::1').is_global) ++ self.assertTrue(ipaddress.ip_address('2001:1::2').is_global) ++ self.assertFalse(ipaddress.ip_address('2001:2::').is_global) ++ self.assertTrue(ipaddress.ip_address('2001:3::').is_global) ++ self.assertFalse(ipaddress.ip_address('2001:4::').is_global) ++ self.assertTrue(ipaddress.ip_address('2001:4:112::').is_global) ++ self.assertFalse(ipaddress.ip_address('2001:10::').is_global) ++ self.assertTrue(ipaddress.ip_address('2001:20::').is_global) ++ self.assertTrue(ipaddress.ip_address('2001:30::').is_global) ++ self.assertFalse(ipaddress.ip_address('2001:40::').is_global) ++ self.assertFalse(ipaddress.ip_address('2002::').is_global) ++ + # some generic IETF reserved addresses + self.assertEqual(True, ipaddress.ip_address('100::').is_reserved) + self.assertEqual(True, ipaddress.ip_network('4000::1/128').is_reserved) diff --git a/Lib/test/test_json/test_tool.py b/Lib/test/test_json/test_tool.py index 1d7fca6efb1..0715cc75b66 100644 --- a/Lib/test/test_json/test_tool.py @@ -4191,7 +5954,7 @@ index 6558952308f..9c3901a63f3 100644 if __name__ == '__main__': diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py -index 6b443c48f8f..33e52529bf3 100644 +index 6b443c48f8f..8daa5f4071d 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -33,6 +33,8 @@ @@ -4211,7 +5974,26 @@ index 6b443c48f8f..33e52529bf3 100644 def test_putenv_unsetenv(self): name = "PYTHONTESTVAR" value = "testvalue" -@@ -2168,6 +2171,7 @@ +@@ -1635,6 +1638,18 @@ + self.assertRaises(OSError, os.makedirs, path, exist_ok=True) + os.remove(path) + ++ @unittest.skipUnless(os.name == 'nt', "requires Windows") ++ def test_win32_mkdir_700(self): ++ base = os_helper.TESTFN ++ path = os.path.abspath(os.path.join(os_helper.TESTFN, 'dir')) ++ os.mkdir(path, mode=0o700) ++ out = subprocess.check_output(["cacls.exe", path, "/s"], encoding="oem") ++ os.rmdir(path) ++ self.assertEqual( ++ out.strip(), ++ f'{path} "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;OW)"', ++ ) ++ + def tearDown(self): + path = os.path.join(os_helper.TESTFN, 'dir1', 'dir2', 'dir3', + 'dir4', 'dir5', 'dir6') +@@ -2168,6 +2183,7 @@ self.check(os.fchown, -1, -1) @unittest.skipUnless(hasattr(os, 'fpathconf'), 'test needs os.fpathconf()') @@ -4219,7 +6001,7 @@ index 6b443c48f8f..33e52529bf3 100644 def test_fpathconf(self): self.check(os.pathconf, "PC_NAME_MAX") self.check(os.fpathconf, "PC_NAME_MAX") -@@ -2294,6 +2298,7 @@ +@@ -2294,6 +2310,7 @@ self.assertRaises(OverflowError, os.setreuid, 0, self.UID_OVERFLOW) @unittest.skipUnless(hasattr(os, 'setreuid'), 'test needs os.setreuid()') @@ -4227,7 +6009,7 @@ index 6b443c48f8f..33e52529bf3 100644 def test_setreuid_neg1(self): # Needs to accept -1. We run this in a subprocess to avoid # altering the test runner's process state (issue8045). -@@ -2311,6 +2316,7 @@ +@@ -2311,6 +2328,7 @@ self.assertRaises(OverflowError, os.setregid, 0, self.GID_OVERFLOW) @unittest.skipUnless(hasattr(os, 'setregid'), 'test needs os.setregid()') @@ -4235,7 +6017,7 @@ index 6b443c48f8f..33e52529bf3 100644 def test_setregid_neg1(self): # Needs to accept -1. We run this in a subprocess to avoid # altering the test runner's process state (issue8045). -@@ -2983,6 +2989,7 @@ +@@ -2983,6 +3001,7 @@ class PidTests(unittest.TestCase): @unittest.skipUnless(hasattr(os, 'getppid'), "test needs os.getppid") @@ -4243,7 +6025,7 @@ index 6b443c48f8f..33e52529bf3 100644 def test_getppid(self): p = subprocess.Popen([sys.executable, '-c', 'import os; print(os.getppid())'], -@@ -2991,6 +2998,7 @@ +@@ -2991,6 +3010,7 @@ # We are the parent of our subprocess self.assertEqual(int(stdout), os.getpid()) @@ -4251,7 +6033,7 @@ index 6b443c48f8f..33e52529bf3 100644 def check_waitpid(self, code, exitcode, callback=None): if sys.platform == 'win32': # On Windows, os.spawnv() simply joins arguments with spaces: -@@ -3053,6 +3061,7 @@ +@@ -3053,6 +3073,7 @@ self.check_waitpid(code, exitcode=-signum, callback=kill_process) @@ -4259,7 +6041,7 @@ index 6b443c48f8f..33e52529bf3 100644 class SpawnTests(unittest.TestCase): def create_args(self, *, with_env=False, use_bytes=False): self.exitcode = 17 -@@ -3135,6 +3144,7 @@ +@@ -3135,6 +3156,7 @@ self.assertEqual(exitcode, self.exitcode) @requires_os_func('spawnv') @@ -4267,7 +6049,7 @@ index 6b443c48f8f..33e52529bf3 100644 def test_nowait(self): args = self.create_args() pid = os.spawnv(os.P_NOWAIT, args[0], args) -@@ -3678,6 +3688,7 @@ +@@ -3678,6 +3700,7 @@ self.assertGreaterEqual(size.columns, 0) self.assertGreaterEqual(size.lines, 0) @@ -4698,6 +6480,34 @@ index e6d36825f15..93e1ab556ae 100644 def assertSigInt(self, *args, **kwargs): proc = subprocess.run(*args, **kwargs, text=True, stderr=subprocess.PIPE) self.assertTrue(proc.stderr.endswith("\nKeyboardInterrupt\n")) +diff --git a/Lib/test/test_sax.py b/Lib/test/test_sax.py +index 97e96668f85..9b3014a94a0 100644 +--- a/Lib/test/test_sax.py ++++ b/Lib/test/test_sax.py +@@ -1215,10 +1215,10 @@ + + self.assertEqual(result.getvalue(), start + b"text") + ++ @unittest.skipIf(pyexpat.version_info < (2, 6, 0), ++ f'Expat {pyexpat.version_info} does not ' ++ 'support reparse deferral') + def test_flush_reparse_deferral_enabled(self): +- if pyexpat.version_info < (2, 6, 0): +- self.skipTest(f'Expat {pyexpat.version_info} does not support reparse deferral') +- + result = BytesIO() + xmlgen = XMLGenerator(result) + parser = create_parser() +@@ -1251,8 +1251,8 @@ + + if pyexpat.version_info >= (2, 6, 0): + parser._parser.SetReparseDeferralEnabled(False) ++ self.assertEqual(result.getvalue(), start) # i.e. no elements started + +- self.assertEqual(result.getvalue(), start) # i.e. no elements started + self.assertFalse(parser._parser.GetReparseDeferralEnabled()) + + parser.flush() diff --git a/Lib/test/test_select.py b/Lib/test/test_select.py index cf32cf2f6a6..810c67aa2b3 100644 --- a/Lib/test/test_select.py @@ -4873,7 +6683,7 @@ index c70e1fa9ae1..18086ac55b9 100644 libpath = os.path.dirname(os.path.dirname(encodings.__file__)) exe_prefix = os.path.dirname(sys.executable) diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py -index 4f1fc3fd92d..4b89436b844 100644 +index 4f1fc3fd92d..9cbe03d8838 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -3,6 +3,7 @@ @@ -4884,7 +6694,38 @@ index 4f1fc3fd92d..4b89436b844 100644 import errno import io -@@ -692,7 +693,7 @@ +@@ -557,19 +558,27 @@ + def __init__(self, methodName='runTest'): + unittest.TestCase.__init__(self, methodName=methodName) + ThreadableTest.__init__(self) ++ self.cli = None ++ self.serv = None ++ ++ def socketpair(self): ++ # To be overridden by some child classes. ++ return socket.socketpair() + + def setUp(self): +- self.serv, self.cli = socket.socketpair() ++ self.serv, self.cli = self.socketpair() + + def tearDown(self): +- self.serv.close() ++ if self.serv: ++ self.serv.close() + self.serv = None + + def clientSetUp(self): + pass + + def clientTearDown(self): +- self.cli.close() ++ if self.cli: ++ self.cli.close() + self.cli = None + ThreadableTest.clientTearDown(self) + +@@ -692,7 +701,7 @@ super().setUp() def bindSock(self, sock): @@ -4893,7 +6734,7 @@ index 4f1fc3fd92d..4b89436b844 100644 socket_helper.bind_unix_socket(sock, path) self.addCleanup(os_helper.unlink, path) -@@ -1154,8 +1155,11 @@ +@@ -1154,8 +1163,11 @@ # Find one service that exists, then check all the related interfaces. # I've ordered this by protocols that have both a tcp and udp # protocol, at least for modern Linuxes. @@ -4907,7 +6748,7 @@ index 4f1fc3fd92d..4b89436b844 100644 # avoid the 'echo' service on this platform, as there is an # assumption breaking non-standard port/protocol entry services = ('daytime', 'qotd', 'domain') -@@ -1913,12 +1917,13 @@ +@@ -1913,12 +1925,13 @@ self._test_socket_fileno(s, socket.AF_INET6, socket.SOCK_STREAM) if hasattr(socket, "AF_UNIX"): @@ -4924,7 +6765,7 @@ index 4f1fc3fd92d..4b89436b844 100644 except PermissionError: pass else: -@@ -3526,7 +3531,7 @@ +@@ -3526,7 +3539,7 @@ def _testFDPassCMSG_LEN(self): self.createAndSendFDs(1) @@ -4933,7 +6774,7 @@ index 4f1fc3fd92d..4b89436b844 100644 @unittest.skipIf(AIX, "skipping, see issue #22397") @requireAttrs(socket, "CMSG_SPACE") def testFDPassSeparate(self): -@@ -3537,7 +3542,7 @@ +@@ -3537,7 +3550,7 @@ maxcmsgs=2) @testFDPassSeparate.client_skip @@ -4942,7 +6783,7 @@ index 4f1fc3fd92d..4b89436b844 100644 @unittest.skipIf(AIX, "skipping, see issue #22397") def _testFDPassSeparate(self): fd0, fd1 = self.newFDs(2) -@@ -3550,7 +3555,7 @@ +@@ -3550,7 +3563,7 @@ array.array("i", [fd1]))]), len(MSG)) @@ -4951,7 +6792,7 @@ index 4f1fc3fd92d..4b89436b844 100644 @unittest.skipIf(AIX, "skipping, see issue #22397") @requireAttrs(socket, "CMSG_SPACE") def testFDPassSeparateMinSpace(self): -@@ -3564,7 +3569,7 @@ +@@ -3564,7 +3577,7 @@ maxcmsgs=2, ignoreflags=socket.MSG_CTRUNC) @testFDPassSeparateMinSpace.client_skip @@ -4960,7 +6801,7 @@ index 4f1fc3fd92d..4b89436b844 100644 @unittest.skipIf(AIX, "skipping, see issue #22397") def _testFDPassSeparateMinSpace(self): fd0, fd1 = self.newFDs(2) -@@ -3588,7 +3593,7 @@ +@@ -3588,7 +3601,7 @@ nbytes = self.sendmsgToServer([msg]) self.assertEqual(nbytes, len(msg)) @@ -4969,6 +6810,119 @@ index 4f1fc3fd92d..4b89436b844 100644 def testFDPassEmpty(self): # Try to pass an empty FD array. Can receive either no array # or an empty array. +@@ -4630,6 +4643,112 @@ + self.assertEqual(msg, MSG) + + ++class PurePythonSocketPairTest(SocketPairTest): ++ # Explicitly use socketpair AF_INET or AF_INET6 to ensure that is the ++ # code path we're using regardless platform is the pure python one where ++ # `_socket.socketpair` does not exist. (AF_INET does not work with ++ # _socket.socketpair on many platforms). ++ def socketpair(self): ++ # called by super().setUp(). ++ try: ++ return socket.socketpair(socket.AF_INET6) ++ except OSError: ++ return socket.socketpair(socket.AF_INET) ++ ++ # Local imports in this class make for easy security fix backporting. ++ ++ def setUp(self): ++ if hasattr(_socket, "socketpair"): ++ self._orig_sp = socket.socketpair ++ # This forces the version using the non-OS provided socketpair ++ # emulation via an AF_INET socket in Lib/socket.py. ++ socket.socketpair = socket._fallback_socketpair ++ else: ++ # This platform already uses the non-OS provided version. ++ self._orig_sp = None ++ super().setUp() ++ ++ def tearDown(self): ++ super().tearDown() ++ if self._orig_sp is not None: ++ # Restore the default socket.socketpair definition. ++ socket.socketpair = self._orig_sp ++ ++ def test_recv(self): ++ msg = self.serv.recv(1024) ++ self.assertEqual(msg, MSG) ++ ++ def _test_recv(self): ++ self.cli.send(MSG) ++ ++ def test_send(self): ++ self.serv.send(MSG) ++ ++ def _test_send(self): ++ msg = self.cli.recv(1024) ++ self.assertEqual(msg, MSG) ++ ++ def test_ipv4(self): ++ cli, srv = socket.socketpair(socket.AF_INET) ++ cli.close() ++ srv.close() ++ ++ def _test_ipv4(self): ++ pass ++ ++ @unittest.skipIf(not hasattr(_socket, 'IPPROTO_IPV6') or ++ not hasattr(_socket, 'IPV6_V6ONLY'), ++ "IPV6_V6ONLY option not supported") ++ @unittest.skipUnless(socket_helper.IPV6_ENABLED, 'IPv6 required for this test') ++ def test_ipv6(self): ++ cli, srv = socket.socketpair(socket.AF_INET6) ++ cli.close() ++ srv.close() ++ ++ def _test_ipv6(self): ++ pass ++ ++ def test_injected_authentication_failure(self): ++ orig_getsockname = socket.socket.getsockname ++ inject_sock = None ++ ++ def inject_getsocketname(self): ++ nonlocal inject_sock ++ sockname = orig_getsockname(self) ++ # Connect to the listening socket ahead of the ++ # client socket. ++ if inject_sock is None: ++ inject_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ++ inject_sock.setblocking(False) ++ try: ++ inject_sock.connect(sockname[:2]) ++ except (BlockingIOError, InterruptedError): ++ pass ++ inject_sock.setblocking(True) ++ return sockname ++ ++ sock1 = sock2 = None ++ try: ++ socket.socket.getsockname = inject_getsocketname ++ with self.assertRaises(OSError): ++ sock1, sock2 = socket.socketpair() ++ finally: ++ socket.socket.getsockname = orig_getsockname ++ if inject_sock: ++ inject_sock.close() ++ if sock1: # This cleanup isn't needed on a successful test. ++ sock1.close() ++ if sock2: ++ sock2.close() ++ ++ def _test_injected_authentication_failure(self): ++ # No-op. Exists for base class threading infrastructure to call. ++ # We could refactor this test into its own lesser class along with the ++ # setUp and tearDown code to construct an ideal; it is simpler to keep ++ # it here and live with extra overhead one this _one_ failure test. ++ pass ++ ++ + class NonBlockingTCPTests(ThreadedTCPSocketTest): + + def __init__(self, methodName='runTest'): diff --git a/Lib/test/test_socketserver.py b/Lib/test/test_socketserver.py index 211321f3761..faa038816a7 100644 --- a/Lib/test/test_socketserver.py @@ -5181,11 +7135,72 @@ index 5ee9839c048..e4b37afab4e 100644 def test_get_makefile_filename(self): makefile = sysconfig.get_makefile_filename() self.assertTrue(os.path.isfile(makefile), makefile) +diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py +index cfc13bccb20..007c3e94acb 100644 +--- a/Lib/test/test_tarfile.py ++++ b/Lib/test/test_tarfile.py +@@ -1139,6 +1139,48 @@ + finally: + tar.close() + ++ def test_pax_header_bad_formats(self): ++ # The fields from the pax header have priority over the ++ # TarInfo. ++ pax_header_replacements = ( ++ b" foo=bar\n", ++ b"0 \n", ++ b"1 \n", ++ b"2 \n", ++ b"3 =\n", ++ b"4 =a\n", ++ b"1000000 foo=bar\n", ++ b"0 foo=bar\n", ++ b"-12 foo=bar\n", ++ b"000000000000000000000000036 foo=bar\n", ++ ) ++ pax_headers = {"foo": "bar"} ++ ++ for replacement in pax_header_replacements: ++ with self.subTest(header=replacement): ++ tar = tarfile.open(tmpname, "w", format=tarfile.PAX_FORMAT, ++ encoding="iso8859-1") ++ try: ++ t = tarfile.TarInfo() ++ t.name = "pax" # non-ASCII ++ t.uid = 1 ++ t.pax_headers = pax_headers ++ tar.addfile(t) ++ finally: ++ tar.close() ++ ++ with open(tmpname, "rb") as f: ++ data = f.read() ++ self.assertIn(b"11 foo=bar\n", data) ++ data = data.replace(b"11 foo=bar\n", replacement) ++ ++ with open(tmpname, "wb") as f: ++ f.truncate() ++ f.write(data) ++ ++ with self.assertRaisesRegex(tarfile.ReadError, r"method tar: ReadError\('invalid header'\)"): ++ tarfile.open(tmpname, encoding="iso8859-1") ++ + + class WriteTestBase(TarTest): + # Put all write tests in here that are supposed to be tested diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py -index 30d57baf977..e0df63e3ea9 100644 +index 30d57baf977..e1fb2a39750 100644 --- a/Lib/test/test_tempfile.py +++ b/Lib/test/test_tempfile.py -@@ -200,6 +200,7 @@ +@@ -11,6 +11,7 @@ + import stat + import types + import weakref ++import subprocess + from unittest import mock + + import unittest +@@ -200,6 +201,7 @@ @unittest.skipUnless(hasattr(os, 'fork'), "os.fork is required for this test") @@ -5193,7 +7208,7 @@ index 30d57baf977..e0df63e3ea9 100644 def test_process_awareness(self): # ensure that the random source differs between # child and parent. -@@ -461,6 +462,7 @@ +@@ -461,6 +463,7 @@ self.assertEqual(mode, expected) @unittest.skipUnless(has_spawnl, 'os.spawnl not available') @@ -5201,6 +7216,40 @@ index 30d57baf977..e0df63e3ea9 100644 def test_noinherit(self): # _mkstemp_inner file handles are not inherited by child processes +@@ -800,6 +803,33 @@ + finally: + os.rmdir(dir) + ++ @unittest.skipUnless(os.name == "nt", "Only on Windows.") ++ def test_mode_win32(self): ++ # Use icacls.exe to extract the users with some level of access ++ # Main thing we are testing is that the BUILTIN\Users group has ++ # no access. The exact ACL is going to vary based on which user ++ # is running the test. ++ dir = self.do_create() ++ try: ++ out = subprocess.check_output(["icacls.exe", dir], encoding="oem").casefold() ++ finally: ++ os.rmdir(dir) ++ ++ dir = dir.casefold() ++ users = set() ++ found_user = False ++ for line in out.strip().splitlines(): ++ acl = None ++ # First line of result includes our directory ++ if line.startswith(dir): ++ acl = line.removeprefix(dir).strip() ++ elif line and line[:1].isspace(): ++ acl = line.strip() ++ if acl: ++ users.add(acl.partition(":")[0]) ++ ++ self.assertNotIn(r"BUILTIN\Users".casefold(), users) ++ + def test_collision_with_existing_file(self): + # mkdtemp tries another name when a file with + # the chosen name already exists diff --git a/Lib/test/test_thread.py b/Lib/test/test_thread.py index 4ae8a833b99..76ea55f14ad 100644 --- a/Lib/test/test_thread.py @@ -5314,6 +7363,118 @@ index fe17aac6c1f..0df55745afd 100644 def test_http_body_pipe(self): # A file reading from a pipe. # A pipe cannot be seek'ed. There is no way to determine the +diff --git a/Lib/test/test_urlparse.py b/Lib/test/test_urlparse.py +index b0aed37de7d..5e08aa1bbad 100644 +--- a/Lib/test/test_urlparse.py ++++ b/Lib/test/test_urlparse.py +@@ -70,7 +70,9 @@ + + class UrlParseTestCase(unittest.TestCase): + +- def checkRoundtrips(self, url, parsed, split): ++ def checkRoundtrips(self, url, parsed, split, url2=None): ++ if url2 is None: ++ url2 = url + result = urllib.parse.urlparse(url) + self.assertEqual(result, parsed) + t = (result.scheme, result.netloc, result.path, +@@ -78,7 +80,7 @@ + self.assertEqual(t, parsed) + # put it back together and it should be the same + result2 = urllib.parse.urlunparse(result) +- self.assertEqual(result2, url) ++ self.assertEqual(result2, url2) + self.assertEqual(result2, result.geturl()) + + # the result of geturl() is a fixpoint; we can always parse it +@@ -104,7 +106,7 @@ + result.query, result.fragment) + self.assertEqual(t, split) + result2 = urllib.parse.urlunsplit(result) +- self.assertEqual(result2, url) ++ self.assertEqual(result2, url2) + self.assertEqual(result2, result.geturl()) + + # check the fixpoint property of re-parsing the result of geturl() +@@ -142,9 +144,39 @@ + + def test_roundtrips(self): + str_cases = [ ++ ('path/to/file', ++ ('', '', 'path/to/file', '', '', ''), ++ ('', '', 'path/to/file', '', '')), ++ ('/path/to/file', ++ ('', '', '/path/to/file', '', '', ''), ++ ('', '', '/path/to/file', '', '')), ++ ('//path/to/file', ++ ('', 'path', '/to/file', '', '', ''), ++ ('', 'path', '/to/file', '', '')), ++ ('////path/to/file', ++ ('', '', '//path/to/file', '', '', ''), ++ ('', '', '//path/to/file', '', '')), ++ ('scheme:path/to/file', ++ ('scheme', '', 'path/to/file', '', '', ''), ++ ('scheme', '', 'path/to/file', '', '')), ++ ('scheme:/path/to/file', ++ ('scheme', '', '/path/to/file', '', '', ''), ++ ('scheme', '', '/path/to/file', '', '')), ++ ('scheme://path/to/file', ++ ('scheme', 'path', '/to/file', '', '', ''), ++ ('scheme', 'path', '/to/file', '', '')), ++ ('scheme:////path/to/file', ++ ('scheme', '', '//path/to/file', '', '', ''), ++ ('scheme', '', '//path/to/file', '', '')), + ('file:///tmp/junk.txt', + ('file', '', '/tmp/junk.txt', '', '', ''), + ('file', '', '/tmp/junk.txt', '', '')), ++ ('file:////tmp/junk.txt', ++ ('file', '', '//tmp/junk.txt', '', '', ''), ++ ('file', '', '//tmp/junk.txt', '', '')), ++ ('file://///tmp/junk.txt', ++ ('file', '', '///tmp/junk.txt', '', '', ''), ++ ('file', '', '///tmp/junk.txt', '', '')), + ('imap://mail.python.org/mbox1', + ('imap', 'mail.python.org', '/mbox1', '', '', ''), + ('imap', 'mail.python.org', '/mbox1', '', '')), +@@ -175,6 +207,38 @@ + for url, parsed, split in str_cases + bytes_cases: + self.checkRoundtrips(url, parsed, split) + ++ def test_roundtrips_normalization(self): ++ str_cases = [ ++ ('///path/to/file', ++ '/path/to/file', ++ ('', '', '/path/to/file', '', '', ''), ++ ('', '', '/path/to/file', '', '')), ++ ('scheme:///path/to/file', ++ 'scheme:/path/to/file', ++ ('scheme', '', '/path/to/file', '', '', ''), ++ ('scheme', '', '/path/to/file', '', '')), ++ ('file:/tmp/junk.txt', ++ 'file:///tmp/junk.txt', ++ ('file', '', '/tmp/junk.txt', '', '', ''), ++ ('file', '', '/tmp/junk.txt', '', '')), ++ ('http:/tmp/junk.txt', ++ 'http:///tmp/junk.txt', ++ ('http', '', '/tmp/junk.txt', '', '', ''), ++ ('http', '', '/tmp/junk.txt', '', '')), ++ ('https:/tmp/junk.txt', ++ 'https:///tmp/junk.txt', ++ ('https', '', '/tmp/junk.txt', '', '', ''), ++ ('https', '', '/tmp/junk.txt', '', '')), ++ ] ++ def _encode(t): ++ return (t[0].encode('ascii'), ++ t[1].encode('ascii'), ++ tuple(x.encode('ascii') for x in t[2]), ++ tuple(x.encode('ascii') for x in t[3])) ++ bytes_cases = [_encode(x) for x in str_cases] ++ for url, url2, parsed, split in str_cases + bytes_cases: ++ self.checkRoundtrips(url, parsed, split, url2) ++ + def test_http_roundtrips(self): + # urllib.parse.urlsplit treats 'http:' as an optimized special case, + # so we test both 'http:' and 'https:' in all the following. diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index fea986b9d86..f25fd5916e4 100644 --- a/Lib/test/test_venv.py @@ -5474,8 +7635,37 @@ index 673cc995d3f..547fcf40ac7 100644 def test_environment_preferred(self): webbrowser = import_helper.import_fresh_module('webbrowser') try: +diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py +index 68717c09be9..d0445da71cf 100644 +--- a/Lib/test/test_xml_etree.py ++++ b/Lib/test/test_xml_etree.py +@@ -1623,11 +1623,10 @@ + with self.assertRaises(ValueError): + ET.XMLPullParser(events=('start', 'end', 'bogus')) + ++ @unittest.skipIf(pyexpat.version_info < (2, 6, 0), ++ f'Expat {pyexpat.version_info} does not ' ++ 'support reparse deferral') + def test_flush_reparse_deferral_enabled(self): +- if pyexpat.version_info < (2, 6, 0): +- self.skipTest(f'Expat {pyexpat.version_info} does not ' +- 'support reparse deferral') +- + parser = ET.XMLPullParser(events=('start', 'end')) + + for chunk in (""): +@@ -1659,8 +1658,8 @@ + self.skipTest(f'XMLParser.(Get|Set)ReparseDeferralEnabled ' + 'methods not available in C') + parser._parser._parser.SetReparseDeferralEnabled(False) ++ self.assert_event_tags(parser, []) # i.e. no elements started + +- self.assert_event_tags(parser, []) # i.e. no elements started + if ET is pyET: + self.assertFalse(parser._parser._parser.GetReparseDeferralEnabled()) + diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py -index 32c01704d9d..6ca98ede360 100644 +index 32c01704d9d..50bf8f4690d 100644 --- a/Lib/test/test_zipfile.py +++ b/Lib/test/test_zipfile.py @@ -21,7 +21,7 @@ @@ -5503,6 +7693,91 @@ index 32c01704d9d..6ca98ede360 100644 def test_execute_zip64(self): output = subprocess.check_output([self.exe_zip64, sys.executable]) self.assertIn(b'number in executable: 5', output) +@@ -3280,6 +3282,84 @@ + zipfile.Path(zf) + zf.extractall(source_path.parent) + ++ def test_malformed_paths(self): ++ """ ++ Path should handle malformed paths gracefully. ++ ++ Paths with leading slashes are not visible. ++ ++ Paths with dots are treated like regular files. ++ """ ++ 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())) == ['../'] ++ assert root.joinpath('..').joinpath('parent.txt').read_bytes() == b'content' ++ ++ def test_unsupported_names(self): ++ """ ++ Path segments with special characters are readable. ++ ++ On some platforms or file systems, characters like ++ ``:`` and ``?`` are not allowed, but they are valid ++ in the zip file. ++ """ ++ data = io.BytesIO() ++ zf = zipfile.ZipFile(data, "w") ++ zf.writestr("path?", b"content") ++ zf.writestr("V: NMS.flac", b"fLaC...") ++ zf.filename = '' ++ root = zipfile.Path(zf) ++ contents = root.iterdir() ++ assert next(contents).name == 'path?' ++ item = next(contents) ++ assert item.name == 'V: NMS.flac', item.name ++ assert root.joinpath('V: NMS.flac').read_bytes() == b"fLaC..." ++ ++ def test_backslash_not_separator(self): ++ """ ++ In a zip file, backslashes are not separators. ++ """ ++ data = io.BytesIO() ++ zf = zipfile.ZipFile(data, "w") ++ zf.writestr(DirtyZipInfo.for_name("foo\\bar", zf), b"content") ++ zf.filename = '' ++ root = zipfile.Path(zf) ++ (first,) = root.iterdir() ++ assert not first.is_dir() ++ assert first.name == 'foo\\bar', first.name ++ ++ ++class DirtyZipInfo(zipfile.ZipInfo): ++ """ ++ Bypass name sanitization. ++ """ ++ ++ def __init__(self, filename, *args, **kwargs): ++ super().__init__(filename, *args, **kwargs) ++ self.filename = filename ++ ++ @classmethod ++ def for_name(cls, name, archive): ++ """ ++ Construct the same way that ZipFile.writestr does. ++ ++ TODO: extract this functionality and re-use ++ """ ++ self = cls(filename=name, date_time=time.localtime(time.time())[:6]) ++ self.compress_type = archive.compression ++ self.compress_level = archive.compresslevel ++ if self.filename.endswith('/'): # pragma: no cover ++ self.external_attr = 0o40775 << 16 # drwxrwxr-x ++ self.external_attr |= 0x10 # MS-DOS directory flag ++ else: ++ self.external_attr = 0o600 << 16 # ?rw------- ++ return self ++ + + class StripExtraTests(unittest.TestCase): + # Note: all of the "z" characters are technically invalid, but up diff --git a/Lib/test/test_zipimport_support.py b/Lib/test/test_zipimport_support.py index 7bf50a33728..d172895be51 100644 --- a/Lib/test/test_zipimport_support.py @@ -5524,6 +7799,59 @@ index 7bf50a33728..d172895be51 100644 def test_doctest_issue4197(self): # To avoid having to keep two copies of the doctest module's # unit tests in sync, this test works by taking the source of +diff --git a/Lib/test/test_zlib.py b/Lib/test/test_zlib.py +index cb0610837ba..208bd1e9e9f 100644 +--- a/Lib/test/test_zlib.py ++++ b/Lib/test/test_zlib.py +@@ -18,6 +18,19 @@ + hasattr(zlib.decompressobj(), "copy"), + '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): + +@@ -445,9 +458,8 @@ + 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 +@@ -776,16 +788,7 @@ + + 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() diff --git a/Lib/unittest/test/test_program.py b/Lib/unittest/test/test_program.py index b7fbbc1e7ba..a544819d516 100644 --- a/Lib/unittest/test/test_program.py @@ -5562,6 +7890,19 @@ index 0082d394dc9..c6ef4851745 100644 def test_warnings(self): """ Check that warnings argument of TextTestRunner correctly affects the +diff --git a/Lib/urllib/parse.py b/Lib/urllib/parse.py +index 0ab2023843f..44806e67a8f 100644 +--- a/Lib/urllib/parse.py ++++ b/Lib/urllib/parse.py +@@ -521,7 +521,7 @@ + empty query; the RFC states that these are equivalent).""" + scheme, netloc, url, query, fragment, _coerce_result = ( + _coerce_args(*components)) +- if netloc or (scheme and scheme in uses_netloc and url[:2] != '//'): ++ if netloc or (scheme and scheme in uses_netloc) or url[:2] == '//': + if url and url[:1] != '/': url = '/' + url + url = '//' + (netloc or '') + url + if scheme: diff --git a/Lib/webbrowser.py b/Lib/webbrowser.py index ec3cece48c9..34a772ab3c4 100755 --- a/Lib/webbrowser.py @@ -5647,6 +7988,52 @@ index ec3cece48c9..34a772ab3c4 100755 def main(): import getopt +diff --git a/Lib/zipfile.py b/Lib/zipfile.py +index 7d18bc2479f..4cd44fb1e4a 100644 +--- a/Lib/zipfile.py ++++ b/Lib/zipfile.py +@@ -9,6 +9,7 @@ + import itertools + import os + import posixpath ++import re + import shutil + import stat + import struct +@@ -2151,7 +2152,7 @@ + def _ancestry(path): + """ + Given a path with elements separated by +- posixpath.sep, generate all elements of that path ++ posixpath.sep, generate all elements of that path. + + >>> list(_ancestry('b/d')) + ['b/d', 'b'] +@@ -2163,9 +2164,14 @@ + ['b'] + >>> list(_ancestry('')) + [] ++ ++ Multiple separators are treated like a single. ++ ++ >>> list(_ancestry('//b//d///f//')) ++ ['//b//d///f', '//b//d', '//b'] + """ + path = path.rstrip(posixpath.sep) +- while path and path != posixpath.sep: ++ while path.rstrip(posixpath.sep): + yield path + path, tail = posixpath.split(path) + +@@ -2381,7 +2387,7 @@ + + @property + def name(self): +- return pathlib.Path(self.at).name or self.filename.name ++ return pathlib.PurePosixPath(self.at).name or self.filename.name + + @property + def filename(self): --- /dev/null +++ b/Mac/Resources/app-store-compliance.patch @@ -0,0 +1 @@ @@ -5946,6 +8333,325 @@ index fa99dd86c41..6a84a1b0c50 100644 .PHONY: frameworkinstallmaclib frameworkinstallapps frameworkinstallunixtools .PHONY: frameworkaltinstallunixtools recheck clean clobber distclean .PHONY: smelly funny patchcheck touch altmaninstall commoninstall +diff --git a/Modules/_winapi.c b/Modules/_winapi.c +index f6bb07fd8b0..5cc138188ff 100644 +--- a/Modules/_winapi.c ++++ b/Modules/_winapi.c +@@ -470,7 +470,7 @@ + { + HANDLE handle; + +- if (PySys_Audit("_winapi.CreateFile", "uIIII", ++ if (PySys_Audit("_winapi.CreateFile", "sIIII", + file_name, desired_access, share_mode, + creation_disposition, flags_and_attributes) < 0) { + return INVALID_HANDLE_VALUE; +@@ -690,7 +690,7 @@ + { + HANDLE handle; + +- if (PySys_Audit("_winapi.CreateNamedPipe", "uII", ++ if (PySys_Audit("_winapi.CreateNamedPipe", "sII", + name, open_mode, pipe_mode) < 0) { + return INVALID_HANDLE_VALUE; + } +diff --git a/Modules/expat/expat.h b/Modules/expat/expat.h +index 95464b0dd17..d0d6015a662 100644 +--- a/Modules/expat/expat.h ++++ b/Modules/expat/expat.h +@@ -18,6 +18,7 @@ + Copyright (c) 2022 Thijs Schreijer + Copyright (c) 2023 Hanno Böck + Copyright (c) 2023 Sony Corporation / Snild Dolkow ++ Copyright (c) 2024 Taichi Haradaguchi <20001722@ymail.ne.jp> + Licensed under the MIT license: + + Permission is hereby granted, free of charge, to any person obtaining +@@ -1042,7 +1043,7 @@ + XMLPARSEAPI(const XML_Feature *) + XML_GetFeatureList(void); + +-#if XML_GE == 1 ++#if defined(XML_DTD) || (defined(XML_GE) && XML_GE == 1) + /* Added in Expat 2.4.0 for XML_DTD defined and + * added in Expat 2.6.0 for XML_GE == 1. */ + XMLPARSEAPI(XML_Bool) +@@ -1065,7 +1066,7 @@ + */ + #define XML_MAJOR_VERSION 2 + #define XML_MINOR_VERSION 6 +-#define XML_MICRO_VERSION 0 ++#define XML_MICRO_VERSION 3 + + #ifdef __cplusplus + } +diff --git a/Modules/expat/internal.h b/Modules/expat/internal.h +index cce71e4c516..167ec36804a 100644 +--- a/Modules/expat/internal.h ++++ b/Modules/expat/internal.h +@@ -28,10 +28,11 @@ + Copyright (c) 2002-2003 Fred L. Drake, Jr. + Copyright (c) 2002-2006 Karl Waclawek + Copyright (c) 2003 Greg Stein +- Copyright (c) 2016-2023 Sebastian Pipping ++ Copyright (c) 2016-2024 Sebastian Pipping + Copyright (c) 2018 Yury Gribov + Copyright (c) 2019 David Loffredo +- Copyright (c) 2023 Sony Corporation / Snild Dolkow ++ Copyright (c) 2023-2024 Sony Corporation / Snild Dolkow ++ Copyright (c) 2024 Taichi Haradaguchi <20001722@ymail.ne.jp> + Licensed under the MIT license: + + Permission is hereby granted, free of charge, to any person obtaining +@@ -155,14 +156,20 @@ + void _INTERNAL_trim_to_complete_utf8_characters(const char *from, + const char **fromLimRef); + +-#if XML_GE == 1 ++#if defined(XML_GE) && XML_GE == 1 + unsigned long long testingAccountingGetCountBytesDirect(XML_Parser parser); + unsigned long long testingAccountingGetCountBytesIndirect(XML_Parser parser); + const char *unsignedCharToPrintable(unsigned char c); + #endif + +-extern XML_Bool g_reparseDeferralEnabledDefault; // written ONLY in runtests.c +-extern unsigned int g_parseAttempts; // used for testing only ++extern ++#if ! defined(XML_TESTING) ++ const ++#endif ++ XML_Bool g_reparseDeferralEnabledDefault; // written ONLY in runtests.c ++#if defined(XML_TESTING) ++extern unsigned int g_bytesScanned; // used for testing only ++#endif + + #ifdef __cplusplus + } +diff --git a/Modules/expat/siphash.h b/Modules/expat/siphash.h +index a1ed99e687b..04f6f74585b 100644 +--- a/Modules/expat/siphash.h ++++ b/Modules/expat/siphash.h +@@ -126,8 +126,7 @@ + | ((uint64_t)((p)[4]) << 32) | ((uint64_t)((p)[5]) << 40) \ + | ((uint64_t)((p)[6]) << 48) | ((uint64_t)((p)[7]) << 56)) + +-#define SIPHASH_INITIALIZER \ +- { 0, 0, 0, 0, {0}, 0, 0 } ++#define SIPHASH_INITIALIZER {0, 0, 0, 0, {0}, 0, 0} + + struct siphash { + uint64_t v0, v1, v2, v3; +diff --git a/Modules/expat/xmlparse.c b/Modules/expat/xmlparse.c +index aaf0fa9c8f9..d9285b213b3 100644 +--- a/Modules/expat/xmlparse.c ++++ b/Modules/expat/xmlparse.c +@@ -1,4 +1,4 @@ +-/* 628e24d4966bedbd4800f6ed128d06d29703765b4bce12d3b7f099f90f842fc9 (2.6.0+) ++/* ba4cdf9bdb534f355a9def4c9e25d20ee8e72f95b0a4d930be52e563f5080196 (2.6.3+) + __ __ _ + ___\ \/ /_ __ __ _| |_ + / _ \\ /| '_ \ / _` | __| +@@ -38,7 +38,8 @@ + Copyright (c) 2022 Jann Horn + Copyright (c) 2022 Sean McBride + Copyright (c) 2023 Owain Davies +- Copyright (c) 2023 Sony Corporation / Snild Dolkow ++ Copyright (c) 2023-2024 Sony Corporation / Snild Dolkow ++ Copyright (c) 2024 Berkay Eren Ürün + Licensed under the MIT license: + + Permission is hereby granted, free of charge, to any person obtaining +@@ -210,7 +211,7 @@ + #endif + + /* Round up n to be a multiple of sz, where sz is a power of 2. */ +-#define ROUND_UP(n, sz) (((n) + ((sz)-1)) & ~((sz)-1)) ++#define ROUND_UP(n, sz) (((n) + ((sz) - 1)) & ~((sz) - 1)) + + /* Do safe (NULL-aware) pointer arithmetic */ + #define EXPAT_SAFE_PTR_DIFF(p, q) (((p) && (q)) ? ((p) - (q)) : 0) +@@ -248,7 +249,7 @@ + it odd, since odd numbers are always relative prime to a power of 2. + */ + #define SECOND_HASH(hash, mask, power) \ +- ((((hash) & ~(mask)) >> ((power)-1)) & ((mask) >> 2)) ++ ((((hash) & ~(mask)) >> ((power) - 1)) & ((mask) >> 2)) + #define PROBE_STEP(hash, mask, power) \ + ((unsigned char)((SECOND_HASH(hash, mask, power)) | 1)) + +@@ -294,7 +295,7 @@ + The name of the element is stored in both the document and API + encodings. The memory buffer 'buf' is a separately-allocated + memory area which stores the name. During the XML_Parse()/ +- XMLParseBuffer() when the element is open, the memory for the 'raw' ++ XML_ParseBuffer() when the element is open, the memory for the 'raw' + version of the name (in the document encoding) is shared with the + document buffer. If the element is open across calls to + XML_Parse()/XML_ParseBuffer(), the buffer is re-allocated to +@@ -629,8 +630,14 @@ + ? 0 \ + : ((*((pool)->ptr)++ = c), 1)) + +-XML_Bool g_reparseDeferralEnabledDefault = XML_TRUE; // write ONLY in runtests.c +-unsigned int g_parseAttempts = 0; // used for testing only ++#if ! defined(XML_TESTING) ++const ++#endif ++ XML_Bool g_reparseDeferralEnabledDefault ++ = XML_TRUE; // write ONLY in runtests.c ++#if defined(XML_TESTING) ++unsigned int g_bytesScanned = 0; // used for testing only ++#endif + + struct XML_ParserStruct { + /* The first member must be m_userData so that the XML_GetUserData +@@ -1017,7 +1024,9 @@ + return XML_ERROR_NONE; + } + } +- g_parseAttempts += 1; ++#if defined(XML_TESTING) ++ g_bytesScanned += (unsigned)have_now; ++#endif + const enum XML_Error ret = parser->m_processor(parser, start, end, endPtr); + if (ret == XML_ERROR_NONE) { + // if we consumed nothing, remember what we had on this parse attempt. +@@ -2030,6 +2039,12 @@ + + if (parser == NULL) + return XML_STATUS_ERROR; ++ ++ if (len < 0) { ++ parser->m_errorCode = XML_ERROR_INVALID_ARGUMENT; ++ return XML_STATUS_ERROR; ++ } ++ + switch (parser->m_parsingStatus.parsing) { + case XML_SUSPENDED: + parser->m_errorCode = XML_ERROR_SUSPENDED; +@@ -5838,18 +5853,17 @@ + /* Set a safe default value in case 'next' does not get set */ + next = textStart; + +-#ifdef XML_DTD + if (entity->is_param) { + int tok + = XmlPrologTok(parser->m_internalEncoding, textStart, textEnd, &next); + result = doProlog(parser, parser->m_internalEncoding, textStart, textEnd, + tok, next, &next, XML_FALSE, XML_FALSE, + XML_ACCOUNT_ENTITY_EXPANSION); +- } else +-#endif /* XML_DTD */ ++ } else { + result = doContent(parser, parser->m_tagLevel, parser->m_internalEncoding, + textStart, textEnd, &next, XML_FALSE, + XML_ACCOUNT_ENTITY_EXPANSION); ++ } + + if (result == XML_ERROR_NONE) { + if (textEnd != next && parser->m_parsingStatus.parsing == XML_SUSPENDED) { +@@ -5886,18 +5900,17 @@ + /* Set a safe default value in case 'next' does not get set */ + next = textStart; + +-#ifdef XML_DTD + if (entity->is_param) { + int tok + = XmlPrologTok(parser->m_internalEncoding, textStart, textEnd, &next); + result = doProlog(parser, parser->m_internalEncoding, textStart, textEnd, + tok, next, &next, XML_FALSE, XML_TRUE, + XML_ACCOUNT_ENTITY_EXPANSION); +- } else +-#endif /* XML_DTD */ ++ } else { + result = doContent(parser, openEntity->startTagLevel, + parser->m_internalEncoding, textStart, textEnd, &next, + XML_FALSE, XML_ACCOUNT_ENTITY_EXPANSION); ++ } + + if (result != XML_ERROR_NONE) + return result; +@@ -5924,7 +5937,6 @@ + return XML_ERROR_NONE; + } + +-#ifdef XML_DTD + if (entity->is_param) { + int tok; + parser->m_processor = prologProcessor; +@@ -5932,9 +5944,7 @@ + return doProlog(parser, parser->m_encoding, s, end, tok, next, nextPtr, + (XML_Bool)! parser->m_parsingStatus.finalBuffer, XML_TRUE, + XML_ACCOUNT_DIRECT); +- } else +-#endif /* XML_DTD */ +- { ++ } else { + parser->m_processor = contentProcessor; + /* see externalEntityContentProcessor vs contentProcessor */ + result = doContent(parser, parser->m_parentParser ? 1 : 0, +@@ -6232,7 +6242,7 @@ + dtd->keepProcessing = dtd->standalone; + goto endEntityValue; + } +- if (entity->open) { ++ if (entity->open || (entity == parser->m_declEntity)) { + if (enc == parser->m_encoding) + parser->m_eventPtr = entityTextPtr; + result = XML_ERROR_RECURSIVE_ENTITY_REF; +@@ -7008,6 +7018,16 @@ + if (! newE) + return 0; + if (oldE->nDefaultAtts) { ++ /* Detect and prevent integer overflow. ++ * The preprocessor guard addresses the "always false" warning ++ * from -Wtype-limits on platforms where ++ * sizeof(int) < sizeof(size_t), e.g. on x86_64. */ ++#if UINT_MAX >= SIZE_MAX ++ if ((size_t)oldE->nDefaultAtts ++ > ((size_t)(-1) / sizeof(DEFAULT_ATTRIBUTE))) { ++ return 0; ++ } ++#endif + newE->defaultAtts + = ms->malloc_fcn(oldE->nDefaultAtts * sizeof(DEFAULT_ATTRIBUTE)); + if (! newE->defaultAtts) { +@@ -7550,6 +7570,15 @@ + int next; + + if (! dtd->scaffIndex) { ++ /* Detect and prevent integer overflow. ++ * The preprocessor guard addresses the "always false" warning ++ * from -Wtype-limits on platforms where ++ * sizeof(unsigned int) < sizeof(size_t), e.g. on x86_64. */ ++#if UINT_MAX >= SIZE_MAX ++ if (parser->m_groupSize > ((size_t)(-1) / sizeof(int))) { ++ return -1; ++ } ++#endif + dtd->scaffIndex = (int *)MALLOC(parser, parser->m_groupSize * sizeof(int)); + if (! dtd->scaffIndex) + return -1; +@@ -7779,6 +7808,8 @@ + + static float + accountingGetCurrentAmplification(XML_Parser rootParser) { ++ // 1.........1.........12 => 22 ++ const size_t lenOfShortestInclude = sizeof("") - 1; + const XmlBigCount countBytesOutput + = rootParser->m_accounting.countBytesDirect + + rootParser->m_accounting.countBytesIndirect; +@@ -7786,7 +7817,9 @@ + = rootParser->m_accounting.countBytesDirect + ? (countBytesOutput + / (float)(rootParser->m_accounting.countBytesDirect)) +- : 1.0f; ++ : ((lenOfShortestInclude ++ + rootParser->m_accounting.countBytesIndirect) ++ / (float)lenOfShortestInclude); + assert(! rootParser->m_parentParser); + return amplificationFactor; + } diff --git a/Modules/getpath.c b/Modules/getpath.c index ef6dd59a084..e8f91831286 100644 --- a/Modules/getpath.c @@ -5976,11 +8682,68 @@ index ef6dd59a084..e8f91831286 100644 status = calculate_argv0_path_framework(calculate, pathconfig); if (_PyStatus_EXCEPTION(status)) { return status; +diff --git a/Modules/main.c b/Modules/main.c +index 2684d230672..48d9247c132 100644 +--- a/Modules/main.c ++++ b/Modules/main.c +@@ -524,6 +524,10 @@ + return; + } + ++ if (PySys_Audit("cpython.run_stdin", NULL) < 0) { ++ return; ++ } ++ + PyCompilerFlags cf = _PyCompilerFlags_INIT; + int res = PyRun_AnyFileFlags(stdin, "", &cf); + *exitcode = (res != 0); +diff --git a/Modules/overlapped.c b/Modules/overlapped.c +index b9ca86cbd1f..ad8f5430eaf 100644 +--- a/Modules/overlapped.c ++++ b/Modules/overlapped.c +@@ -692,6 +692,24 @@ + if (!HasOverlappedIoCompleted(&self->overlapped) && + self->type != TYPE_NOT_STARTED) + { ++ // NOTE: We should not get here, if we do then something is wrong in ++ // the IocpProactor or ProactorEventLoop. Since everything uses IOCP if ++ // the overlapped IO hasn't completed yet then we should not be ++ // deallocating! ++ // ++ // The problem is likely that this OverlappedObject was removed from ++ // the IocpProactor._cache before it was complete. The _cache holds a ++ // reference while IO is pending so that it does not get deallocated ++ // while the kernel has retained the OVERLAPPED structure. ++ // ++ // CancelIoEx (likely called from self.cancel()) may have successfully ++ // completed, but the OVERLAPPED is still in use until either ++ // HasOverlappedIoCompleted() is true or GetQueuedCompletionStatus has ++ // returned this OVERLAPPED object. ++ // ++ // NOTE: Waiting when IOCP is in use can hang indefinitely, but this ++ // CancelIoEx is superfluous in that self.cancel() was already called, ++ // so I've only ever seen this return FALSE with GLE=ERROR_NOT_FOUND + if (Py_CancelIoEx && Py_CancelIoEx(self->handle, &self->overlapped)) + wait = TRUE; + diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c -index c0421a94c17..0298ca58304 100644 +index c0421a94c17..50397b35207 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c -@@ -326,8 +326,6 @@ +@@ -33,6 +33,12 @@ + #include "pycore_import.h" // _PyImport_ReInitLock() + #include "pycore_initconfig.h" // _PyStatus_EXCEPTION() + #include "pycore_pystate.h" // _PyInterpreterState_GET() ++ ++#ifdef MS_WINDOWS ++# include // SetEntriesInAcl ++# include // SDDL_REVISION_1 ++#endif ++ + #include "structmember.h" // PyMemberDef + #ifndef MS_WINDOWS + # include "posixmodule.h" +@@ -326,8 +332,6 @@ # else /* Unix functions that the configure script doesn't check for */ # ifndef __VXWORKS__ @@ -5989,7 +8752,7 @@ index c0421a94c17..0298ca58304 100644 # if defined(__USLC__) && defined(__SCO_VERSION__) /* SCO UDK Compiler */ # define HAVE_FORK1 1 # endif -@@ -340,7 +338,6 @@ +@@ -340,7 +344,6 @@ # define HAVE_KILL 1 # define HAVE_OPENDIR 1 # define HAVE_PIPE 1 @@ -5997,6 +8760,92 @@ index c0421a94c17..0298ca58304 100644 # define HAVE_WAIT 1 # define HAVE_TTYNAME 1 # endif /* _MSC_VER */ +@@ -592,6 +595,11 @@ + goto fatal_error; + } + ++ status = _PyRuntimeState_ReInitThreads(runtime); ++ if (_PyStatus_EXCEPTION(status)) { ++ goto fatal_error; ++ } ++ + PyThreadState *tstate = _PyThreadState_GET(); + _Py_EnsureTstateNotNULL(tstate); + +@@ -607,11 +615,6 @@ + + _PySignal_AfterFork(); + +- status = _PyRuntimeState_ReInitThreads(runtime); +- if (_PyStatus_EXCEPTION(status)) { +- goto fatal_error; +- } +- + status = _PyInterpreterState_DeleteExceptMain(runtime); + if (_PyStatus_EXCEPTION(status)) { + goto fatal_error; +@@ -4465,7 +4468,6 @@ + + #endif /* MS_WINDOWS */ + +- + /*[clinic input] + os.mkdir + +@@ -4495,6 +4497,12 @@ + /*[clinic end generated code: output=a70446903abe821f input=a61722e1576fab03]*/ + { + int result; ++#ifdef MS_WINDOWS ++ int error = 0; ++ int pathError = 0; ++ SECURITY_ATTRIBUTES secAttr = { sizeof(secAttr) }; ++ SECURITY_ATTRIBUTES *pSecAttr = NULL; ++#endif + #ifdef HAVE_MKDIRAT + int mkdirat_unavailable = 0; + #endif +@@ -4506,11 +4514,38 @@ + + #ifdef MS_WINDOWS + Py_BEGIN_ALLOW_THREADS +- result = CreateDirectoryW(path->wide, NULL); ++ if (mode == 0700 /* 0o700 */) { ++ ULONG sdSize; ++ pSecAttr = &secAttr; ++ // Set a discretionary ACL (D) that is protected (P) and includes ++ // inheritable (OICI) entries that allow (A) full control (FA) to ++ // SYSTEM (SY), Administrators (BA), and the owner (OW). ++ if (!ConvertStringSecurityDescriptorToSecurityDescriptorW( ++ L"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;OW)", ++ SDDL_REVISION_1, ++ &secAttr.lpSecurityDescriptor, ++ &sdSize ++ )) { ++ error = GetLastError(); ++ } ++ } ++ if (!error) { ++ result = CreateDirectoryW(path->wide, pSecAttr); ++ if (secAttr.lpSecurityDescriptor && ++ // uncommonly, LocalFree returns non-zero on error, but still uses ++ // GetLastError() to see what the error code is ++ LocalFree(secAttr.lpSecurityDescriptor)) { ++ error = GetLastError(); ++ } ++ } + Py_END_ALLOW_THREADS + +- if (!result) ++ if (error) { ++ return PyErr_SetFromWindowsErr(error); ++ } ++ if (!result) { + return path_error(path); ++ } + #else + Py_BEGIN_ALLOW_THREADS + #if HAVE_MKDIRAT diff --git a/Python/importlib_external.h b/Python/importlib_external.h index e77ca4c2194..724aefabdee 100644 --- a/Python/importlib_external.h @@ -11481,6 +14330,40 @@ index 50cf340e543..bd2c036bb53 100644 "_json", "_locale", "_lsprof", +diff --git a/README.rst b/README.rst +index cf64d7dbc1a..884b077edc2 100644 +--- a/README.rst ++++ b/README.rst +@@ -1,4 +1,4 @@ +-This is Python version 3.10.14 ++This is Python version 3.10.15 + ============================== + + .. image:: https://travis-ci.com/python/cpython.svg?branch=master +diff --git a/Tools/ssl/multissltests.py b/Tools/ssl/multissltests.py +index 61b87a2d22f..7b9983fe052 100755 +--- a/Tools/ssl/multissltests.py ++++ b/Tools/ssl/multissltests.py +@@ -404,15 +404,15 @@ + depend_target = 'depend' + + def _post_install(self): +- if self.version.startswith("3.0"): +- self._post_install_300() ++ if self.version.startswith("3."): ++ self._post_install_3xx() + + def _build_src(self, config_args=()): +- if self.version.startswith("3.0"): ++ if self.version.startswith("3."): + config_args += ("enable-fips",) + super()._build_src(config_args) + +- def _post_install_300(self): ++ def _post_install_3xx(self): + # create ssl/ subdir with example configs + # Install FIPS module + self._subprocess_call( diff --git a/config.sub b/config.sub index d74fb6deac9..1bb6a05dc11 100755 --- a/config.sub @@ -12015,7 +14898,7 @@ index d74fb6deac9..1bb6a05dc11 100755 # Local variables: diff --git a/configure b/configure -index 4b71c4e00f8..f8f2296e394 100755 +index 4b71c4e00f8..ffd219bff19 100755 --- a/configure +++ b/configure @@ -660,6 +660,8 @@ @@ -12295,17 +15178,17 @@ index 4b71c4e00f8..f8f2296e394 100755 +fi +if test -z "$CXX"; then + case "$host" in -+ aarch64-apple-ios*-simulator) CXX=arm64-apple-ios-simulator-clang ;; -+ aarch64-apple-ios*) CXX=arm64-apple-ios-clang ;; -+ x86_64-apple-ios*-simulator) CXX=x86_64-apple-ios-simulator-clang ;; ++ aarch64-apple-ios*-simulator) CXX=arm64-apple-ios-simulator-clang++ ;; ++ aarch64-apple-ios*) CXX=arm64-apple-ios-clang++ ;; ++ x86_64-apple-ios*-simulator) CXX=x86_64-apple-ios-simulator-clang++ ;; + -+ aarch64-apple-tvos*-simulator) CXX=arm64-apple-tvos-simulator-clang ;; -+ aarch64-apple-tvos*) CXX=arm64-apple-tvos-clang ;; -+ x86_64-apple-tvos*-simulator) CXX=x86_64-apple-tvos-simulator-clang ;; ++ aarch64-apple-tvos*-simulator) CXX=arm64-apple-tvos-simulator-clang++ ;; ++ aarch64-apple-tvos*) CXX=arm64-apple-tvos-clang++ ;; ++ x86_64-apple-tvos*-simulator) CXX=x86_64-apple-tvos-simulator-clang++ ;; + -+ aarch64-apple-watchos*-simulator) CXX=arm64-apple-watchos-simulator-clang ;; -+ aarch64-apple-watchos*) CXX=arm64_32-apple-watchos-clang ;; -+ x86_64-apple-watchos*-simulator) CXX=x86_64-apple-watchos-simulator-clang ;; ++ aarch64-apple-watchos*-simulator) CXX=arm64-apple-watchos-simulator-clang++ ;; ++ aarch64-apple-watchos*) CXX=arm64_32-apple-watchos-clang++ ;; ++ x86_64-apple-watchos*-simulator) CXX=x86_64-apple-watchos-simulator-clang++ ;; + *) + esac +fi @@ -13524,7 +16407,7 @@ index 4b71c4e00f8..f8f2296e394 100755 "Misc/python.pc") CONFIG_FILES="$CONFIG_FILES Misc/python.pc" ;; "Misc/python-embed.pc") CONFIG_FILES="$CONFIG_FILES Misc/python-embed.pc" ;; diff --git a/configure.ac b/configure.ac -index ac3be3850a9..4915646e0fb 100644 +index ac3be3850a9..4bfd669aa87 100644 --- a/configure.ac +++ b/configure.ac @@ -71,7 +71,7 @@ @@ -13688,17 +16571,17 @@ index ac3be3850a9..4915646e0fb 100644 +fi +if test -z "$CXX"; then + case "$host" in -+ aarch64-apple-ios*-simulator) CXX=arm64-apple-ios-simulator-clang ;; -+ aarch64-apple-ios*) CXX=arm64-apple-ios-clang ;; -+ x86_64-apple-ios*-simulator) CXX=x86_64-apple-ios-simulator-clang ;; ++ aarch64-apple-ios*-simulator) CXX=arm64-apple-ios-simulator-clang++ ;; ++ aarch64-apple-ios*) CXX=arm64-apple-ios-clang++ ;; ++ x86_64-apple-ios*-simulator) CXX=x86_64-apple-ios-simulator-clang++ ;; + -+ aarch64-apple-tvos*-simulator) CXX=arm64-apple-tvos-simulator-clang ;; -+ aarch64-apple-tvos*) CXX=arm64-apple-tvos-clang ;; -+ x86_64-apple-tvos*-simulator) CXX=x86_64-apple-tvos-simulator-clang ;; ++ aarch64-apple-tvos*-simulator) CXX=arm64-apple-tvos-simulator-clang++ ;; ++ aarch64-apple-tvos*) CXX=arm64-apple-tvos-clang++ ;; ++ x86_64-apple-tvos*-simulator) CXX=x86_64-apple-tvos-simulator-clang++ ;; + -+ aarch64-apple-watchos*-simulator) CXX=arm64-apple-watchos-simulator-clang ;; -+ aarch64-apple-watchos*) CXX=arm64_32-apple-watchos-clang ;; -+ x86_64-apple-watchos*-simulator) CXX=x86_64-apple-watchos-simulator-clang ;; ++ aarch64-apple-watchos*-simulator) CXX=arm64-apple-watchos-simulator-clang++ ;; ++ aarch64-apple-watchos*) CXX=arm64_32-apple-watchos-clang++ ;; ++ x86_64-apple-watchos*-simulator) CXX=x86_64-apple-watchos-simulator-clang++ ;; + *) + esac +fi @@ -15176,6 +18059,11 @@ index ac3be3850a9..4915646e0fb 100644 +#!/bin/sh +xcrun --sdk iphoneos${IOS_SDK_VERSION} clang -target arm64-apple-ios $@ --- /dev/null ++++ b/iOS/Resources/bin/arm64-apple-ios-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphoneos${IOS_SDK_VERSION} clang++ -target arm64-apple-ios $@ +--- /dev/null +++ b/iOS/Resources/bin/arm64-apple-ios-cpp @@ -0,0 +1,2 @@ +#!/bin/sh @@ -15191,6 +18079,11 @@ index ac3be3850a9..4915646e0fb 100644 +#!/bin/sh +xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target arm64-apple-ios-simulator $@ --- /dev/null ++++ b/iOS/Resources/bin/arm64-apple-ios-simulator-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target arm64-apple-ios-simulator $@ +--- /dev/null +++ b/iOS/Resources/bin/arm64-apple-ios-simulator-cpp @@ -0,0 +1,2 @@ +#!/bin/sh @@ -15206,6 +18099,11 @@ index ac3be3850a9..4915646e0fb 100644 +#!/bin/sh +xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang -target x86_64-apple-ios-simulator $@ --- /dev/null ++++ b/iOS/Resources/bin/x86_64-apple-ios-simulator-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/sh ++xcrun --sdk iphonesimulator${IOS_SDK_VERSION} clang++ -target x86_64-apple-ios-simulator $@ +--- /dev/null +++ b/iOS/Resources/bin/x86_64-apple-ios-simulator-cpp @@ -0,0 +1,2 @@ +#!/bin/sh @@ -16510,6 +19408,11 @@ index a39610a1c7c..09ea06965e9 100644 +#!/bin/bash +xcrun --sdk appletvos${TVOS_SDK_VERSION} clang -target arm64-apple-tvos $@ --- /dev/null ++++ b/tvOS/Resources/bin/arm64-apple-tvos-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk appletvos${TVOS_SDK_VERSION} clang++ -target arm64-apple-tvos $@ +--- /dev/null +++ b/tvOS/Resources/bin/arm64-apple-tvos-cpp @@ -0,0 +1,2 @@ +#!/bin/bash @@ -16525,6 +19428,11 @@ index a39610a1c7c..09ea06965e9 100644 +#!/bin/bash +xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} clang -target arm64-apple-tvos-simulator $@ --- /dev/null ++++ b/tvOS/Resources/bin/arm64-apple-tvos-simulator-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} clang++ -target arm64-apple-tvos-simulator $@ +--- /dev/null +++ b/tvOS/Resources/bin/arm64-apple-tvos-simulator-cpp @@ -0,0 +1,2 @@ +#!/bin/bash @@ -16540,6 +19448,11 @@ index a39610a1c7c..09ea06965e9 100644 +#!/bin/bash +xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} clang -target x86_64-apple-tvos-simulator $@ --- /dev/null ++++ b/tvOS/Resources/bin/x86_64-apple-tvos-simulator-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk appletvsimulator${TVOS_SDK_VERSION} clang++ -target x86_64-apple-tvos-simulator $@ +--- /dev/null +++ b/tvOS/Resources/bin/x86_64-apple-tvos-simulator-cpp @@ -0,0 +1,2 @@ +#!/bin/bash @@ -16742,6 +19655,11 @@ index a39610a1c7c..09ea06965e9 100644 +#!/bin/bash +xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} clang -target arm64-apple-watchos-simulator $@ --- /dev/null ++++ b/watchOS/Resources/bin/arm64-apple-watchos-simulator-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} clang++ -target arm64-apple-watchos-simulator $@ +--- /dev/null +++ b/watchOS/Resources/bin/arm64-apple-watchos-simulator-cpp @@ -0,0 +1,2 @@ +#!/bin/bash @@ -16757,6 +19675,11 @@ index a39610a1c7c..09ea06965e9 100644 +#!/bin/bash +xcrun --sdk watchos${WATCHOS_SDK_VERSION} clang -target arm64_32-apple-watchos $@ --- /dev/null ++++ b/watchOS/Resources/bin/arm64_32-apple-watchos-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk watchos${WATCHOS_SDK_VERSION} clang++ -target arm64_32-apple-watchos $@ +--- /dev/null +++ b/watchOS/Resources/bin/arm64_32-apple-watchos-cpp @@ -0,0 +1,2 @@ +#!/bin/bash @@ -16772,6 +19695,11 @@ index a39610a1c7c..09ea06965e9 100644 +#!/bin/bash +xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} clang -target x86_64-apple-watchos-simulator $@ --- /dev/null ++++ b/watchOS/Resources/bin/x86_64-apple-watchos-simulator-clang++ +@@ -0,0 +1,2 @@ ++#!/bin/bash ++xcrun --sdk watchsimulator${WATCHOS_SDK_VERSION} clang++ -target x86_64-apple-watchos-simulator $@ +--- /dev/null +++ b/watchOS/Resources/bin/x86_64-apple-watchos-simulator-cpp @@ -0,0 +1,2 @@ +#!/bin/bash diff --git a/patch/Python/release.macOS.exclude b/patch/Python/release.macOS.exclude index f45bf7f..3bc247c 100644 --- a/patch/Python/release.macOS.exclude +++ b/patch/Python/release.macOS.exclude @@ -2,7 +2,21 @@ # when building macOS Python-Apple-support tarballs from the official Framework # It is used by `tar -X` during the Makefile build. # +._Headers +._Python +._Resources +Resources/._Python.app Resources/Python.app +Versions/._Current +Versions/*/.__CodeSignature +Versions/*/._bin +Versions/*/._etc +Versions/*/._Frameworks +Versions/*/._Headers +Versions/*/._include +Versions/*/._lib +Versions/*/._Resources +Versions/*/._share Versions/*/bin Versions/*/etc Versions/*/Frameworks