Skip to content

Commit

Permalink
fix t.web.test.test_cgi.CGIScriptTests.test_urlParameters on <3.8.8 b…
Browse files Browse the repository at this point in the history
…y backporting parse_qsl
  • Loading branch information
graingert committed Aug 18, 2023
1 parent 7a0f110 commit a436220
Showing 1 changed file with 118 additions and 3 deletions.
121 changes: 118 additions & 3 deletions src/twisted/web/_cgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import os
import sys
import tempfile
import urllib.parse
from collections.abc import Mapping
from email.message import Message
from email.parser import FeedParser
Expand All @@ -25,6 +24,122 @@
"""


# backport parse_qsl with separator kwarg
# https://github.com/python/cpython/issues/87133
if sys.version_info >= (3, 8, 8):
from urllib.parse import parse_qsl as _parse_qsl
else:
from urllib.parse import unquote

# Helpers for bytes handling
# For 3.2, we deliberately require applications that
# handle improperly quoted URLs to do their own
# decoding and encoding. If valid use cases are
# presented, we may relax this by using latin-1
# decoding internally for 3.3
_implicit_encoding = "ascii"
_implicit_errors = "strict"

def _noop(obj):
return obj

def _encode_result(obj, encoding=_implicit_encoding, errors=_implicit_errors):
return obj.encode(encoding, errors)

Check warning on line 47 in src/twisted/web/_cgi.py

View check run for this annotation

Codecov / codecov/patch

src/twisted/web/_cgi.py#L47

Added line #L47 was not covered by tests

def _decode_args(args, encoding=_implicit_encoding, errors=_implicit_errors):
return tuple(x.decode(encoding, errors) if x else "" for x in args)

def _coerce_args(*args):
# Invokes decode if necessary to create str args
# and returns the coerced inputs along with
# an appropriate result coercion function
# - noop for str inputs
# - encoding function otherwise
str_input = isinstance(args[0], str)
for arg in args[1:]:
# We special-case the empty string to support the
# "scheme=''" default argument to some functions
if arg and isinstance(arg, str) != str_input:
raise TypeError("Cannot mix str and non-str arguments")

Check warning on line 63 in src/twisted/web/_cgi.py

View check run for this annotation

Codecov / codecov/patch

src/twisted/web/_cgi.py#L63

Added line #L63 was not covered by tests
if str_input:
return args + (_noop,)
return _decode_args(args) + (_encode_result,)

Check warning on line 66 in src/twisted/web/_cgi.py

View check run for this annotation

Codecov / codecov/patch

src/twisted/web/_cgi.py#L66

Added line #L66 was not covered by tests

def _parse_qsl(
qs,
keep_blank_values=False,
strict_parsing=False,
encoding="utf-8",
errors="replace",
max_num_fields=None,
separator="&",
):
"""Parse a query given as a string argument.
Arguments:
qs: percent-encoded query string to be parsed
keep_blank_values: flag indicating whether blank values in
percent-encoded queries should be treated as blank strings.
A true value indicates that blanks should be retained as blank
strings. The default false value indicates that blank values
are to be ignored and treated as if they were not included.
strict_parsing: flag indicating what to do with parsing errors. If
false (the default), errors are silently ignored. If true,
errors raise a ValueError exception.
encoding and errors: specify how to decode percent-encoded sequences
into Unicode characters, as accepted by the bytes.decode() method.
max_num_fields: int. If set, then throws a ValueError
if there are more than n fields read by parse_qsl().
separator: str. The symbol to use for separating the query arguments.
Defaults to &.
Returns a list, as G-d intended.
"""
qs, _coerce_result = _coerce_args(qs)
separator, _ = _coerce_args(separator)

if not separator or (not isinstance(separator, (str, bytes))):
raise ValueError("Separator must be of type string or bytes.")

Check warning on line 108 in src/twisted/web/_cgi.py

View check run for this annotation

Codecov / codecov/patch

src/twisted/web/_cgi.py#L108

Added line #L108 was not covered by tests

# If max_num_fields is defined then check that the number of fields
# is less than max_num_fields. This prevents a memory exhaustion DOS
# attack via post bodies with many fields.
if max_num_fields is not None:
num_fields = 1 + qs.count(separator)

Check warning on line 114 in src/twisted/web/_cgi.py

View check run for this annotation

Codecov / codecov/patch

src/twisted/web/_cgi.py#L114

Added line #L114 was not covered by tests
if max_num_fields < num_fields:
raise ValueError("Max number of fields exceeded")

Check warning on line 116 in src/twisted/web/_cgi.py

View check run for this annotation

Codecov / codecov/patch

src/twisted/web/_cgi.py#L116

Added line #L116 was not covered by tests

pairs = [s1 for s1 in qs.split(separator)]
r = []
for name_value in pairs:
if not name_value and not strict_parsing:
continue

Check warning on line 122 in src/twisted/web/_cgi.py

View check run for this annotation

Codecov / codecov/patch

src/twisted/web/_cgi.py#L122

Added line #L122 was not covered by tests
nv = name_value.split("=", 1)
if len(nv) != 2:
if strict_parsing:
raise ValueError("bad query field: %r" % (name_value,))

Check warning on line 126 in src/twisted/web/_cgi.py

View check run for this annotation

Codecov / codecov/patch

src/twisted/web/_cgi.py#L126

Added line #L126 was not covered by tests
# Handle case of a control-name with no equal sign
if keep_blank_values:
nv.append("")

Check warning on line 129 in src/twisted/web/_cgi.py

View check run for this annotation

Codecov / codecov/patch

src/twisted/web/_cgi.py#L129

Added line #L129 was not covered by tests
else:
continue

Check warning on line 131 in src/twisted/web/_cgi.py

View check run for this annotation

Codecov / codecov/patch

src/twisted/web/_cgi.py#L131

Added line #L131 was not covered by tests
if len(nv[1]) or keep_blank_values:
name = nv[0].replace("+", " ")
name = unquote(name, encoding=encoding, errors=errors)
name = _coerce_result(name)
value = nv[1].replace("+", " ")
value = unquote(value, encoding=encoding, errors=errors)
value = _coerce_result(value)
r.append((name, value))
return r


def _parseparam(s):
while s[:1] == ";":
s = s[1:]
Expand Down Expand Up @@ -424,7 +539,7 @@ def read_urlencoded(self):
qs = qs.decode(self.encoding, self.errors)
if self.qs_on_post:
qs += "&" + self.qs_on_post

Check warning on line 541 in src/twisted/web/_cgi.py

View check run for this annotation

Codecov / codecov/patch

src/twisted/web/_cgi.py#L541

Added line #L541 was not covered by tests
query = urllib.parse.parse_qsl(
query = _parse_qsl(
qs,
self.keep_blank_values,
self.strict_parsing,
Expand All @@ -445,7 +560,7 @@ def read_multi(self, environ, keep_blank_values, strict_parsing):
raise ValueError("Invalid boundary in multipart form: %r" % (ib,))

Check warning on line 560 in src/twisted/web/_cgi.py

View check run for this annotation

Codecov / codecov/patch

src/twisted/web/_cgi.py#L560

Added line #L560 was not covered by tests
self.list = []
if self.qs_on_post:
query = urllib.parse.parse_qsl(
query = _parse_qsl(

Check warning on line 563 in src/twisted/web/_cgi.py

View check run for this annotation

Codecov / codecov/patch

src/twisted/web/_cgi.py#L563

Added line #L563 was not covered by tests
self.qs_on_post,
self.keep_blank_values,
self.strict_parsing,
Expand Down

0 comments on commit a436220

Please sign in to comment.