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.
Files changed (54) hide show
  1. {python_multipart-0.0.6 → python_multipart-0.0.8}/PKG-INFO +4 -4
  2. {python_multipart-0.0.6 → python_multipart-0.0.8}/README.rst +1 -1
  3. {python_multipart-0.0.6 → python_multipart-0.0.8}/multipart/__init__.py +4 -4
  4. {python_multipart-0.0.6 → python_multipart-0.0.8}/multipart/multipart.py +31 -35
  5. {python_multipart-0.0.6 → python_multipart-0.0.8}/pyproject.toml +6 -11
  6. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_multipart.py +12 -4
  7. {python_multipart-0.0.6 → python_multipart-0.0.8}/.gitignore +0 -0
  8. {python_multipart-0.0.6 → python_multipart-0.0.8}/LICENSE.txt +0 -0
  9. {python_multipart-0.0.6 → python_multipart-0.0.8}/multipart/decoders.py +0 -0
  10. {python_multipart-0.0.6 → python_multipart-0.0.8}/multipart/exceptions.py +0 -0
  11. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/__init__.py +0 -0
  12. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/compat.py +0 -0
  13. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/CR_in_header.http +0 -0
  14. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/CR_in_header.yaml +0 -0
  15. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/CR_in_header_value.http +0 -0
  16. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/CR_in_header_value.yaml +0 -0
  17. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/almost_match_boundary.http +0 -0
  18. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/almost_match_boundary.yaml +0 -0
  19. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/almost_match_boundary_without_CR.http +0 -0
  20. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/almost_match_boundary_without_CR.yaml +0 -0
  21. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/almost_match_boundary_without_LF.http +0 -0
  22. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/almost_match_boundary_without_LF.yaml +0 -0
  23. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/almost_match_boundary_without_final_hyphen.http +0 -0
  24. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/almost_match_boundary_without_final_hyphen.yaml +0 -0
  25. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/bad_end_of_headers.http +0 -0
  26. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/bad_end_of_headers.yaml +0 -0
  27. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/bad_header_char.http +0 -0
  28. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/bad_header_char.yaml +0 -0
  29. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/bad_initial_boundary.http +0 -0
  30. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/bad_initial_boundary.yaml +0 -0
  31. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/base64_encoding.http +0 -0
  32. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/base64_encoding.yaml +0 -0
  33. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/empty_header.http +0 -0
  34. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/empty_header.yaml +0 -0
  35. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/multiple_fields.http +0 -0
  36. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/multiple_fields.yaml +0 -0
  37. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/multiple_files.http +0 -0
  38. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/multiple_files.yaml +0 -0
  39. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/quoted_printable_encoding.http +0 -0
  40. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/quoted_printable_encoding.yaml +0 -0
  41. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field.http +0 -0
  42. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field.yaml +0 -0
  43. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field_blocks.http +0 -0
  44. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field_blocks.yaml +0 -0
  45. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field_longer.http +0 -0
  46. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field_longer.yaml +0 -0
  47. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field_single_file.http +0 -0
  48. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field_single_file.yaml +0 -0
  49. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field_with_leading_newlines.http +0 -0
  50. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_field_with_leading_newlines.yaml +0 -0
  51. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_file.http +0 -0
  52. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/single_file.yaml +0 -0
  53. {python_multipart-0.0.6/multipart → python_multipart-0.0.8}/tests/test_data/http/utf8_filename.http +0 -0
  54. {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.6
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/tags
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==1.7.3; extra == 'dev'
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
@@ -24,5 +24,5 @@ If you want to test:
24
24
 
25
25
  .. code-block:: bash
26
26
 
27
- $ pip install .[dev]
27
+ $ pip install '.[dev]'
28
28
  $ inv test
@@ -1,15 +1,15 @@
1
1
  # This is the canonical package information.
2
- __author__ = 'Andrew Dunham'
3
- __license__ = 'Apache'
2
+ __author__ = "Andrew Dunham"
3
+ __license__ = "Apache"
4
4
  __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham"
5
- __version__ = "0.0.6"
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 a string, we assume that it conforms to WSGI and does
88
- # not contain any code point that's not in latin-1.
89
- if isinstance(value, str): # pragma: no cover
90
- value = value.encode('latin-1')
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 b';' not in value:
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
- # Parse the options.
101
- for match in OPTION_RE.finditer(rest):
102
- key = match.group(1).lower()
103
- value = match.group(2)
104
- if value[0] == QUOTE and value[-1] == QUOTE:
105
- # Unquote the value.
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 == b'filename':
112
- if value[1:3] == b':\\' or value[:2] == b'\\\\':
113
- value = value.split(b'\\')[-1]
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==1.7.3",
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/tags"
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
- "multipart",
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 MagicMock, Mock, patch
13
+ from unittest.mock import Mock
16
14
 
17
- from ..multipart import *
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):