Commit 5c23b8e6 authored by Victor Stinner's avatar Victor Stinner

Issue #4953: cgi.FieldStorage and cgi.parse() parse the request as bytes, not

as unicode, and accept binary files. Add encoding and errors attributes to
cgi.FieldStorage.
parent 1d87deb6
...@@ -31,13 +31,15 @@ __version__ = "2.6" ...@@ -31,13 +31,15 @@ __version__ = "2.6"
# Imports # Imports
# ======= # =======
from io import StringIO from io import StringIO, BytesIO, TextIOWrapper
import sys import sys
import os import os
import urllib.parse import urllib.parse
import email.parser from email.parser import FeedParser
from warnings import warn from warnings import warn
import html import html
import locale
import tempfile
__all__ = ["MiniFieldStorage", "FieldStorage", __all__ = ["MiniFieldStorage", "FieldStorage",
"parse", "parse_qs", "parse_qsl", "parse_multipart", "parse", "parse_qs", "parse_qsl", "parse_multipart",
...@@ -109,7 +111,7 @@ def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0): ...@@ -109,7 +111,7 @@ def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
Arguments, all optional: Arguments, all optional:
fp : file pointer; default: sys.stdin fp : file pointer; default: sys.stdin.buffer
environ : environment dictionary; default: os.environ environ : environment dictionary; default: os.environ
...@@ -126,6 +128,18 @@ def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0): ...@@ -126,6 +128,18 @@ def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
""" """
if fp is None: if fp is None:
fp = sys.stdin fp = sys.stdin
# field keys and values (except for files) are returned as strings
# an encoding is required to decode the bytes read from self.fp
if hasattr(fp,'encoding'):
encoding = fp.encoding
else:
encoding = 'latin-1'
# fp.read() must return bytes
if isinstance(fp, TextIOWrapper):
fp = fp.buffer
if not 'REQUEST_METHOD' in environ: if not 'REQUEST_METHOD' in environ:
environ['REQUEST_METHOD'] = 'GET' # For testing stand-alone environ['REQUEST_METHOD'] = 'GET' # For testing stand-alone
if environ['REQUEST_METHOD'] == 'POST': if environ['REQUEST_METHOD'] == 'POST':
...@@ -136,7 +150,7 @@ def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0): ...@@ -136,7 +150,7 @@ def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
clength = int(environ['CONTENT_LENGTH']) clength = int(environ['CONTENT_LENGTH'])
if maxlen and clength > maxlen: if maxlen and clength > maxlen:
raise ValueError('Maximum content length exceeded') raise ValueError('Maximum content length exceeded')
qs = fp.read(clength) qs = fp.read(clength).decode(encoding)
else: else:
qs = '' # Unknown content-type qs = '' # Unknown content-type
if 'QUERY_STRING' in environ: if 'QUERY_STRING' in environ:
...@@ -154,7 +168,8 @@ def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0): ...@@ -154,7 +168,8 @@ def parse(fp=None, environ=os.environ, keep_blank_values=0, strict_parsing=0):
else: else:
qs = "" qs = ""
environ['QUERY_STRING'] = qs # XXX Shouldn't, really environ['QUERY_STRING'] = qs # XXX Shouldn't, really
return urllib.parse.parse_qs(qs, keep_blank_values, strict_parsing) return urllib.parse.parse_qs(qs, keep_blank_values, strict_parsing,
encoding=encoding)
# parse query string function called from urlparse, # parse query string function called from urlparse,
...@@ -236,8 +251,8 @@ def parse_multipart(fp, pdict): ...@@ -236,8 +251,8 @@ def parse_multipart(fp, pdict):
if not line: if not line:
terminator = lastpart # End outer loop terminator = lastpart # End outer loop
break break
if line[:2] == "--": if line.startswith("--"):
terminator = line.strip() terminator = line.rstrip()
if terminator in (nextpart, lastpart): if terminator in (nextpart, lastpart):
break break
lines.append(line) lines.append(line)
...@@ -352,9 +367,10 @@ class FieldStorage: ...@@ -352,9 +367,10 @@ class FieldStorage:
value: the value as a *string*; for file uploads, this value: the value as a *string*; for file uploads, this
transparently reads the file every time you request the value transparently reads the file every time you request the value
and returns *bytes*
file: the file(-like) object from which you can read the data; file: the file(-like) object from which you can read the data *as
None if the data is stored a simple string bytes* ; None if the data is stored a simple string
type: the content-type, or None if not specified type: the content-type, or None if not specified
...@@ -375,15 +391,18 @@ class FieldStorage: ...@@ -375,15 +391,18 @@ class FieldStorage:
directory and unlinking them as soon as they have been opened. directory and unlinking them as soon as they have been opened.
""" """
def __init__(self, fp=None, headers=None, outerboundary=b'',
def __init__(self, fp=None, headers=None, outerboundary="", environ=os.environ, keep_blank_values=0, strict_parsing=0,
environ=os.environ, keep_blank_values=0, strict_parsing=0): limit=None, encoding='utf-8', errors='replace'):
"""Constructor. Read multipart/* until last part. """Constructor. Read multipart/* until last part.
Arguments, all optional: Arguments, all optional:
fp : file pointer; default: sys.stdin fp : file pointer; default: sys.stdin.buffer
(not used when the request method is GET) (not used when the request method is GET)
Can be :
1. a TextIOWrapper object
2. an object whose read() and readline() methods return bytes
headers : header dictionary-like object; default: headers : header dictionary-like object; default:
taken from environ as per CGI spec taken from environ as per CGI spec
...@@ -404,6 +423,16 @@ class FieldStorage: ...@@ -404,6 +423,16 @@ class FieldStorage:
If false (the default), errors are silently ignored. If false (the default), errors are silently ignored.
If true, errors raise a ValueError exception. If true, errors raise a ValueError exception.
limit : used internally to read parts of multipart/form-data forms,
to exit from the reading loop when reached. It is the difference
between the form content-length and the number of bytes already
read
encoding, errors : the encoding and error handler used to decode the
binary stream to strings. Must be the same as the charset defined
for the page sending the form (content-type : meta http-equiv or
header)
""" """
method = 'GET' method = 'GET'
self.keep_blank_values = keep_blank_values self.keep_blank_values = keep_blank_values
...@@ -418,7 +447,8 @@ class FieldStorage: ...@@ -418,7 +447,8 @@ class FieldStorage:
qs = sys.argv[1] qs = sys.argv[1]
else: else:
qs = "" qs = ""
fp = StringIO(qs) qs = qs.encode(locale.getpreferredencoding(), 'surrogateescape')
fp = BytesIO(qs)
if headers is None: if headers is None:
headers = {'content-type': headers = {'content-type':
"application/x-www-form-urlencoded"} "application/x-www-form-urlencoded"}
...@@ -433,10 +463,26 @@ class FieldStorage: ...@@ -433,10 +463,26 @@ class FieldStorage:
self.qs_on_post = environ['QUERY_STRING'] self.qs_on_post = environ['QUERY_STRING']
if 'CONTENT_LENGTH' in environ: if 'CONTENT_LENGTH' in environ:
headers['content-length'] = environ['CONTENT_LENGTH'] headers['content-length'] = environ['CONTENT_LENGTH']
self.fp = fp or sys.stdin if fp is None:
self.fp = sys.stdin.buffer
# self.fp.read() must return bytes
elif isinstance(fp, TextIOWrapper):
self.fp = fp.buffer
else:
self.fp = fp
self.encoding = encoding
self.errors = errors
self.headers = headers self.headers = headers
if not isinstance(outerboundary, bytes):
raise TypeError('outerboundary must be bytes, not %s'
% type(outerboundary).__name__)
self.outerboundary = outerboundary self.outerboundary = outerboundary
self.bytes_read = 0
self.limit = limit
# Process content-disposition header # Process content-disposition header
cdisp, pdict = "", {} cdisp, pdict = "", {}
if 'content-disposition' in self.headers: if 'content-disposition' in self.headers:
...@@ -449,6 +495,7 @@ class FieldStorage: ...@@ -449,6 +495,7 @@ class FieldStorage:
self.filename = None self.filename = None
if 'filename' in pdict: if 'filename' in pdict:
self.filename = pdict['filename'] self.filename = pdict['filename']
self._binary_file = self.filename is not None
# Process content-type header # Process content-type header
# #
...@@ -470,9 +517,11 @@ class FieldStorage: ...@@ -470,9 +517,11 @@ class FieldStorage:
ctype, pdict = 'application/x-www-form-urlencoded', {} ctype, pdict = 'application/x-www-form-urlencoded', {}
self.type = ctype self.type = ctype
self.type_options = pdict self.type_options = pdict
self.innerboundary = ""
if 'boundary' in pdict: if 'boundary' in pdict:
self.innerboundary = pdict['boundary'] self.innerboundary = pdict['boundary'].encode(self.encoding)
else:
self.innerboundary = b""
clen = -1 clen = -1
if 'content-length' in self.headers: if 'content-length' in self.headers:
try: try:
...@@ -482,6 +531,8 @@ class FieldStorage: ...@@ -482,6 +531,8 @@ class FieldStorage:
if maxlen and clen > maxlen: if maxlen and clen > maxlen:
raise ValueError('Maximum content length exceeded') raise ValueError('Maximum content length exceeded')
self.length = clen self.length = clen
if self.limit is None and clen:
self.limit = clen
self.list = self.file = None self.list = self.file = None
self.done = 0 self.done = 0
...@@ -582,12 +633,18 @@ class FieldStorage: ...@@ -582,12 +633,18 @@ class FieldStorage:
def read_urlencoded(self): def read_urlencoded(self):
"""Internal: read data in query string format.""" """Internal: read data in query string format."""
qs = self.fp.read(self.length) qs = self.fp.read(self.length)
if not isinstance(qs, bytes):
raise ValueError("%s should return bytes, got %s" \
% (self.fp, type(qs).__name__))
qs = qs.decode(self.encoding, self.errors)
if self.qs_on_post: if self.qs_on_post:
qs += '&' + self.qs_on_post qs += '&' + self.qs_on_post
self.list = list = [] self.list = []
for key, value in urllib.parse.parse_qsl(qs, self.keep_blank_values, query = urllib.parse.parse_qsl(
self.strict_parsing): qs, self.keep_blank_values, self.strict_parsing,
list.append(MiniFieldStorage(key, value)) encoding=self.encoding, errors=self.errors)
for key, value in query:
self.list.append(MiniFieldStorage(key, value))
self.skip_lines() self.skip_lines()
FieldStorageClass = None FieldStorageClass = None
...@@ -599,24 +656,42 @@ class FieldStorage: ...@@ -599,24 +656,42 @@ class FieldStorage:
raise ValueError('Invalid boundary in multipart form: %r' % (ib,)) raise ValueError('Invalid boundary in multipart form: %r' % (ib,))
self.list = [] self.list = []
if self.qs_on_post: if self.qs_on_post:
for key, value in urllib.parse.parse_qsl(self.qs_on_post, query = urllib.parse.parse_qsl(
self.keep_blank_values, self.strict_parsing): self.qs_on_post, self.keep_blank_values, self.strict_parsing,
encoding=self.encoding, errors=self.errors)
for key, value in query:
self.list.append(MiniFieldStorage(key, value)) self.list.append(MiniFieldStorage(key, value))
FieldStorageClass = None FieldStorageClass = None
klass = self.FieldStorageClass or self.__class__ klass = self.FieldStorageClass or self.__class__
parser = email.parser.FeedParser() first_line = self.fp.readline() # bytes
# Create bogus content-type header for proper multipart parsing if not isinstance(first_line, bytes):
parser.feed('Content-Type: %s; boundary=%s\r\n\r\n' % (self.type, ib)) raise ValueError("%s should return bytes, got %s" \
parser.feed(self.fp.read()) % (self.fp, type(first_line).__name__))
full_msg = parser.close() self.bytes_read += len(first_line)
# Get subparts # first line holds boundary ; ignore it, or check that
msgs = full_msg.get_payload() # b"--" + ib == first_line.strip() ?
for msg in msgs: while True:
fp = StringIO(msg.get_payload()) parser = FeedParser()
part = klass(fp, msg, ib, environ, keep_blank_values, hdr_text = b""
strict_parsing) while True:
data = self.fp.readline()
hdr_text += data
if not data.strip():
break
if not hdr_text:
break
# parser takes strings, not bytes
self.bytes_read += len(hdr_text)
parser.feed(hdr_text.decode(self.encoding, self.errors))
headers = parser.close()
part = klass(self.fp, headers, ib, environ, keep_blank_values,
strict_parsing,self.limit-self.bytes_read,
self.encoding, self.errors)
self.bytes_read += part.bytes_read
self.list.append(part) self.list.append(part)
if self.bytes_read >= self.length:
break
self.skip_lines() self.skip_lines()
def read_single(self): def read_single(self):
...@@ -636,7 +711,11 @@ class FieldStorage: ...@@ -636,7 +711,11 @@ class FieldStorage:
todo = self.length todo = self.length
if todo >= 0: if todo >= 0:
while todo > 0: while todo > 0:
data = self.fp.read(min(todo, self.bufsize)) data = self.fp.read(min(todo, self.bufsize)) # bytes
if not isinstance(data, bytes):
raise ValueError("%s should return bytes, got %s"
% (self.fp, type(data).__name__))
self.bytes_read += len(data)
if not data: if not data:
self.done = -1 self.done = -1
break break
...@@ -645,59 +724,77 @@ class FieldStorage: ...@@ -645,59 +724,77 @@ class FieldStorage:
def read_lines(self): def read_lines(self):
"""Internal: read lines until EOF or outerboundary.""" """Internal: read lines until EOF or outerboundary."""
self.file = self.__file = StringIO() if self._binary_file:
self.file = self.__file = BytesIO() # store data as bytes for files
else:
self.file = self.__file = StringIO() # as strings for other fields
if self.outerboundary: if self.outerboundary:
self.read_lines_to_outerboundary() self.read_lines_to_outerboundary()
else: else:
self.read_lines_to_eof() self.read_lines_to_eof()
def __write(self, line): def __write(self, line):
"""line is always bytes, not string"""
if self.__file is not None: if self.__file is not None:
if self.__file.tell() + len(line) > 1000: if self.__file.tell() + len(line) > 1000:
self.file = self.make_file() self.file = self.make_file()
data = self.__file.getvalue() data = self.__file.getvalue()
self.file.write(data) self.file.write(data)
self.__file = None self.__file = None
if self._binary_file:
# keep bytes
self.file.write(line) self.file.write(line)
else:
# decode to string
self.file.write(line.decode(self.encoding, self.errors))
def read_lines_to_eof(self): def read_lines_to_eof(self):
"""Internal: read lines until EOF.""" """Internal: read lines until EOF."""
while 1: while 1:
line = self.fp.readline(1<<16) line = self.fp.readline(1<<16) # bytes
self.bytes_read += len(line)
if not line: if not line:
self.done = -1 self.done = -1
break break
self.__write(line) self.__write(line)
def read_lines_to_outerboundary(self): def read_lines_to_outerboundary(self):
"""Internal: read lines until outerboundary.""" """Internal: read lines until outerboundary.
next = "--" + self.outerboundary Data is read as bytes: boundaries and line ends must be converted
last = next + "--" to bytes for comparisons.
delim = "" """
next_boundary = b"--" + self.outerboundary
last_boundary = next_boundary + b"--"
delim = b""
last_line_lfend = True last_line_lfend = True
_read = 0
while 1: while 1:
line = self.fp.readline(1<<16) if _read >= self.limit:
break
line = self.fp.readline(1<<16) # bytes
self.bytes_read += len(line)
_read += len(line)
if not line: if not line:
self.done = -1 self.done = -1
break break
if line[:2] == "--" and last_line_lfend: if line.startswith(b"--") and last_line_lfend:
strippedline = line.strip() strippedline = line.rstrip()
if strippedline == next: if strippedline == next_boundary:
break break
if strippedline == last: if strippedline == last_boundary:
self.done = 1 self.done = 1
break break
odelim = delim odelim = delim
if line[-2:] == "\r\n": if line.endswith(b"\r\n"):
delim = "\r\n" delim = b"\r\n"
line = line[:-2] line = line[:-2]
last_line_lfend = True last_line_lfend = True
elif line[-1] == "\n": elif line.endswith(b"\n"):
delim = "\n" delim = b"\n"
line = line[:-1] line = line[:-1]
last_line_lfend = True last_line_lfend = True
else: else:
delim = "" delim = b""
last_line_lfend = False last_line_lfend = False
self.__write(odelim + line) self.__write(odelim + line)
...@@ -705,22 +802,23 @@ class FieldStorage: ...@@ -705,22 +802,23 @@ class FieldStorage:
"""Internal: skip lines until outer boundary if defined.""" """Internal: skip lines until outer boundary if defined."""
if not self.outerboundary or self.done: if not self.outerboundary or self.done:
return return
next = "--" + self.outerboundary next_boundary = b"--" + self.outerboundary
last = next + "--" last_boundary = next_boundary + b"--"
last_line_lfend = True last_line_lfend = True
while 1: while True:
line = self.fp.readline(1<<16) line = self.fp.readline(1<<16)
self.bytes_read += len(line)
if not line: if not line:
self.done = -1 self.done = -1
break break
if line[:2] == "--" and last_line_lfend: if line.endswith(b"--") and last_line_lfend:
strippedline = line.strip() strippedline = line.strip()
if strippedline == next: if strippedline == next_boundary:
break break
if strippedline == last: if strippedline == last_boundary:
self.done = 1 self.done = 1
break break
last_line_lfend = line.endswith('\n') last_line_lfend = line.endswith(b'\n')
def make_file(self): def make_file(self):
"""Overridable: return a readable & writable file. """Overridable: return a readable & writable file.
...@@ -730,7 +828,8 @@ class FieldStorage: ...@@ -730,7 +828,8 @@ class FieldStorage:
- seek(0) - seek(0)
- data is read from it - data is read from it
The file is always opened in text mode. The file is opened in binary mode for files, in text mode
for other fields
This version opens a temporary file for reading and writing, This version opens a temporary file for reading and writing,
and immediately deletes (unlinks) it. The trick (on Unix!) is and immediately deletes (unlinks) it. The trick (on Unix!) is
...@@ -745,8 +844,11 @@ class FieldStorage: ...@@ -745,8 +844,11 @@ class FieldStorage:
which unlinks the temporary files you have created. which unlinks the temporary files you have created.
""" """
import tempfile if self._binary_file:
return tempfile.TemporaryFile("w+", encoding="utf-8", newline="\n") return tempfile.TemporaryFile("wb+")
else:
return tempfile.TemporaryFile("w+",
encoding=self.encoding, newline = '\n')
# Test/debug code # Test/debug code
...@@ -910,8 +1012,12 @@ def escape(s, quote=None): ...@@ -910,8 +1012,12 @@ def escape(s, quote=None):
return s return s
def valid_boundary(s, _vb_pattern="^[ -~]{0,200}[!-~]$"): def valid_boundary(s, _vb_pattern=None):
import re import re
if isinstance(s, bytes):
_vb_pattern = b"^[ -~]{0,200}[!-~]$"
else:
_vb_pattern = "^[ -~]{0,200}[!-~]$"
return re.match(_vb_pattern, s) return re.match(_vb_pattern, s)
# Invoke mainline # Invoke mainline
......
...@@ -4,7 +4,7 @@ import os ...@@ -4,7 +4,7 @@ import os
import sys import sys
import tempfile import tempfile
import unittest import unittest
from io import StringIO from io import StringIO, BytesIO
class HackedSysModule: class HackedSysModule:
# The regression test will have real values in sys.argv, which # The regression test will have real values in sys.argv, which
...@@ -14,7 +14,6 @@ class HackedSysModule: ...@@ -14,7 +14,6 @@ class HackedSysModule:
cgi.sys = HackedSysModule() cgi.sys = HackedSysModule()
class ComparableException: class ComparableException:
def __init__(self, err): def __init__(self, err):
self.err = err self.err = err
...@@ -38,7 +37,7 @@ def do_test(buf, method): ...@@ -38,7 +37,7 @@ def do_test(buf, method):
env['REQUEST_METHOD'] = 'GET' env['REQUEST_METHOD'] = 'GET'
env['QUERY_STRING'] = buf env['QUERY_STRING'] = buf
elif method == "POST": elif method == "POST":
fp = StringIO(buf) fp = BytesIO(buf.encode('latin-1')) # FieldStorage expects bytes
env['REQUEST_METHOD'] = 'POST' env['REQUEST_METHOD'] = 'POST'
env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
env['CONTENT_LENGTH'] = str(len(buf)) env['CONTENT_LENGTH'] = str(len(buf))
...@@ -106,9 +105,10 @@ def first_second_elts(list): ...@@ -106,9 +105,10 @@ def first_second_elts(list):
return [(p[0], p[1][0]) for p in list] return [(p[0], p[1][0]) for p in list]
def gen_result(data, environ): def gen_result(data, environ):
fake_stdin = StringIO(data) encoding = 'latin-1'
fake_stdin = BytesIO(data.encode(encoding))
fake_stdin.seek(0) fake_stdin.seek(0)
form = cgi.FieldStorage(fp=fake_stdin, environ=environ) form = cgi.FieldStorage(fp=fake_stdin, environ=environ, encoding=encoding)
result = {} result = {}
for k, v in dict(form).items(): for k, v in dict(form).items():
...@@ -122,9 +122,9 @@ class CgiTests(unittest.TestCase): ...@@ -122,9 +122,9 @@ class CgiTests(unittest.TestCase):
for orig, expect in parse_strict_test_cases: for orig, expect in parse_strict_test_cases:
# Test basic parsing # Test basic parsing
d = do_test(orig, "GET") d = do_test(orig, "GET")
self.assertEqual(d, expect, "Error parsing %s" % repr(orig)) self.assertEqual(d, expect, "Error parsing %s method GET" % repr(orig))
d = do_test(orig, "POST") d = do_test(orig, "POST")
self.assertEqual(d, expect, "Error parsing %s" % repr(orig)) self.assertEqual(d, expect, "Error parsing %s method POST" % repr(orig))
env = {'QUERY_STRING': orig} env = {'QUERY_STRING': orig}
fs = cgi.FieldStorage(environ=env) fs = cgi.FieldStorage(environ=env)
...@@ -181,9 +181,9 @@ class CgiTests(unittest.TestCase): ...@@ -181,9 +181,9 @@ class CgiTests(unittest.TestCase):
setattr(self, name, a) setattr(self, name, a)
return a return a
f = TestReadlineFile(tempfile.TemporaryFile("w+")) f = TestReadlineFile(tempfile.TemporaryFile("wb+"))
self.addCleanup(f.close) self.addCleanup(f.close)
f.write('x' * 256 * 1024) f.write(b'x' * 256 * 1024)
f.seek(0) f.seek(0)
env = {'REQUEST_METHOD':'PUT'} env = {'REQUEST_METHOD':'PUT'}
fs = cgi.FieldStorage(fp=f, environ=env) fs = cgi.FieldStorage(fp=f, environ=env)
...@@ -192,6 +192,7 @@ class CgiTests(unittest.TestCase): ...@@ -192,6 +192,7 @@ class CgiTests(unittest.TestCase):
# (by read_binary); if we are chunking properly, it will be called 5 times # (by read_binary); if we are chunking properly, it will be called 5 times
# as long as the chunksize is 1 << 16. # as long as the chunksize is 1 << 16.
self.assertTrue(f.numcalls > 2) self.assertTrue(f.numcalls > 2)
f.close()
def test_fieldstorage_multipart(self): def test_fieldstorage_multipart(self):
#Test basic FieldStorage multipart parsing #Test basic FieldStorage multipart parsing
...@@ -216,11 +217,13 @@ Content-Disposition: form-data; name="submit" ...@@ -216,11 +217,13 @@ Content-Disposition: form-data; name="submit"
Add\x20 Add\x20
-----------------------------721837373350705526688164684-- -----------------------------721837373350705526688164684--
""" """
fs = cgi.FieldStorage(fp=StringIO(postdata), environ=env) encoding = 'ascii'
fp = BytesIO(postdata.encode(encoding))
fs = cgi.FieldStorage(fp, environ=env, encoding=encoding)
self.assertEqual(len(fs.list), 4) self.assertEqual(len(fs.list), 4)
expect = [{'name':'id', 'filename':None, 'value':'1234'}, expect = [{'name':'id', 'filename':None, 'value':'1234'},
{'name':'title', 'filename':None, 'value':''}, {'name':'title', 'filename':None, 'value':''},
{'name':'file', 'filename':'test.txt', 'value':'Testing 123.'}, {'name':'file', 'filename':'test.txt', 'value':b'Testing 123.\n'},
{'name':'submit', 'filename':None, 'value':' Add '}] {'name':'submit', 'filename':None, 'value':' Add '}]
for x in range(len(fs.list)): for x in range(len(fs.list)):
for k, exp in expect[x].items(): for k, exp in expect[x].items():
...@@ -245,8 +248,7 @@ Content-Disposition: form-data; name="submit" ...@@ -245,8 +248,7 @@ Content-Disposition: form-data; name="submit"
self.assertEqual(self._qs_result, v) self.assertEqual(self._qs_result, v)
def testQSAndFormData(self): def testQSAndFormData(self):
data = """ data = """---123
---123
Content-Disposition: form-data; name="key2" Content-Disposition: form-data; name="key2"
value2y value2y
...@@ -270,8 +272,7 @@ value4 ...@@ -270,8 +272,7 @@ value4
self.assertEqual(self._qs_result, v) self.assertEqual(self._qs_result, v)
def testQSAndFormDataFile(self): def testQSAndFormDataFile(self):
data = """ data = """---123
---123
Content-Disposition: form-data; name="key2" Content-Disposition: form-data; name="key2"
value2y value2y
...@@ -299,7 +300,7 @@ this is the content of the fake file ...@@ -299,7 +300,7 @@ this is the content of the fake file
} }
result = self._qs_result.copy() result = self._qs_result.copy()
result.update({ result.update({
'upload': 'this is the content of the fake file' 'upload': b'this is the content of the fake file\n'
}) })
v = gen_result(data, environ) v = gen_result(data, environ)
self.assertEqual(result, v) self.assertEqual(result, v)
......
...@@ -43,6 +43,10 @@ Core and Builtins ...@@ -43,6 +43,10 @@ Core and Builtins
Library Library
------- -------
- Issue #4953: cgi.FieldStorage and cgi.parse() parse the request as bytes, not
as unicode, and accept binary files. Add encoding and errors attributes to
cgi.FieldStorage.
- Add encoding and errors arguments to urllib.parse_qs() and urllib.parse_qsl() - Add encoding and errors arguments to urllib.parse_qs() and urllib.parse_qsl()
- Issue #10899: No function type annotations in the standard library. - Issue #10899: No function type annotations in the standard library.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment