From 06d7f183228f6e65be2aac644fa21aa30d68f2d0 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 24 Sep 2026 21:27:46 +0200 Subject: [PATCH 1/3] Speed up opening boundary matching --- python_multipart/multipart.py | 7 ++++- tests/test_multipart.py | 51 +++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/python_multipart/multipart.py b/python_multipart/multipart.py index 49cdd8e..a0f0482 100644 --- a/python_multipart/multipart.py +++ b/python_multipart/multipart.py @@ -1051,7 +1051,7 @@ def __init__( if not isinstance(max_size, Number) or max_size < 1: raise ValueError("max_size must be a positive number, not %r" % max_size) - self.max_size = max_size + self.max_size: int | float = max_size self._current_size = 0 self.max_header_count = max_header_count @@ -1203,6 +1203,11 @@ def data_callback(name: CallbackName, end_i: int, remaining: bool = False) -> No i -= 1 elif state == MultipartState.START_BOUNDARY: + if index == 0 and data.startswith(boundary[2:], i, length): + index = boundary_length - 2 + i += index + continue + # Check to ensure that the last 2 characters in our boundary # are CRLF. if index == boundary_length - 2: diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 949d70c..a8634a5 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -850,6 +850,57 @@ def test_content_transfer_encoding_is_case_insensitive(content_transfer_encoding assert file.file_object.read() == b"Test" +@pytest.mark.parametrize("boundary", [b"", b"x", b"x" * 256]) +@pytest.mark.parametrize("empty", [False, True]) +def test_multipart_opening_boundary_chunks(boundary: bytes, empty: bool) -> None: + opening = b"--" + boundary + data = ( + opening + b"--\r\n" + if empty + else (opening + b'\r\nContent-Disposition: form-data; name="field"\r\n\r\nvalue\r\n' + opening + b"--\r\n") + ) + for split in [*range(len(opening) + 3), len(data)]: + fields: list[Field] = [] + events: list[str] = [] + parser = FormParser( + "multipart/form-data", fields.append, None, on_end=lambda: events.append("end"), boundary=boundary + ) + for chunk in (data[:split], data[split:]): + assert parser.write(chunk) == len(chunk) + parser.finalize() + + assert [(field.field_name, field.value) for field in fields] == ([] if empty else [(b"field", b"value")]) + assert events == ["end"] + + +@pytest.mark.parametrize( + ("data", "offset"), [(b"--Boundary\r\n", 2), (b"--boundaryX\n", 10), (b"--boundary\rX", 11), (b"--boundary-X", 11)] +) +def test_multipart_opening_boundary_error_offset(data: bytes, offset: int) -> None: + for split in range(offset + 1): + parser = MultipartParser(b"boundary") + assert parser.write(data[:split]) == split + with pytest.raises(MultipartParseError) as exc_info: + parser.write(data[split:]) + assert exc_info.value.offset == offset - split + + +@pytest.mark.parametrize("max_size", range(1, 13)) +def test_multipart_opening_boundary_max_size(max_size: int) -> None: + data = b"--boundary\r\n\r\nvalue\r\n--boundary--" + events: list[str] = [] + parser = MultipartParser( + b"boundary", + {"on_part_begin": lambda: events.append("begin"), "on_end": lambda: events.append("end")}, + max_size=max_size, + ) + assert parser.write(data) == max_size + parser.max_size = len(data) + assert parser.write(data[max_size:]) == len(data) - max_size + parser.finalize() + assert events == ["begin", "end"] + + @parametrize_class class TestFormParser(unittest.TestCase): def make(self, boundary: str | bytes, config: dict[str, Any] = {}) -> None: From 53c941c61d1bdd26a5a97bbe1b639e6bc21a2a91 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 24 Sep 2026 21:35:54 +0200 Subject: [PATCH 2/3] Keep one opening boundary truncation regression test --- tests/test_multipart.py | 39 ++------------------------------------- 1 file changed, 2 insertions(+), 37 deletions(-) diff --git a/tests/test_multipart.py b/tests/test_multipart.py index a8634a5..061d1cc 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -850,44 +850,9 @@ def test_content_transfer_encoding_is_case_insensitive(content_transfer_encoding assert file.file_object.read() == b"Test" -@pytest.mark.parametrize("boundary", [b"", b"x", b"x" * 256]) -@pytest.mark.parametrize("empty", [False, True]) -def test_multipart_opening_boundary_chunks(boundary: bytes, empty: bool) -> None: - opening = b"--" + boundary - data = ( - opening + b"--\r\n" - if empty - else (opening + b'\r\nContent-Disposition: form-data; name="field"\r\n\r\nvalue\r\n' + opening + b"--\r\n") - ) - for split in [*range(len(opening) + 3), len(data)]: - fields: list[Field] = [] - events: list[str] = [] - parser = FormParser( - "multipart/form-data", fields.append, None, on_end=lambda: events.append("end"), boundary=boundary - ) - for chunk in (data[:split], data[split:]): - assert parser.write(chunk) == len(chunk) - parser.finalize() - - assert [(field.field_name, field.value) for field in fields] == ([] if empty else [(b"field", b"value")]) - assert events == ["end"] - - -@pytest.mark.parametrize( - ("data", "offset"), [(b"--Boundary\r\n", 2), (b"--boundaryX\n", 10), (b"--boundary\rX", 11), (b"--boundary-X", 11)] -) -def test_multipart_opening_boundary_error_offset(data: bytes, offset: int) -> None: - for split in range(offset + 1): - parser = MultipartParser(b"boundary") - assert parser.write(data[:split]) == split - with pytest.raises(MultipartParseError) as exc_info: - parser.write(data[split:]) - assert exc_info.value.offset == offset - split - - -@pytest.mark.parametrize("max_size", range(1, 13)) -def test_multipart_opening_boundary_max_size(max_size: int) -> None: +def test_multipart_opening_boundary_max_size() -> None: data = b"--boundary\r\n\r\nvalue\r\n--boundary--" + max_size = 9 events: list[str] = [] parser = MultipartParser( b"boundary", From 582240e08bfbd29573007ac51cbceb70b3fc47cd Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 24 Sep 2026 21:38:29 +0200 Subject: [PATCH 3/3] Restore the existing max_size assignment --- python_multipart/multipart.py | 2 +- tests/test_multipart.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python_multipart/multipart.py b/python_multipart/multipart.py index a0f0482..f7f7f9f 100644 --- a/python_multipart/multipart.py +++ b/python_multipart/multipart.py @@ -1051,7 +1051,7 @@ def __init__( if not isinstance(max_size, Number) or max_size < 1: raise ValueError("max_size must be a positive number, not %r" % max_size) - self.max_size: int | float = max_size + self.max_size = max_size self._current_size = 0 self.max_header_count = max_header_count diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 061d1cc..292cd21 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -860,7 +860,7 @@ def test_multipart_opening_boundary_max_size() -> None: max_size=max_size, ) assert parser.write(data) == max_size - parser.max_size = len(data) + parser.max_size = len(data) # type: ignore[assignment] # mypy narrows max_size to float & Number. assert parser.write(data[max_size:]) == len(data) - max_size parser.finalize() assert events == ["begin", "end"]