Skip to content
Merged
Show file tree
Hide file tree
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
11 changes: 7 additions & 4 deletions pcapkit/protocols/application/httpv2.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from pcapkit.const.http.error_code import ErrorCode as Enum_ErrorCode
from pcapkit.const.http.frame import Frame as Enum_Frame
from pcapkit.const.http.setting import Setting as Enum_Setting
from pcapkit.corekit.multidict import OrderedMultiDict
from pcapkit.corekit.multidict import MultiDict, OrderedMultiDict
from pcapkit.protocols.application.http import HTTP as HTTPBase
from pcapkit.protocols.data.application.httpv2 import HTTP as Data_HTTP
from pcapkit.protocols.data.application.httpv2 import ContinuationFrame as Data_ContinuationFrame
Expand Down Expand Up @@ -1282,15 +1282,16 @@ def _make_http_rst_stream(self, frame: 'Optional[Data_RSTStreamFrame]' = None, *

def _make_http_settings(self, frame: 'Optional[Data_SettingsFrame]' = None, *,
ack: 'bool' = False,
settings: 'Optional[OrderedMultiDict[Enum_Setting, int] | bytes | list[Schema_SettingPair | tuple[Enum_Setting, int]]]' = None, # pylint: disable=line-too-long
settings: 'Optional[OrderedMultiDict[Enum_Setting, int] | dict[Enum_Setting | int, int] | bytes | list[Schema_SettingPair | tuple[Enum_Setting, int]]]' = None, # pylint: disable=line-too-long
**kwargs: 'Any') -> 'tuple[Schema_SettingsFrame, Flags]':
"""Make HTTP/2 ``SETTINGS`` frame.

Args:
frame: Frame data model.
ack: Acknowledge flag.
settings: Settings, as raw octets, an
:class:`~pcapkit.corekit.multidict.OrderedMultiDict` or a list of
:class:`~pcapkit.corekit.multidict.OrderedMultiDict`, a plain
:obj:`dict` of identifier to value or a list of
``(identifier, value)`` pairs.
**kwargs: Arbitrary keyword arguments.

Expand All @@ -1314,7 +1315,9 @@ def _make_http_settings(self, frame: 'Optional[Data_SettingsFrame]' = None, *,
settings_val = settings # type: bytes | list[Schema_SettingPair]
elif isinstance(settings, dict):
settings_val = []
for key, val in settings.items(multi=True):
# A plain ``dict`` has no ``multi`` keyword (#2223).
items = settings.items(multi=True) if isinstance(settings, MultiDict) else settings.items()
for key, val in items:
settings_val.append(Schema_SettingPair(
id=key,
value=val,
Expand Down
73 changes: 73 additions & 0 deletions tests/protocols/application/test_httpv2_settings_dict_2223_unit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
"""HTTP/2 ``SETTINGS`` frames build from a plain :obj:`dict`.

GitHub issue #2223: ``HTTP._make_http_settings`` accepted a :obj:`dict` but
called ``settings.items(multi=True)``, which only
:class:`~pcapkit.corekit.multidict.MultiDict` supports, so a plain :obj:`dict`
raised a stdlib :exc:`TypeError` instead of building the frame. A plain
:obj:`dict` builds the same octets as the equivalent list of pairs, and an
:class:`~pcapkit.corekit.multidict.OrderedMultiDict` keeps its repeated
identifiers.

Every case builds its own octets in memory and reads no capture. Classes are
imported inside each test, after
:func:`~tests._support.reimport_once_per_class`.

"""

import unittest

from tests._support import reimport_once_per_class

#: ``HEADER_TABLE_SIZE = 2`` as a ``SETTINGS`` frame on stream 0.
ONE_PAIR = bytes.fromhex('000006040000000000000100000002')


class TestHTTPv2SettingsPlainDict(unittest.TestCase):
"""Pin ``SETTINGS`` construction from each accepted mapping type."""

def setUp(self) -> None:
reimport_once_per_class(self)

def build(self, settings: 'object') -> 'bytes':
"""Build a ``SETTINGS`` frame from ``settings``."""
from pcapkit.const.http.frame import Frame
from pcapkit.protocols.application.httpv2 import HTTP

return bytes(HTTP(type=Frame.SETTINGS, frame={'settings': settings}))

def test_plain_dict_builds_the_list_octets(self) -> None:
from pcapkit.const.http.setting import Setting

self.assertEqual(self.build([(Setting.HEADER_TABLE_SIZE, 2)]), ONE_PAIR)
for name, settings in (('int key', {1: 2}),
('enum key', {Setting.HEADER_TABLE_SIZE: 2})):
with self.subTest(settings=name):
self.assertEqual(self.build(settings), ONE_PAIR)

def test_plain_dict_keeps_insertion_order(self) -> None:
settings = {3: 100, 1: 4096, 4: 65535}
self.assertEqual(self.build(settings), self.build(list(settings.items())))

def test_plain_dict_frame_parses_back(self) -> None:
import io

from pcapkit.protocols.application.httpv2 import HTTP

raw = self.build({1: 4096, 3: 100})
info = HTTP(io.BytesIO(raw), len(raw)).info
self.assertEqual([(int(k), v) for k, v in info.settings.items(multi=True)],
[(1, 4096), (3, 100)])

def test_ordered_multidict_keeps_repeated_identifiers(self) -> None:
from pcapkit.corekit.multidict import OrderedMultiDict

pairs = [(1, 4096), (1, 2)]
self.assertEqual(self.build(OrderedMultiDict(pairs)), self.build(pairs))

def test_empty_plain_dict_builds_an_empty_frame(self) -> None:
self.assertEqual(self.build({}), bytes.fromhex('000000040000000000'))


if __name__ == '__main__':
unittest.main()
Loading