python-multipart 0.0.18__tar.gz → 0.0.20__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 (70) hide show
  1. {python_multipart-0.0.18 → python_multipart-0.0.20}/CHANGELOG.md +8 -0
  2. {python_multipart-0.0.18 → python_multipart-0.0.20}/PKG-INFO +4 -3
  3. {python_multipart-0.0.18 → python_multipart-0.0.20}/pyproject.toml +1 -1
  4. {python_multipart-0.0.18 → python_multipart-0.0.20}/python_multipart/__init__.py +1 -1
  5. {python_multipart-0.0.18 → python_multipart-0.0.20}/python_multipart/multipart.py +24 -4
  6. python_multipart-0.0.20/tests/test_data/http/empty_message.http +1 -0
  7. python_multipart-0.0.20/tests/test_data/http/empty_message.yaml +2 -0
  8. python_multipart-0.0.20/tests/test_data/http/empty_message_with_bad_end.http +1 -0
  9. python_multipart-0.0.20/tests/test_data/http/empty_message_with_bad_end.yaml +3 -0
  10. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_multipart.py +26 -0
  11. {python_multipart-0.0.18 → python_multipart-0.0.20}/.gitignore +0 -0
  12. {python_multipart-0.0.18 → python_multipart-0.0.20}/LICENSE.txt +0 -0
  13. {python_multipart-0.0.18 → python_multipart-0.0.20}/README.md +0 -0
  14. {python_multipart-0.0.18 → python_multipart-0.0.20}/multipart/__init__.py +0 -0
  15. {python_multipart-0.0.18 → python_multipart-0.0.20}/multipart/decoders.py +0 -0
  16. {python_multipart-0.0.18 → python_multipart-0.0.20}/multipart/exceptions.py +0 -0
  17. {python_multipart-0.0.18 → python_multipart-0.0.20}/multipart/multipart.py +0 -0
  18. {python_multipart-0.0.18 → python_multipart-0.0.20}/python_multipart/decoders.py +0 -0
  19. {python_multipart-0.0.18 → python_multipart-0.0.20}/python_multipart/exceptions.py +0 -0
  20. {python_multipart-0.0.18 → python_multipart-0.0.20}/python_multipart/py.typed +0 -0
  21. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/__init__.py +0 -0
  22. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/compat.py +0 -0
  23. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/CRLF_in_header.http +0 -0
  24. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/CRLF_in_header.yaml +0 -0
  25. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/CR_in_header.http +0 -0
  26. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/CR_in_header.yaml +0 -0
  27. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/CR_in_header_value.http +0 -0
  28. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/CR_in_header_value.yaml +0 -0
  29. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/almost_match_boundary.http +0 -0
  30. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/almost_match_boundary.yaml +0 -0
  31. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/almost_match_boundary_without_CR.http +0 -0
  32. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/almost_match_boundary_without_CR.yaml +0 -0
  33. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/almost_match_boundary_without_LF.http +0 -0
  34. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/almost_match_boundary_without_LF.yaml +0 -0
  35. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/almost_match_boundary_without_final_hyphen.http +0 -0
  36. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/almost_match_boundary_without_final_hyphen.yaml +0 -0
  37. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/bad_end_of_headers.http +0 -0
  38. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/bad_end_of_headers.yaml +0 -0
  39. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/bad_header_char.http +0 -0
  40. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/bad_header_char.yaml +0 -0
  41. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/bad_initial_boundary.http +0 -0
  42. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/bad_initial_boundary.yaml +0 -0
  43. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/base64_encoding.http +0 -0
  44. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/base64_encoding.yaml +0 -0
  45. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/empty_header.http +0 -0
  46. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/empty_header.yaml +0 -0
  47. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/header_with_number.http +0 -0
  48. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/header_with_number.yaml +0 -0
  49. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/mixed_plain_and_base64_encoding.http +0 -0
  50. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/mixed_plain_and_base64_encoding.yaml +0 -0
  51. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/multiple_fields.http +0 -0
  52. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/multiple_fields.yaml +0 -0
  53. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/multiple_files.http +0 -0
  54. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/multiple_files.yaml +0 -0
  55. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/quoted_printable_encoding.http +0 -0
  56. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/quoted_printable_encoding.yaml +0 -0
  57. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/single_field.http +0 -0
  58. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/single_field.yaml +0 -0
  59. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/single_field_blocks.http +0 -0
  60. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/single_field_blocks.yaml +0 -0
  61. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/single_field_longer.http +0 -0
  62. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/single_field_longer.yaml +0 -0
  63. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/single_field_single_file.http +0 -0
  64. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/single_field_single_file.yaml +0 -0
  65. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/single_field_with_leading_newlines.http +0 -0
  66. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/single_field_with_leading_newlines.yaml +0 -0
  67. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/single_file.http +0 -0
  68. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/single_file.yaml +0 -0
  69. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/utf8_filename.http +0 -0
  70. {python_multipart-0.0.18 → python_multipart-0.0.20}/tests/test_data/http/utf8_filename.yaml +0 -0
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.20 (2024-12-16)
4
+
5
+ * Handle messages containing only end boundary [#142](https://github.com/Kludex/python-multipart/pull/142).
6
+
7
+ ## 0.0.19 (2024-11-30)
8
+
9
+ * Don't warn when CRLF is found after last boundary on `MultipartParser` [#193](https://github.com/Kludex/python-multipart/pull/193).
10
+
3
11
  ## 0.0.18 (2024-11-28)
4
12
 
5
13
  * Hard break if found data after last boundary on `MultipartParser` [#189](https://github.com/Kludex/python-multipart/pull/189).
@@ -1,13 +1,14 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: python-multipart
3
- Version: 0.0.18
3
+ Version: 0.0.20
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: Apache-2.0
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE.txt
11
12
  Classifier: Development Status :: 5 - Production/Stable
12
13
  Classifier: Environment :: Web Environment
13
14
  Classifier: Intended Audience :: Developers
@@ -47,7 +47,7 @@ dev-dependencies = [
47
47
  "ruff==0.8.0",
48
48
  "mypy",
49
49
  "types-PyYAML",
50
- "atheris==2.3.0; python_version != '3.12'",
50
+ "atheris==2.3.0; python_version <= '3.11'",
51
51
  # Documentation
52
52
  "mkdocs",
53
53
  "mkdocs-material",
@@ -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.18"
5
+ __version__ = "0.0.20"
6
6
 
7
7
  from .multipart import (
8
8
  BaseParser,
@@ -130,7 +130,8 @@ class MultipartState(IntEnum):
130
130
  PART_DATA_START = 8
131
131
  PART_DATA = 9
132
132
  PART_DATA_END = 10
133
- END = 11
133
+ END_BOUNDARY = 11
134
+ END = 12
134
135
 
135
136
 
136
137
  # Flags for the multipart parser.
@@ -1119,7 +1120,10 @@ class MultipartParser(BaseParser):
1119
1120
  # Check to ensure that the last 2 characters in our boundary
1120
1121
  # are CRLF.
1121
1122
  if index == len(boundary) - 2:
1122
- if c != CR:
1123
+ if c == HYPHEN:
1124
+ # Potential empty message.
1125
+ state = MultipartState.END_BOUNDARY
1126
+ elif c != CR:
1123
1127
  # Error!
1124
1128
  msg = "Did not find CR at end of boundary (%d)" % (i,)
1125
1129
  self.logger.warning(msg)
@@ -1396,7 +1400,23 @@ class MultipartParser(BaseParser):
1396
1400
  # the start of the boundary itself.
1397
1401
  i -= 1
1398
1402
 
1403
+ elif state == MultipartState.END_BOUNDARY:
1404
+ if index == len(boundary) - 2 + 1:
1405
+ if c != HYPHEN:
1406
+ msg = "Did not find - at end of boundary (%d)" % (i,)
1407
+ self.logger.warning(msg)
1408
+ e = MultipartParseError(msg)
1409
+ e.offset = i
1410
+ raise e
1411
+ index += 1
1412
+ self.callback("end")
1413
+ state = MultipartState.END
1414
+
1399
1415
  elif state == MultipartState.END:
1416
+ # Don't do anything if chunk ends with CRLF.
1417
+ if c == CR and i + 1 < length and data[i + 1] == LF:
1418
+ i += 2
1419
+ continue
1400
1420
  # Skip data after the last boundary.
1401
1421
  self.logger.warning("Skipping data after last boundary")
1402
1422
  i = length
@@ -1703,8 +1723,8 @@ class FormParser:
1703
1723
 
1704
1724
  def _on_end() -> None:
1705
1725
  nonlocal writer
1706
- assert writer is not None
1707
- writer.finalize()
1726
+ if writer is not None:
1727
+ writer.finalize()
1708
1728
  if self.on_end is not None:
1709
1729
  self.on_end()
1710
1730
 
@@ -0,0 +1 @@
1
+ ----boundary--
@@ -0,0 +1,2 @@
1
+ boundary: --boundary
2
+ expected: []
@@ -0,0 +1,3 @@
1
+ boundary: --boundary
2
+ expected:
3
+ error: 13
@@ -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
@@ -1248,6 +1250,30 @@ class TestFormParser(unittest.TestCase):
1248
1250
  f = FormParser("multipart/form-data", on_field=Mock(), on_file=on_file, boundary="boundary")
1249
1251
  f.write(data.encode("latin-1"))
1250
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
+
1251
1277
  def test_max_size_multipart(self) -> None:
1252
1278
  # Load test data.
1253
1279
  test_file = "single_field_single_file.http"