Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 110 additions & 1 deletion h11/tests/test_receivebuffer.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import re
from typing import Tuple
from types import SimpleNamespace
from typing import List, Match, Optional, Tuple

import pytest

from .. import _receivebuffer
from .._receivebuffer import ReceiveBuffer


Expand Down Expand Up @@ -133,3 +135,110 @@ def test_receivebuffer_for_invalid_delimiter(data: Tuple[bytes]) -> None:
b"Connection: close",
]
assert bytes(b) == b"Some body"


@pytest.fixture
def receivebuffer_with_scan_sizes(
monkeypatch: pytest.MonkeyPatch,
) -> Tuple[ReceiveBuffer, List[int]]:
"""Count search-window sizes, not timings or native bytearray copying."""

# A subclass lets us wrap find on this instance without replacing its storage.
class TrackedBytearray(bytearray):
pass

data = TrackedBytearray()
scan_sizes: List[int] = []
real_find = data.find
real_search = _receivebuffer.blank_line_regex.search

def find(sub: bytes, start: int = 0) -> int:
scan_sizes.append(max(0, len(data) - start))
return real_find(sub, start)

def search(buffer: bytearray, start: int = 0) -> Optional[Match[bytes]]:
scan_sizes.append(max(0, len(buffer) - start))
return real_search(buffer, start)

monkeypatch.setattr(data, "find", find)
monkeypatch.setattr(
_receivebuffer, "blank_line_regex", SimpleNamespace(search=search)
)
b = ReceiveBuffer()
b._data = data
return b, scan_sizes


@pytest.mark.parametrize("size", [256, 4096])
@pytest.mark.parametrize("consume", [0, 32])
def test_receivebuffer_next_line_scan_bounds(
receivebuffer_with_scan_sizes: Tuple[ReceiveBuffer, List[int]],
size: int,
consume: int,
) -> None:
b, scan_sizes = receivebuffer_with_scan_sizes
line = b"x" * size + b"\r\n"
following = b"next\r\n"

for _ in range(3):
scan_sizes.clear()
for byte in line[:-1]:
b += bytes([byte])
assert b.maybe_extract_next_line() is None
assert b.maybe_extract_next_line() is None

if consume:
assert b.maybe_extract_at_most(consume) == line[:consume]

b += line[-1:] + following
assert b.maybe_extract_next_line() == line[consume:]
assert b.maybe_extract_next_line() == following
assert not b

# Allow delimiter overlap, unchanged-buffer polls, and a rescan after
# consuming a prefix, but not a full-buffer scan for every new byte.
assert scan_sizes
assert sum(scan_sizes) <= 8 * (len(line) + len(following))


@pytest.mark.parametrize("size", [256, 4096])
@pytest.mark.parametrize("consume", [0, 32])
@pytest.mark.parametrize(
"line_ending,blank_line",
[
pytest.param(b"\r\n", b"\r\n", id="crlf"),
pytest.param(b"\n", b"\n", id="lf"),
pytest.param(b"\n", b"\r\n", id="lf-crlf"),
pytest.param(b"\r\n", b"\n", id="crlf-lf"),
],
)
def test_receivebuffer_lines_scan_bounds(
receivebuffer_with_scan_sizes: Tuple[ReceiveBuffer, List[int]],
size: int,
consume: int,
line_ending: bytes,
blank_line: bytes,
) -> None:
b, scan_sizes = receivebuffer_with_scan_sizes
lines = [b"first: " + b"x" * size, b"second: value"]
block = line_ending.join(lines) + line_ending + blank_line
following = b"next: value\r\n\r\n"

for _ in range(3):
scan_sizes.clear()
for byte in block[:-1]:
b += bytes([byte])
assert b.maybe_extract_lines() is None
assert b.maybe_extract_lines() is None

if consume:
assert b.maybe_extract_at_most(consume) == block[:consume]

b += block[-1:] + following + blank_line
assert b.maybe_extract_lines() == [lines[0][consume:], lines[1]]
assert b.maybe_extract_lines() == [b"next: value"]
assert b.maybe_extract_lines() == []
assert not b

assert scan_sizes
assert sum(scan_sizes) <= 8 * (len(block) + len(following) + len(blank_line))