python-multipart 0.0.6__tar.gz → 0.0.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_multipart-0.0.6 → python_multipart-0.0.8}/PKG-INFO +4 -4
- {python_multipart-0.0.6 → python_multipart-0.0.8}/README.rst +1 -1
- {python_multipart-0.0.6 → python_multipart-0.0.8}/multipart/__init__.py +4 -4
- {python_multipart-0.0.6 → python_multipart-0.0.8}/multipart/multipart.py +31 -35
- {python_multipart-0.0.6 → python_multipart-0.0.8}/pyproject.toml +6 -11
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_multipart.py +12 -4
- {python_multipart-0.0.6 → python_multipart-0.0.8}/.gitignore +0 -0
- {python_multipart-0.0.6 → python_multipart-0.0.8}/LICENSE.txt +0 -0
- {python_multipart-0.0.6 → python_multipart-0.0.8}/multipart/decoders.py +0 -0
- {python_multipart-0.0.6 → python_multipart-0.0.8}/multipart/exceptions.py +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/__init__.py +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/compat.py +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/CR_in_header.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/CR_in_header.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/CR_in_header_value.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/CR_in_header_value.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/almost_match_boundary.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/almost_match_boundary.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/almost_match_boundary_without_CR.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/almost_match_boundary_without_CR.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/almost_match_boundary_without_LF.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/almost_match_boundary_without_LF.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/almost_match_boundary_without_final_hyphen.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/almost_match_boundary_without_final_hyphen.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/bad_end_of_headers.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/bad_end_of_headers.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/bad_header_char.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/bad_header_char.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/bad_initial_boundary.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/bad_initial_boundary.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/base64_encoding.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/base64_encoding.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/empty_header.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/empty_header.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/multiple_fields.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/multiple_fields.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/multiple_files.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/multiple_files.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/quoted_printable_encoding.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/quoted_printable_encoding.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field_blocks.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field_blocks.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field_longer.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field_longer.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field_single_file.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field_single_file.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field_with_leading_newlines.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field_with_leading_newlines.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_file.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_file.yaml +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/utf8_filename.http +0 -0
- {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/utf8_filename.yaml +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-multipart
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.8
|
|
4
4
|
Summary: A streaming multipart parser for Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/andrew-d/python-multipart
|
|
6
6
|
Project-URL: Documentation, https://andrew-d.github.io/python-multipart/
|
|
7
|
-
Project-URL: Changelog, https://github.com/andrew-d/python-multipart/
|
|
7
|
+
Project-URL: Changelog, https://github.com/andrew-d/python-multipart/blob/master/CHANGELOG.md
|
|
8
8
|
Project-URL: Source, https://github.com/andrew-d/python-multipart
|
|
9
9
|
Author-email: Andrew Dunham <andrew@du.nham.ca>
|
|
10
10
|
License-Expression: Apache-2.0
|
|
@@ -28,7 +28,7 @@ Requires-Dist: atomicwrites==1.2.1; extra == 'dev'
|
|
|
28
28
|
Requires-Dist: attrs==19.2.0; extra == 'dev'
|
|
29
29
|
Requires-Dist: coverage==6.5.0; extra == 'dev'
|
|
30
30
|
Requires-Dist: hatch; extra == 'dev'
|
|
31
|
-
Requires-Dist: invoke==
|
|
31
|
+
Requires-Dist: invoke==2.2.0; extra == 'dev'
|
|
32
32
|
Requires-Dist: more-itertools==4.3.0; extra == 'dev'
|
|
33
33
|
Requires-Dist: pbr==4.3.0; extra == 'dev'
|
|
34
34
|
Requires-Dist: pluggy==1.0.0; extra == 'dev'
|
|
@@ -65,5 +65,5 @@ If you want to test:
|
|
|
65
65
|
|
|
66
66
|
.. code-block:: bash
|
|
67
67
|
|
|
68
|
-
$ pip install .[dev]
|
|
68
|
+
$ pip install '.[dev]'
|
|
69
69
|
$ inv test
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# This is the canonical package information.
|
|
2
|
-
__author__
|
|
3
|
-
__license__
|
|
2
|
+
__author__ = "Andrew Dunham"
|
|
3
|
+
__license__ = "Apache"
|
|
4
4
|
__copyright__ = "Copyright (c) 2012-2013, Andrew Dunham"
|
|
5
|
-
__version__
|
|
5
|
+
__version__ = "0.0.8"
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
from .multipart import (
|
|
9
9
|
FormParser,
|
|
10
10
|
MultipartParser,
|
|
11
|
-
QuerystringParser,
|
|
12
11
|
OctetStreamParser,
|
|
12
|
+
QuerystringParser,
|
|
13
13
|
create_form_parser,
|
|
14
14
|
parse_form,
|
|
15
15
|
)
|
|
@@ -2,13 +2,14 @@ from .decoders import *
|
|
|
2
2
|
from .exceptions import *
|
|
3
3
|
|
|
4
4
|
import os
|
|
5
|
-
import re
|
|
6
5
|
import sys
|
|
7
6
|
import shutil
|
|
8
7
|
import logging
|
|
9
8
|
import tempfile
|
|
10
9
|
from io import BytesIO
|
|
11
10
|
from numbers import Number
|
|
11
|
+
from email.message import Message
|
|
12
|
+
from typing import Dict, Union, Tuple
|
|
12
13
|
|
|
13
14
|
# Unique missing object.
|
|
14
15
|
_missing = object()
|
|
@@ -65,55 +66,50 @@ lower_char = lambda c: c | 0x20
|
|
|
65
66
|
ord_char = lambda c: c
|
|
66
67
|
join_bytes = lambda b: bytes(list(b))
|
|
67
68
|
|
|
68
|
-
# These are regexes for parsing header values.
|
|
69
|
-
SPECIAL_CHARS = re.escape(b'()<>@,;:\\"/[]?={} \t')
|
|
70
|
-
QUOTED_STR = br'"(?:\\.|[^"])*"'
|
|
71
|
-
VALUE_STR = br'(?:[^' + SPECIAL_CHARS + br']+|' + QUOTED_STR + br')'
|
|
72
|
-
OPTION_RE_STR = (
|
|
73
|
-
br'(?:;|^)\s*([^' + SPECIAL_CHARS + br']+)\s*=\s*(' + VALUE_STR + br')'
|
|
74
|
-
)
|
|
75
|
-
OPTION_RE = re.compile(OPTION_RE_STR)
|
|
76
|
-
QUOTE = b'"'[0]
|
|
77
69
|
|
|
78
|
-
|
|
79
|
-
def parse_options_header(value):
|
|
70
|
+
def parse_options_header(value: Union[str, bytes]) -> Tuple[bytes, Dict[bytes, bytes]]:
|
|
80
71
|
"""
|
|
81
72
|
Parses a Content-Type header into a value in the following format:
|
|
82
73
|
(content_type, {parameters})
|
|
83
74
|
"""
|
|
75
|
+
# Uses email.message.Message to parse the header as described in PEP 594.
|
|
76
|
+
# Ref: https://peps.python.org/pep-0594/#cgi
|
|
84
77
|
if not value:
|
|
85
78
|
return (b'', {})
|
|
86
79
|
|
|
87
|
-
# If we are passed
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
80
|
+
# If we are passed bytes, we assume that it conforms to WSGI, encoding in latin-1.
|
|
81
|
+
if isinstance(value, bytes): # pragma: no cover
|
|
82
|
+
value = value.decode('latin-1')
|
|
83
|
+
|
|
84
|
+
# For types
|
|
85
|
+
assert isinstance(value, str), 'Value should be a string by now'
|
|
91
86
|
|
|
92
87
|
# If we have no options, return the string as-is.
|
|
93
|
-
if
|
|
94
|
-
return (value.lower().strip(), {})
|
|
88
|
+
if ";" not in value:
|
|
89
|
+
return (value.lower().strip().encode('latin-1'), {})
|
|
95
90
|
|
|
96
91
|
# Split at the first semicolon, to get our value and then options.
|
|
97
|
-
ctype, rest = value.split(b';', 1)
|
|
92
|
+
# ctype, rest = value.split(b';', 1)
|
|
93
|
+
message = Message()
|
|
94
|
+
message['content-type'] = value
|
|
95
|
+
params = message.get_params()
|
|
96
|
+
# If there were no parameters, this would have already returned above
|
|
97
|
+
assert params, 'At least the content type value should be present'
|
|
98
|
+
ctype = params.pop(0)[0].encode('latin-1')
|
|
98
99
|
options = {}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if value
|
|
105
|
-
|
|
106
|
-
value = value[1:-1]
|
|
107
|
-
value = value.replace(b'\\\\', b'\\').replace(b'\\"', b'"')
|
|
108
|
-
|
|
100
|
+
for param in params:
|
|
101
|
+
key, value = param
|
|
102
|
+
# If the value returned from get_params() is a 3-tuple, the last
|
|
103
|
+
# element corresponds to the value.
|
|
104
|
+
# See: https://docs.python.org/3/library/email.compat32-message.html
|
|
105
|
+
if isinstance(value, tuple):
|
|
106
|
+
value = value[-1]
|
|
109
107
|
# If the value is a filename, we need to fix a bug on IE6 that sends
|
|
110
108
|
# the full file path instead of the filename.
|
|
111
|
-
if key ==
|
|
112
|
-
if value[1:3] ==
|
|
113
|
-
value = value.split(
|
|
114
|
-
|
|
115
|
-
options[key] = value
|
|
116
|
-
|
|
109
|
+
if key == 'filename':
|
|
110
|
+
if value[1:3] == ':\\' or value[:2] == '\\\\':
|
|
111
|
+
value = value.split('\\')[-1]
|
|
112
|
+
options[key.encode('latin-1')] = value.encode('latin-1')
|
|
117
113
|
return ctype, options
|
|
118
114
|
|
|
119
115
|
|
|
@@ -9,9 +9,7 @@ description = "A streaming multipart parser for Python"
|
|
|
9
9
|
readme = "README.rst"
|
|
10
10
|
license = "Apache-2.0"
|
|
11
11
|
requires-python = ">=3.7"
|
|
12
|
-
authors = [
|
|
13
|
-
{ name = "Andrew Dunham", email = "andrew@du.nham.ca" },
|
|
14
|
-
]
|
|
12
|
+
authors = [{ name = "Andrew Dunham", email = "andrew@du.nham.ca" }]
|
|
15
13
|
classifiers = [
|
|
16
14
|
'Development Status :: 5 - Production/Stable',
|
|
17
15
|
'Environment :: Web Environment',
|
|
@@ -41,7 +39,7 @@ dev = [
|
|
|
41
39
|
"pytest==7.2.0",
|
|
42
40
|
"pytest-cov==4.0.0",
|
|
43
41
|
"PyYAML==5.1",
|
|
44
|
-
"invoke==
|
|
42
|
+
"invoke==2.2.0",
|
|
45
43
|
"pytest-timeout==2.1.0",
|
|
46
44
|
"hatch",
|
|
47
45
|
]
|
|
@@ -49,17 +47,14 @@ dev = [
|
|
|
49
47
|
[project.urls]
|
|
50
48
|
Homepage = "https://github.com/andrew-d/python-multipart"
|
|
51
49
|
Documentation = "https://andrew-d.github.io/python-multipart/"
|
|
52
|
-
Changelog = "https://github.com/andrew-d/python-multipart/
|
|
50
|
+
Changelog = "https://github.com/andrew-d/python-multipart/blob/master/CHANGELOG.md"
|
|
53
51
|
Source = "https://github.com/andrew-d/python-multipart"
|
|
54
52
|
|
|
55
53
|
[tool.hatch.version]
|
|
56
54
|
path = "multipart/__init__.py"
|
|
57
55
|
|
|
58
56
|
[tool.hatch.build.targets.wheel]
|
|
59
|
-
packages = [
|
|
60
|
-
|
|
61
|
-
]
|
|
57
|
+
packages = ["multipart"]
|
|
58
|
+
|
|
62
59
|
[tool.hatch.build.targets.sdist]
|
|
63
|
-
include = [
|
|
64
|
-
"/multipart",
|
|
65
|
-
]
|
|
60
|
+
include = ["/multipart", "/tests"]
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
|
-
import glob
|
|
4
3
|
import yaml
|
|
5
|
-
import base64
|
|
6
4
|
import random
|
|
7
5
|
import tempfile
|
|
8
6
|
import unittest
|
|
@@ -12,9 +10,9 @@ from .compat import (
|
|
|
12
10
|
slow_test,
|
|
13
11
|
)
|
|
14
12
|
from io import BytesIO
|
|
15
|
-
from unittest.mock import
|
|
13
|
+
from unittest.mock import Mock
|
|
16
14
|
|
|
17
|
-
from
|
|
15
|
+
from multipart.multipart import *
|
|
18
16
|
|
|
19
17
|
|
|
20
18
|
# Get the current directory for our later test cases.
|
|
@@ -272,6 +270,16 @@ class TestParseOptionsHeader(unittest.TestCase):
|
|
|
272
270
|
t, p = parse_options_header(b'text/plain; filename="C:\\this\\is\\a\\path\\file.txt"')
|
|
273
271
|
|
|
274
272
|
self.assertEqual(p[b'filename'], b'file.txt')
|
|
273
|
+
|
|
274
|
+
def test_redos_attack_header(self):
|
|
275
|
+
t, p = parse_options_header(b'application/x-www-form-urlencoded; !="\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\')
|
|
276
|
+
# If vulnerable, this test wouldn't finish, the line above would hang
|
|
277
|
+
self.assertIn(b'"\\', p[b'!'])
|
|
278
|
+
|
|
279
|
+
def test_handles_rfc_2231(self):
|
|
280
|
+
t, p = parse_options_header(b'text/plain; param*=us-ascii\'en-us\'encoded%20message')
|
|
281
|
+
|
|
282
|
+
self.assertEqual(p[b'param'], b'encoded message')
|
|
275
283
|
|
|
276
284
|
|
|
277
285
|
class TestBaseParser(unittest.TestCase):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/CR_in_header.http
RENAMED
|
File without changes
|
{python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/CR_in_header.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/empty_header.http
RENAMED
|
File without changes
|
{python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/empty_header.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/multiple_files.http
RENAMED
|
File without changes
|
{python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/multiple_files.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field.http
RENAMED
|
File without changes
|
{python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_file.http
RENAMED
|
File without changes
|
{python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_file.yaml
RENAMED
|
File without changes
|
{python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/utf8_filename.http
RENAMED
|
File without changes
|
{python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/utf8_filename.yaml
RENAMED
|
File without changes
|