python-multipart 0.0.17__tar.gz → 0.0.19__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 (66) hide show
  1. {python_multipart-0.0.17 → python_multipart-0.0.19}/CHANGELOG.md +8 -0
  2. {python_multipart-0.0.17 → python_multipart-0.0.19}/PKG-INFO +2 -3
  3. {python_multipart-0.0.17 → python_multipart-0.0.19}/pyproject.toml +2 -2
  4. {python_multipart-0.0.17 → python_multipart-0.0.19}/python_multipart/__init__.py +1 -1
  5. {python_multipart-0.0.17 → python_multipart-0.0.19}/python_multipart/multipart.py +8 -4
  6. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_multipart.py +65 -1
  7. {python_multipart-0.0.17 → python_multipart-0.0.19}/.gitignore +0 -0
  8. {python_multipart-0.0.17 → python_multipart-0.0.19}/LICENSE.txt +0 -0
  9. {python_multipart-0.0.17 → python_multipart-0.0.19}/README.md +0 -0
  10. {python_multipart-0.0.17 → python_multipart-0.0.19}/multipart/__init__.py +0 -0
  11. {python_multipart-0.0.17 → python_multipart-0.0.19}/multipart/decoders.py +0 -0
  12. {python_multipart-0.0.17 → python_multipart-0.0.19}/multipart/exceptions.py +0 -0
  13. {python_multipart-0.0.17 → python_multipart-0.0.19}/multipart/multipart.py +0 -0
  14. {python_multipart-0.0.17 → python_multipart-0.0.19}/python_multipart/decoders.py +0 -0
  15. {python_multipart-0.0.17 → python_multipart-0.0.19}/python_multipart/exceptions.py +0 -0
  16. {python_multipart-0.0.17 → python_multipart-0.0.19}/python_multipart/py.typed +0 -0
  17. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/__init__.py +0 -0
  18. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/compat.py +0 -0
  19. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/CRLF_in_header.http +0 -0
  20. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/CRLF_in_header.yaml +0 -0
  21. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/CR_in_header.http +0 -0
  22. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/CR_in_header.yaml +0 -0
  23. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/CR_in_header_value.http +0 -0
  24. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/CR_in_header_value.yaml +0 -0
  25. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/almost_match_boundary.http +0 -0
  26. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/almost_match_boundary.yaml +0 -0
  27. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/almost_match_boundary_without_CR.http +0 -0
  28. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/almost_match_boundary_without_CR.yaml +0 -0
  29. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/almost_match_boundary_without_LF.http +0 -0
  30. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/almost_match_boundary_without_LF.yaml +0 -0
  31. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/almost_match_boundary_without_final_hyphen.http +0 -0
  32. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/almost_match_boundary_without_final_hyphen.yaml +0 -0
  33. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/bad_end_of_headers.http +0 -0
  34. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/bad_end_of_headers.yaml +0 -0
  35. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/bad_header_char.http +0 -0
  36. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/bad_header_char.yaml +0 -0
  37. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/bad_initial_boundary.http +0 -0
  38. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/bad_initial_boundary.yaml +0 -0
  39. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/base64_encoding.http +0 -0
  40. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/base64_encoding.yaml +0 -0
  41. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/empty_header.http +0 -0
  42. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/empty_header.yaml +0 -0
  43. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/header_with_number.http +0 -0
  44. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/header_with_number.yaml +0 -0
  45. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/mixed_plain_and_base64_encoding.http +0 -0
  46. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/mixed_plain_and_base64_encoding.yaml +0 -0
  47. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/multiple_fields.http +0 -0
  48. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/multiple_fields.yaml +0 -0
  49. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/multiple_files.http +0 -0
  50. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/multiple_files.yaml +0 -0
  51. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/quoted_printable_encoding.http +0 -0
  52. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/quoted_printable_encoding.yaml +0 -0
  53. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/single_field.http +0 -0
  54. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/single_field.yaml +0 -0
  55. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/single_field_blocks.http +0 -0
  56. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/single_field_blocks.yaml +0 -0
  57. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/single_field_longer.http +0 -0
  58. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/single_field_longer.yaml +0 -0
  59. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/single_field_single_file.http +0 -0
  60. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/single_field_single_file.yaml +0 -0
  61. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/single_field_with_leading_newlines.http +0 -0
  62. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/single_field_with_leading_newlines.yaml +0 -0
  63. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/single_file.http +0 -0
  64. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/single_file.yaml +0 -0
  65. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/utf8_filename.http +0 -0
  66. {python_multipart-0.0.17 → python_multipart-0.0.19}/tests/test_data/http/utf8_filename.yaml +0 -0
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.19 (2024-11-30)
4
+
5
+ * Don't warn when CRLF is found after last boundary on `MultipartParser` [#193](https://github.com/Kludex/python-multipart/pull/193).
6
+
7
+ ## 0.0.18 (2024-11-28)
8
+
9
+ * Hard break if found data after last boundary on `MultipartParser` [#189](https://github.com/Kludex/python-multipart/pull/189).
10
+
3
11
  ## 0.0.17 (2024-10-31)
4
12
 
5
13
  * Handle PermissionError in fallback code for old import name [#182](https://github.com/Kludex/python-multipart/pull/182).
@@ -1,14 +1,13 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: python-multipart
3
- Version: 0.0.17
3
+ Version: 0.0.19
4
4
  Summary: A streaming multipart parser for Python
5
5
  Project-URL: Homepage, https://github.com/Kludex/python-multipart
6
6
  Project-URL: Documentation, https://kludex.github.io/python-multipart/
7
7
  Project-URL: Changelog, https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md
8
8
  Project-URL: Source, https://github.com/Kludex/python-multipart
9
9
  Author-email: Andrew Dunham <andrew@du.nham.ca>, Marcelo Trylesinski <marcelotryle@gmail.com>
10
- License-Expression: Apache-2.0
11
- License-File: LICENSE.txt
10
+ License: Apache-2.0
12
11
  Classifier: Development Status :: 5 - Production/Stable
13
12
  Classifier: Environment :: Web Environment
14
13
  Classifier: Intended Audience :: Developers
@@ -44,7 +44,7 @@ dev-dependencies = [
44
44
  "PyYAML==6.0.1",
45
45
  "invoke==2.2.0",
46
46
  "pytest-timeout==2.3.1",
47
- "ruff==0.3.4",
47
+ "ruff==0.8.0",
48
48
  "mypy",
49
49
  "types-PyYAML",
50
50
  "atheris==2.3.0; python_version != '3.12'",
@@ -122,4 +122,4 @@ exclude_lines = [
122
122
  ]
123
123
 
124
124
  [tool.check-sdist]
125
- git-only = ["docs", "fuzz", "scripts", "mkdocs.yml", "uv.lock"]
125
+ git-only = ["docs", "fuzz", "scripts", "mkdocs.yml", "uv.lock", "SECURITY.md"]
@@ -2,7 +2,7 @@
2
2
  __author__ = "Andrew Dunham"
3
3
  __license__ = "Apache"
4
4
  __copyright__ = "Copyright (c) 2012-2013, Andrew Dunham"
5
- __version__ = "0.0.17"
5
+ __version__ = "0.0.19"
6
6
 
7
7
  from .multipart import (
8
8
  BaseParser,
@@ -1105,7 +1105,6 @@ class MultipartParser(BaseParser):
1105
1105
  # Skip leading newlines
1106
1106
  if c == CR or c == LF:
1107
1107
  i += 1
1108
- self.logger.debug("Skipping leading CR/LF at %d", i)
1109
1108
  continue
1110
1109
 
1111
1110
  # index is used as in index into our boundary. Set to 0.
@@ -1398,9 +1397,14 @@ class MultipartParser(BaseParser):
1398
1397
  i -= 1
1399
1398
 
1400
1399
  elif state == MultipartState.END:
1401
- # Do nothing and just consume a byte in the end state.
1402
- if c not in (CR, LF):
1403
- self.logger.warning("Consuming a byte '0x%x' in the end state", c) # pragma: no cover
1400
+ # Don't do anything if chunk ends with CRLF.
1401
+ if c == CR and i + 1 < length and data[i + 1] == LF:
1402
+ i += 2
1403
+ continue
1404
+ # Skip data after the last boundary.
1405
+ self.logger.warning("Skipping data after last boundary")
1406
+ i = length
1407
+ break
1404
1408
 
1405
1409
  else: # pragma: no cover (error case)
1406
1410
  # We got into a strange state somehow! Just stop processing.
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  import os
4
5
  import random
5
6
  import sys
@@ -9,6 +10,7 @@ from io import BytesIO
9
10
  from typing import TYPE_CHECKING, cast
10
11
  from unittest.mock import Mock
11
12
 
13
+ import pytest
12
14
  import yaml
13
15
 
14
16
  from python_multipart.decoders import Base64Decoder, QuotedPrintableDecoder
@@ -825,7 +827,7 @@ class TestFormParser(unittest.TestCase):
825
827
  return
826
828
 
827
829
  # No error!
828
- self.assertEqual(processed, len(param["test"]))
830
+ self.assertEqual(processed, len(param["test"]), param["name"])
829
831
 
830
832
  # Assert that the parser gave us the appropriate fields/files.
831
833
  for e in param["result"]["expected"]:
@@ -1210,6 +1212,68 @@ class TestFormParser(unittest.TestCase):
1210
1212
  self.assertEqual(fields[2].field_name, b"baz")
1211
1213
  self.assertEqual(fields[2].value, b"asdf")
1212
1214
 
1215
+ def test_multipart_parser_newlines_before_first_boundary(self) -> None:
1216
+ """This test makes sure that the parser does not handle when there is junk data after the last boundary."""
1217
+ num = 5_000_000
1218
+ data = (
1219
+ "\r\n" * num + "--boundary\r\n"
1220
+ 'Content-Disposition: form-data; name="file"; filename="filename.txt"\r\n'
1221
+ "Content-Type: text/plain\r\n\r\n"
1222
+ "hello\r\n"
1223
+ "--boundary--"
1224
+ )
1225
+
1226
+ files: list[File] = []
1227
+
1228
+ def on_file(f: FileProtocol) -> None:
1229
+ files.append(cast(File, f))
1230
+
1231
+ f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
1232
+ f.write(data.encode("latin-1"))
1233
+
1234
+ def test_multipart_parser_data_after_last_boundary(self) -> None:
1235
+ """This test makes sure that the parser does not handle when there is junk data after the last boundary."""
1236
+ num = 50_000_000
1237
+ data = (
1238
+ "--boundary\r\n"
1239
+ 'Content-Disposition: form-data; name="file"; filename="filename.txt"\r\n'
1240
+ "Content-Type: text/plain\r\n\r\n"
1241
+ "hello\r\n"
1242
+ "--boundary--" + "-" * num + "\r\n"
1243
+ )
1244
+
1245
+ files: list[File] = []
1246
+
1247
+ def on_file(f: FileProtocol) -> None:
1248
+ files.append(cast(File, f))
1249
+
1250
+ f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
1251
+ f.write(data.encode("latin-1"))
1252
+
1253
+ @pytest.fixture(autouse=True)
1254
+ def inject_fixtures(self, caplog: pytest.LogCaptureFixture) -> None:
1255
+ self._caplog = caplog
1256
+
1257
+ def test_multipart_parser_data_end_with_crlf_without_warnings(self) -> None:
1258
+ """This test makes sure that the parser does not handle when the data ends with a CRLF."""
1259
+ data = (
1260
+ "--boundary\r\n"
1261
+ 'Content-Disposition: form-data; name="file"; filename="filename.txt"\r\n'
1262
+ "Content-Type: text/plain\r\n\r\n"
1263
+ "hello\r\n"
1264
+ "--boundary--\r\n"
1265
+ )
1266
+
1267
+ files: list[File] = []
1268
+
1269
+ def on_file(f: FileProtocol) -> None:
1270
+ files.append(cast(File, f))
1271
+
1272
+ f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
1273
+ with self._caplog.at_level(logging.WARNING):
1274
+ f.write(data.encode("latin-1"))
1275
+ assert len(self._caplog.records) == 0
1276
+
1213
1277
  def test_max_size_multipart(self) -> None:
1214
1278
  # Load test data.
1215
1279
  test_file = "single_field_single_file.http"