diff --git a/0004-Remove-the-usage-of-cgi.parse_multipart.patch b/0004-Remove-the-usage-of-cgi.parse_multipart.patch new file mode 100644 index 0000000..ce90350 --- /dev/null +++ b/0004-Remove-the-usage-of-cgi.parse_multipart.patch @@ -0,0 +1,137 @@ +From 29353a92f01280ee2af3cc25ec80032db28a4e98 Mon Sep 17 00:00:00 2001 +From: Adi Roiban +Date: Tue, 16 Jan 2024 10:52:21 +0100 +Subject: [PATCH] Remove the usage of cgi.parse_multipart and replace with + email module + +--- + src/twisted/web/http.py | 87 ++++++++++++++++++++--------------------- + 1 file changed, 42 insertions(+), 45 deletions(-) + +diff --git a/src/twisted/web/http.py b/src/twisted/web/http.py +index 2bad147..d4b12d8 100644 +--- a/src/twisted/web/http.py ++++ b/src/twisted/web/http.py +@@ -100,13 +100,14 @@ __all__ = [ + import base64 + import binascii + import calendar +-import cgi + import math + import os + import re + import tempfile + import time + import warnings ++from email import message_from_bytes ++from email.message import EmailMessage + from io import BytesIO + from typing import AnyStr, Callable, List, Optional, Tuple + from urllib.parse import ( +@@ -224,15 +225,40 @@ weekdayname_lower = [name.lower() for name in weekdayname] + monthname_lower = [name and name.lower() for name in monthname] + + +-def _parseHeader(line): +- # cgi.parse_header requires a str +- key, pdict = cgi.parse_header(line.decode("charmap")) ++def _parseContentType(line: bytes) -> bytes: ++ """ ++ Parse the Content-Type header. ++ """ ++ msg = EmailMessage() ++ msg["content-type"] = line.decode("charmap") ++ key = msg.get_content_type() ++ encodedKey = key.encode("charmap") ++ return encodedKey ++ ++ ++class _MultiPartParseException(Exception): ++ """ ++ Failed to parse the multipart/form-data payload. ++ """ ++ + +- # We want the key as bytes, and cgi.parse_multipart (which consumes +- # pdict) expects a dict of str keys but bytes values +- key = key.encode("charmap") +- pdict = {x: y.encode("charmap") for x, y in pdict.items()} +- return (key, pdict) ++def _getMultiPartArgs(content, ctype): ++ """ ++ Parse the content of a multipart/form-data request. ++ """ ++ result = {} ++ multiPartHeaders = b"MIME-Version: 1.0\r\n" + b"Content-Type: " + ctype + b"\r\n" ++ msg = message_from_bytes(multiPartHeaders + content) ++ if not msg.is_multipart(): ++ raise _MultiPartParseException("Not a multipart.") ++ ++ for part in msg.get_payload(): ++ name = part.get_param("name", header="content-disposition") ++ if not name: ++ continue ++ payload = part.get_payload(decode=True) ++ result[name.encode("utf8")] = [payload] ++ return result + + + def urlparse(url): +@@ -973,47 +999,18 @@ class Request: + + if self.method == b"POST" and ctype and clength: + mfd = b"multipart/form-data" +- key, pdict = _parseHeader(ctype) +- # This weird CONTENT-LENGTH param is required by +- # cgi.parse_multipart() in some versions of Python 3.7+, see +- # bpo-29979. It looks like this will be relaxed and backported, see +- # https://github.com/python/cpython/pull/8530. +- pdict["CONTENT-LENGTH"] = clength ++ key = _parseContentType(ctype) + if key == b"application/x-www-form-urlencoded": + args.update(parse_qs(self.content.read(), 1)) + elif key == mfd: + try: +- cgiArgs = cgi.parse_multipart( +- self.content, +- pdict, +- encoding="utf8", +- errors="surrogateescape", +- ) +- +- # The parse_multipart function on Python 3.7+ +- # decodes the header bytes as iso-8859-1 and +- # decodes the body bytes as utf8 with +- # surrogateescape -- we want bytes +- self.args.update( +- { +- x.encode("iso-8859-1"): [ +- z.encode("utf8", "surrogateescape") +- if isinstance(z, str) +- else z +- for z in y +- ] +- for x, y in cgiArgs.items() +- if isinstance(x, str) +- } +- ) +- except Exception as e: +- # It was a bad request, or we got a signal. ++ self.content.seek(0) ++ content = self.content.read() ++ self.args.update(_getMultiPartArgs(content, ctype)) ++ except _MultiPartParseException: ++ # It was a bad request. + self.channel._respondToBadRequestAndDisconnect() +- if isinstance(e, (TypeError, ValueError, KeyError)): +- return +- else: +- # If it's not a userspace error from CGI, reraise +- raise ++ return + + self.content.seek(0, 0) + +-- +2.43.0 + diff --git a/0005-Update-dis.findlinestarts-for-Python-3.13.patch b/0005-Update-dis.findlinestarts-for-Python-3.13.patch new file mode 100644 index 0000000..f1220af --- /dev/null +++ b/0005-Update-dis.findlinestarts-for-Python-3.13.patch @@ -0,0 +1,42 @@ +From 5a45846df7bc8cbc683b2b19ba11d20bb7f6fdf1 Mon Sep 17 00:00:00 2001 +From: Adi Roiban +Date: Tue, 16 Jan 2024 14:42:20 +0100 +Subject: [PATCH] Update dis.findlinestarts for Python 3.13 + +--- + src/twisted/python/deprecate.py | 6 +++++- + src/twisted/trial/_synctest.py | 1 + + 2 files changed, 6 insertions(+), 1 deletion(-) + +diff --git a/src/twisted/python/deprecate.py b/src/twisted/python/deprecate.py +index c85b98d..08d37eb 100644 +--- a/src/twisted/python/deprecate.py ++++ b/src/twisted/python/deprecate.py +@@ -606,7 +606,11 @@ def warnAboutFunction(offender, warningString): + warningString, + category=DeprecationWarning, + filename=inspect.getabsfile(offenderModule), +- lineno=max(lineNumber for _, lineNumber in findlinestarts(offender.__code__)), ++ lineno=max( ++ lineNumber ++ for _, lineNumber in findlinestarts(offender.__code__) ++ if lineNumber is not None ++ ), + module=offenderModule.__name__, + registry=offender.__globals__.setdefault("__warningregistry__", {}), + module_globals=None, +diff --git a/src/twisted/trial/_synctest.py b/src/twisted/trial/_synctest.py +index 2cffc2c..8d8e9b1 100644 +--- a/src/twisted/trial/_synctest.py ++++ b/src/twisted/trial/_synctest.py +@@ -1192,6 +1192,7 @@ class SynchronousTestCase(_Assertions): + lineNumbers = [ + lineNumber + for _, lineNumber in _findlinestarts(aFunction.__code__) ++ if lineNumber is not None + ] + if not (min(lineNumbers) <= aWarning.lineno <= max(lineNumbers)): + continue +-- +2.43.0 + diff --git a/python-twisted.spec b/python-twisted.spec index d14b0ce..1c5c4c6 100644 --- a/python-twisted.spec +++ b/python-twisted.spec @@ -23,6 +23,17 @@ Patch2: 0002-23.10.0-fix-and-skip-tests-fedora.patch # https://github.com/twisted/twisted/pull/12054 Patch3: 0003-python3.12.1.patch +# Three backported upstream commits to remove cgi module (removed from Python 3.13) +# https://github.com/twisted/twisted/commit/e6bf82b0a703e4bc78934d +# https://github.com/twisted/twisted/commit/2bceedc79f86c750f27432 +# https://github.com/twisted/twisted/commit/4579398f6b089f93181ba2 +Patch4: 0004-Remove-the-usage-of-cgi.parse_multipart.patch + +# In Python 3.13 line numbers returned by findlinestarts +# can be None for bytecode that does not map to source lines. +# https://github.com/twisted/twisted/pull/12059 +Patch5: 0005-Update-dis.findlinestarts-for-Python-3.13.patch + BuildArch: noarch %description %{common_description}