From e557a07def7b6199325ed6506656b1738a51d72b Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Sun, 1 Nov 2026 22:41:34 -0500 Subject: [PATCH] refactor(protocols): range-check link and SSLv2 makers with _make_uint (#2614) - GRE, FRAD, Netlink, Loopback: make()'s inline unsigned-int checks go through ProtocolBase._make_uint; Netlink and Loopback pack with _make_pack. The byte-order checks stay inline. - FRAD: a float flag bit such as cr=1.0 raises ProtocolError rather than a raw ValueError; the flag and snap_pid messages drop their suffix. - SSLv2: padding, and session_id_hit in make() and read(), use _make_uint; a bool or non-int 3-octet padding raises ProtocolError. Messages read 'SSLv2: invalid : '; their pins follow. - OSPF keeps its check: _make_uint reads self.alias, unset before read(). - The #1739 sweep skips the bits=/size= field width a value helper takes. Byte-identical make() output on a 5743-case grid and the samples; tests pass. --- pcapkit/protocols/application/sslv2.py | 23 +++------ pcapkit/protocols/link/frad.py | 22 +++------ pcapkit/protocols/link/gre.py | 23 +++------ pcapkit/protocols/link/loopback.py | 9 ++-- pcapkit/protocols/link/netlink.py | 26 +++++----- .../test_rfc_constants_one_home_1739_unit.py | 49 ++++++++++++------- .../protocols/application/test_sslv2_unit.py | 24 ++++++--- tests/protocols/link/test_frad_unit.py | 13 +++++ tests/protocols/link/test_loopback_unit.py | 12 ++++- 9 files changed, 110 insertions(+), 91 deletions(-) diff --git a/pcapkit/protocols/application/sslv2.py b/pcapkit/protocols/application/sslv2.py index 54ab497031..72587e5b20 100644 --- a/pcapkit/protocols/application/sslv2.py +++ b/pcapkit/protocols/application/sslv2.py @@ -217,9 +217,7 @@ def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_SSLv2': elif isinstance(body_schema, Schema_ServerHello): # NOTE: wire bytes always fit; this refuses a programmatically built # schema, e.g. ``make(body=Schema_ServerHello(session_id_hit=256))``. - if not 0 <= body_schema.session_id_hit <= 255: - raise ProtocolError(f'SSLv2: SESSION-ID-HIT ({body_schema.session_id_hit}) out ' - f'of range for a single octet (0-255)') + self._make_uint(int(body_schema.session_id_hit), bits=8, name='session_id_hit') body = Data_ServerHello( # NOTE: kept as the raw octet, not normalised to a strict # bool -- see ``Data_ServerHello.session_id_hit``. @@ -297,8 +295,9 @@ def make(self, for the former, ``SERVER_HELLO`` for the latter); if ``body`` is of a type this maker does not recognise; if the resulting record is longer than the chosen header form - allows; or if ``padding`` does not fit the 3-octet header - form's single padding octet (``0``-``255``). + allows; or if ``padding`` is not an :class:`int` (a + :obj:`bool` included) that fits the 3-octet header form's + single padding octet (``0``-``255``). """ msg_type_val = cast('Enum_HandshakeType', @@ -363,11 +362,8 @@ def make(self, if not 0 <= record_length <= 0x3FFF: raise ProtocolError(f'SSLv2: record ({record_length} octet(s)) too long for ' f"the 3-octet header form's 16383-octet limit") - if not 0 <= padding <= 0xFF: - raise ProtocolError(f'SSLv2: padding ({padding}) out of range for the ' - f"3-octet header form's single padding octet (0-255)") indicator = (0x40 if is_escape else 0x00) | (record_length >> 8) - padding_val = padding + padding_val = self._make_uint(padding, bits=8, name='padding') length_low = record_length & 0xFF return Schema_SSLv2( @@ -498,12 +494,9 @@ def _make_sslv2_server_hello(self, self._make_index(server_version, server_version_default, namespace=server_version_namespace, reversed=server_version_reversed, pack=False)) - session_id_hit_val = int(session_id_hit) - if not 0 <= session_id_hit_val <= 255: - # NOTE: ``UInt8Field`` packs mod 256, so ``256`` would silently - # write as ``0x00``, turning a hit into a miss. - raise ProtocolError(f'SSLv2: SESSION-ID-HIT ({session_id_hit_val}) out of range ' - f'for a single octet (0-255)') + # NOTE: ``int()`` first, so the documented ``True``/``False`` pass; + # ``UInt8Field`` packs mod 256, so ``256`` would turn a hit into a miss. + session_id_hit_val = self._make_uint(int(session_id_hit), bits=8, name='session_id_hit') return Schema_ServerHello( # NOTE: preserved as given, not collapsed to a strict 0/1 -- # see ``Data_ServerHello.session_id_hit``. diff --git a/pcapkit/protocols/link/frad.py b/pcapkit/protocols/link/frad.py index 9e943da988..9539ea504c 100644 --- a/pcapkit/protocols/link/frad.py +++ b/pcapkit/protocols/link/frad.py @@ -242,26 +242,20 @@ def make(self, this package keeps (:issue:`1458`). """ - if not isinstance(dlci, int) or isinstance(dlci, bool) or not 0 <= dlci <= 0x3FF: - raise ProtocolError(f'{self.alias}: invalid DLCI: {dlci!r}') - for name, bit in (('cr', cr), ('fecn', fecn), ('becn', becn), ('de', de)): - if bit not in (0, 1): - raise ProtocolError(f'{self.alias}: invalid {name}: {bit!r}, must be 0 or 1') - if not isinstance(control, int) or isinstance(control, bool) or not 0 <= control <= 0xFF: - raise ProtocolError(f'{self.alias}: invalid control: {control!r}') - if not isinstance(nlpid, int) or isinstance(nlpid, bool) or not 0 <= nlpid <= 0xFF: - raise ProtocolError(f'{self.alias}: invalid nlpid: {nlpid!r}') + dlci = self._make_uint(dlci, bits=10, name='DLCI') + cr = self._make_uint(cr, bits=1, name='cr') + fecn = self._make_uint(fecn, bits=1, name='fecn') + becn = self._make_uint(becn, bits=1, name='becn') + de = self._make_uint(de, bits=1, name='de') + control = self._make_uint(control, bits=8, name='control') + nlpid = self._make_uint(nlpid, bits=8, name='nlpid') if nlpid == 0x80: if not isinstance(snap_oui, (bytes, bytearray)) or len(snap_oui) != 3: raise ProtocolError(f'{self.alias}: invalid snap_oui: {snap_oui!r}, ' f'must be exactly 3 octets when nlpid is 0x80') - if (not isinstance(snap_pid, int) or isinstance(snap_pid, bool) - or not 0 <= snap_pid <= 0xFFFF): - raise ProtocolError(f'{self.alias}: invalid snap_pid: {snap_pid!r}, ' - f'must fit in 16 bits when nlpid is 0x80') _schema_nlpid, _ext_nlpid = 0, 0x80 - _oui, _pid = bytes(snap_oui), snap_pid + _oui, _pid = bytes(snap_oui), self._make_uint(snap_pid, bits=16, name='snap_pid') else: if snap_oui is not None or snap_pid is not None: raise ProtocolError(f'{self.alias}: snap_oui and snap_pid are only meaningful ' diff --git a/pcapkit/protocols/link/gre.py b/pcapkit/protocols/link/gre.py index 3e05581127..9fd66a6931 100644 --- a/pcapkit/protocols/link/gre.py +++ b/pcapkit/protocols/link/gre.py @@ -257,30 +257,23 @@ def make(self, if reserved1 is not None and not checksum_present: raise ProtocolError(f'{self.alias}: reserved1 given without checksum') if checksum_present: - if (not isinstance(checksum, int) or isinstance(checksum, bool) - or not 0 <= checksum <= 0xFFFF): - raise ProtocolError(f'{self.alias}: invalid checksum: {checksum!r}') + checksum = self._make_uint(checksum, bits=16, name='checksum') if reserved1 is None: reserved1 = 0 - elif (not isinstance(reserved1, int) or isinstance(reserved1, bool) - or not 0 <= reserved1 <= 0xFFFF): - raise ProtocolError(f'{self.alias}: invalid reserved1: {reserved1!r}') + else: + reserved1 = self._make_uint(reserved1, bits=16, name='reserved1') key_present = key is not None - if key_present and (not isinstance(key, int) or isinstance(key, bool) - or not 0 <= key <= 0xFFFF_FFFF): - raise ProtocolError(f'{self.alias}: invalid key: {key!r}') + if key_present: + key = self._make_uint(key, bits=32, name='key') sequence_present = sequence_number is not None - if sequence_present and (not isinstance(sequence_number, int) - or isinstance(sequence_number, bool) - or not 0 <= sequence_number <= 0xFFFF_FFFF): - raise ProtocolError(f'{self.alias}: invalid sequence_number: {sequence_number!r}') + if sequence_present: + sequence_number = self._make_uint(sequence_number, bits=32, name='sequence_number') if reserved & ~0x4FF8: raise ProtocolError(f'{self.alias}: reserved bits outside 0x4FF8: {reserved:#06x}') - if not isinstance(version, int) or isinstance(version, bool) or not 0 <= version <= 7: - raise ProtocolError(f'{self.alias}: invalid version: {version!r}') + self._make_uint(version, bits=3, name='version') return Schema_GRE( flags={ diff --git a/pcapkit/protocols/link/loopback.py b/pcapkit/protocols/link/loopback.py index 3623d5d887..18154c4bde 100644 --- a/pcapkit/protocols/link/loopback.py +++ b/pcapkit/protocols/link/loopback.py @@ -242,13 +242,12 @@ def make(self, """ if byteorder not in ('big', 'little'): raise ProtocolError(f'Loopback: invalid byte order: {byteorder!r}') - # NOTE: ``bool`` is an ``int`` subclass, so without the explicit check - # ``True`` would pack as address family 1 (#2289). - if isinstance(family, bool) or not isinstance(family, int) or not 0 <= family <= 0xFFFF_FFFF: - raise ProtocolError(f'Loopback: invalid address family: {family!r}') + # NOTE: ``_make_uint`` refuses a ``bool``, which would otherwise pack + # as address family 1 (#2289). + family = self._make_uint(family, bits=32, name='address family') return Schema_Loopback( - family=family.to_bytes(4, byteorder), + family=self._make_pack(family, size=4, lilendian=byteorder == 'little'), payload=payload, ) diff --git a/pcapkit/protocols/link/netlink.py b/pcapkit/protocols/link/netlink.py index ea1af6e7e2..2fac89db93 100644 --- a/pcapkit/protocols/link/netlink.py +++ b/pcapkit/protocols/link/netlink.py @@ -290,25 +290,23 @@ def make(self, """ if byteorder not in ('big', 'little'): raise ProtocolError(f'{self.alias}: invalid byte order: {byteorder!r}') - for name, value in (('nlmsg_type', nlmsg_type), ('nlmsg_flags', nlmsg_flags)): - if not isinstance(value, int) or isinstance(value, bool) or not 0 <= value <= 0xFFFF: - raise ProtocolError(f'{self.alias}: invalid {name}: {value!r}') - for name, value in (('nlmsg_seq', nlmsg_seq), ('nlmsg_pid', nlmsg_pid)): - if not isinstance(value, int) or isinstance(value, bool) or not 0 <= value <= 0xFFFF_FFFF: - raise ProtocolError(f'{self.alias}: invalid {name}: {value!r}') + nlmsg_type = self._make_uint(nlmsg_type, bits=16, name='nlmsg_type') + nlmsg_flags = self._make_uint(nlmsg_flags, bits=16, name='nlmsg_flags') + nlmsg_seq = self._make_uint(nlmsg_seq, bits=32, name='nlmsg_seq') + nlmsg_pid = self._make_uint(nlmsg_pid, bits=32, name='nlmsg_pid') if nlmsg_len is None: nlmsg_len = 16 + len(payload) - elif (not isinstance(nlmsg_len, int) or isinstance(nlmsg_len, bool) - or not 0 <= nlmsg_len <= 0xFFFF_FFFF): - raise ProtocolError(f'{self.alias}: invalid nlmsg_len: {nlmsg_len!r}') + else: + nlmsg_len = self._make_uint(nlmsg_len, bits=32, name='nlmsg_len') + lilendian = byteorder == 'little' return Schema_Netlink( - nlmsg_len=nlmsg_len.to_bytes(4, byteorder), - nlmsg_type=nlmsg_type.to_bytes(2, byteorder), - nlmsg_flags=nlmsg_flags.to_bytes(2, byteorder), - nlmsg_seq=nlmsg_seq.to_bytes(4, byteorder), - nlmsg_pid=nlmsg_pid.to_bytes(4, byteorder), + nlmsg_len=self._make_pack(nlmsg_len, size=4, lilendian=lilendian), + nlmsg_type=self._make_pack(nlmsg_type, size=2, lilendian=lilendian), + nlmsg_flags=self._make_pack(nlmsg_flags, size=2, lilendian=lilendian), + nlmsg_seq=self._make_pack(nlmsg_seq, size=4, lilendian=lilendian), + nlmsg_pid=self._make_pack(nlmsg_pid, size=4, lilendian=lilendian), payload=payload, ) diff --git a/tests/project/test_rfc_constants_one_home_1739_unit.py b/tests/project/test_rfc_constants_one_home_1739_unit.py index 63c77aa558..6f2b7c7585 100644 --- a/tests/project/test_rfc_constants_one_home_1739_unit.py +++ b/tests/project/test_rfc_constants_one_home_1739_unit.py @@ -18,7 +18,8 @@ reads it, and :mod:`pcapkit.toolkit.pypcapfile`, a reader of several, binds none of those names at all; * :class:`TestNotReinlined` -- no module in :data:`SWEPT`, the homes included, - spells a homed value anywhere but in the home's own definition: not as an + spells a homed value anywhere but in the home's own definition, or as the + field width a value helper takes (see :func:`_field_widths`): not as an operand, a call argument, a subscript, a default, a returned value, a tuple or table element, a branch of a conditional, nor an assignment at any level. Each match that means something else is listed in :data:`NOT_A_COPY` or @@ -237,9 +238,6 @@ 'the IP version number, not the Next Header offset', ('pcapkit/protocols/schema/internet/ipv6.py', 'ptr + 6'): 'the end of a Jumbo Payload option (2 octets of header, 4 of value)', - ('pcapkit/protocols/link/frad.py', '0 <= snap_pid <= 65535'): - "the max value of a SNAP PID, FRAD's own 2-octet field (:rfc:`2427#section-3`), " - 'not an IP total length', ('pcapkit/protocols/link/linux_sll1.py', 'return 16'): 'the fixed length of the LINUX_SLL (v1) header, not the PCAP record header', ('pcapkit/protocols/link/linux_sll2.py', 'return 20'): @@ -374,12 +372,6 @@ ('pcapkit/protocols/link/gre.py', 'return 16'): "GRE's own longest possible header -- the 4-octet base plus all three optional " 'extensions at 4 octets each (:rfc:`2784`/:rfc:`2890`) -- not a PCAP record header', - ('pcapkit/protocols/link/gre.py', '0 <= checksum <= 65535'): - "the max value of GRE's own 2-octet Checksum field (:rfc:`2784#section-2.5`), " - 'not an IP total length', - ('pcapkit/protocols/link/gre.py', '0 <= reserved1 <= 65535'): - "the max value of GRE's own 2-octet Reserved1 field (:rfc:`2784#section-2.6`), " - 'not an IP total length', ('pcapkit/protocols/schema/link/gre.py', "{'reserved_2': (4, 9)}"): "the 9-bit span bits 4-12 leave of RFC 2784's 12-bit Reserved0 once RFC 2890 " 'carves ``K``/``S`` out of it (:rfc:`2890#section-2`), not a PCAP-NG resolution-places ' @@ -449,17 +441,14 @@ 'and the 28-octet IPv4 ARP length'), 'pcapkit/protocols/link/loopback.py': ((24, 28), 'the BSD loopback address families of IPv6 ' '(NetBSD and OpenBSD 24, FreeBSD 28)'), - 'pcapkit/protocols/link/ieee802_11.py': ((6, 9, 12, 16, 32), 'a 6-octet MAC address, ' - 'Frame Control bit offsets, and the 12-, 16- and ' - '32-bit widths of 802.11 header fields'), + 'pcapkit/protocols/link/ieee802_11.py': ((6, 9, 12), 'a 6-octet MAC address, and ' + 'Frame Control bit offsets'), 'pcapkit/protocols/link/l2tpv2.py': ((6, 12, 16), 'L2TPv2 header flags, widths and length'), 'pcapkit/protocols/link/linux_sll1.py': ((65535,), "the max value of LINUX_SLL's own " '2-octet link_addr_length field, not an IP total ' 'length'), - 'pcapkit/protocols/link/netlink.py': ((16, 65535), "the nlmsghdr fixed header length (16 " - 'octets, not a PCAP record header) and the max value ' - "of its 16-bit nlmsg_type/nlmsg_flags fields (not " - 'an IPv4 total length)'), + 'pcapkit/protocols/link/netlink.py': ((16,), 'the nlmsghdr fixed header length (16 ' + 'octets), not a PCAP record header'), 'pcapkit/protocols/link/radiotap.py': ((65535,), "the max value of radiotap's own " '2-octet it_len field, not an IP total length'), 'pcapkit/protocols/schema/link/ethernet.py': ((6,), 'a MAC address is 6 octets'), @@ -619,6 +608,27 @@ def _context(text: 'str', operand: 'ast.Constant', parents: 'dict[int, ast.AST]' return _segment(text, node) +#: Value helpers and the keyword each takes a field width by, in bits or octets (#2614). +_WIDTHS = {'_make_uint': 'bits', '_make_sint': 'bits', '_make_pack': 'size', + '_read_uint': 'size', '_make_octets': 'size'} + + +def _field_widths(tree: 'ast.Module') -> 'set[int]': + """``id`` of every constant given as a width to a :data:`_WIDTHS` helper. + + A field's own width, not a copy of a homed constant that shares its value. + + """ + exempt = set() # type: set[int] + for node in ast.walk(tree): + callee = getattr(getattr(node, 'func', None), 'attr', None) + if isinstance(node, ast.Call) and callee in _WIDTHS: + exempt.update(id(keyword.value) for keyword in node.keywords + if keyword.arg == _WIDTHS[callee] + and isinstance(keyword.value, ast.Constant)) + return exempt + + def _sweep() -> 'list[tuple[str, int, int, str]]': """Every homed value in :data:`SWEPT`: module, value, line, expression. @@ -626,7 +636,8 @@ def _sweep() -> 'list[tuple[str, int, int, str]]': subscript, a default, a returned value, an element of a tuple or a table, a branch of a conditional, or an assignment at any level, which is how a renamed copy (``_TCP_HDR = 20``) would read. Only the homes' own - definitions, their tables included, are not hits. + definitions, their tables included, and the widths :func:`_field_widths` + finds are not hits. """ values = _swept_values() @@ -637,7 +648,7 @@ def _sweep() -> 'list[tuple[str, int, int, str]]': text = path.read_text(encoding='utf-8') tree = ast.parse(text) parents = _parents(tree) - sites = _home_sites(rel, tree) + sites = _home_sites(rel, tree) | _field_widths(tree) for node in ast.walk(tree): if (isinstance(node, ast.Constant) and type(node.value) is int and node.value in values and id(node) not in sites): diff --git a/tests/protocols/application/test_sslv2_unit.py b/tests/protocols/application/test_sslv2_unit.py index e870c6f5a4..6e1e6d6794 100644 --- a/tests/protocols/application/test_sslv2_unit.py +++ b/tests/protocols/application/test_sslv2_unit.py @@ -360,15 +360,17 @@ def test_zero_record_length_schema_leaves_the_next_record_as_payload(self) -> No def test_make_rejects_out_of_range_padding(self) -> None: """``padding`` is written as a single octet for the 3-octet header - form, so an out-of-range value is refused rather than wrapped mod 256.""" + form, so an out-of-range value is refused rather than wrapped mod 256; + a ``bool``, which would pack as ``0x00``/``0x01``, and a non-``int``, + which would reach a bare ``TypeError``, are refused too (#2614).""" from pcapkit.protocols.application.sslv2 import SSLv2 from pcapkit.utilities.exceptions import ProtocolError - for padding in (256, 300, -1): + for padding in (256, 300, -1, True, False, '1', 1.0, None): with self.subTest(padding=padding): - with self.assertRaisesRegex(ProtocolError, - rf'SSLv2: padding \({padding}\) out of range'): + with self.assertRaises(ProtocolError) as ctx: SSLv2(msg_type=1, two_byte_header=False, padding=padding, body=b'') + self.assertEqual(str(ctx.exception), f'SSLv2: invalid padding: {padding!r}') self.assertEqual(SSLv2(msg_type=1, two_byte_header=False, padding=255, body=b'').data[2], 255) @@ -454,7 +456,7 @@ def test_make_rejects_out_of_range_session_id_hit(self) -> None: from pcapkit.utilities.exceptions import ProtocolError for bad in (256, 300, -1): - with self.assertRaisesRegex(ProtocolError, r'SSLv2: SESSION-ID-HIT \(' + str(bad) + r'\) out of range'): + with self.assertRaisesRegex(ProtocolError, rf'^SSLv2: invalid session_id_hit: {bad}$'): SSLv2(msg_type=HandshakeType.SERVER_HELLO, body={ 'session_id_hit': bad, 'certificate_type': 0, 'certificate': b'', 'cipher_specs': b'', 'connection_id': b'\x00' * 16, @@ -700,7 +702,8 @@ def test_read_rejects_out_of_range_session_id_hit_on_a_prebuilt_schema(self) -> """A prebuilt :class:`~pcapkit.protocols.schema.application.sslv2.ServerHello` bypasses ``make()``'s own ``SESSION-ID-HIT`` range check; ``read()``'s guard is what refuses it, where it would otherwise pack as ``0x00`` - and silently turn a HIT into a MISS.""" + and silently turn a HIT into a MISS. The guard takes ``int()`` first, + so a ``bool``, a documented spelling, passes it (#2614).""" from pcapkit.const.sslv2.handshake_type import HandshakeType from pcapkit.protocols.application.sslv2 import SSLv2 from pcapkit.protocols.schema.application.sslv2 import ServerHello as Schema_ServerHello @@ -711,8 +714,13 @@ def test_read_rejects_out_of_range_session_id_hit_on_a_prebuilt_schema(self) -> cipher_specs_length=0, connection_id_length=16, certificate=b'', cipher_specs=b'', connection_id=b'\x00' * 16, ) - with self.assertRaisesRegex(ProtocolError, r'SSLv2: SESSION-ID-HIT \(256\) out of range'): + with self.assertRaisesRegex(ProtocolError, r'^SSLv2: invalid session_id_hit: 256$'): SSLv2(msg_type=HandshakeType.SERVER_HELLO, body=schema) + for hit in (False, True): + with self.subTest(hit=hit): + schema.session_id_hit = hit + proto = SSLv2(msg_type=HandshakeType.SERVER_HELLO, body=schema) + self.assertEqual(proto.data[3], int(hit)) # 2-octet header, MSG-TYPE, the hit def test_client_hello_length_mismatch_falls_back_to_raw_body(self) -> None: """A ``CLIENT-HELLO``-tagged body whose declared sub-field lengths @@ -743,7 +751,7 @@ def test_make_itself_rejects_out_of_range_session_id_hit(self) -> None: from pcapkit.utilities.exceptions import ProtocolError proto = SSLv2(msg_type=HandshakeType.CLIENT_FINISHED, body=b'') - with self.assertRaisesRegex(ProtocolError, r'SSLv2: SESSION-ID-HIT \(256\) out of range'): + with self.assertRaisesRegex(ProtocolError, r'^SSLv2: invalid session_id_hit: 256$'): proto.make(msg_type=HandshakeType.SERVER_HELLO, body={'session_id_hit': 256}) diff --git a/tests/protocols/link/test_frad_unit.py b/tests/protocols/link/test_frad_unit.py index eba1e7a58f..9867fe4303 100644 --- a/tests/protocols/link/test_frad_unit.py +++ b/tests/protocols/link/test_frad_unit.py @@ -199,6 +199,19 @@ def test_make_rejects_an_invalid_flag_bit(self) -> None: with self.subTest(**kwargs), self.assertRaises(ProtocolError): FRAD(**kwargs) + def test_make_rejects_a_float_flag_bit(self) -> None: + """``1.0`` equals ``1`` but is no bit, so it raises ``ProtocolError`` + rather than reaching the bit-field packer's ``ValueError`` (#2614).""" + from pcapkit.protocols.link.frad import FRAD + from pcapkit.utilities.exceptions import ProtocolError + + for name in ('cr', 'fecn', 'becn', 'de'): + for bit in (0.0, 1.0): + with self.subTest(name=name, bit=bit): + with self.assertRaises(ProtocolError) as ctx: + FRAD(**{name: bit}) + self.assertEqual(str(ctx.exception), f'FRAD: invalid {name}: {bit!r}') + def test_make_tolerates_nlpid_zero_for_short_read_replay(self) -> None: """``0x00`` is the SNAP pad marker, never a value of its own on the wire, but :meth:`make` must not refuse it: see the ``Note`` on diff --git a/tests/protocols/link/test_loopback_unit.py b/tests/protocols/link/test_loopback_unit.py index ccae463acf..394c23565d 100644 --- a/tests/protocols/link/test_loopback_unit.py +++ b/tests/protocols/link/test_loopback_unit.py @@ -127,6 +127,15 @@ def test_make_rejects_what_does_not_fit(self) -> None: with self.subTest(**kwargs), self.assertRaises(ProtocolError): Loopback(**kwargs) + def test_make_names_an_unknown_byteorder(self) -> None: + """The byte-order check is inline, not ``_make_uint``'s, with this prefix (#2614).""" + from pcapkit.protocols.link.loopback import Loopback + from pcapkit.utilities.exceptions import ProtocolError + + with self.assertRaises(ProtocolError) as ctx: + Loopback(byteorder='native') + self.assertEqual(str(ctx.exception), "Loopback: invalid byte order: 'native'") + def test_make_rejects_a_family_that_is_not_an_int(self) -> None: # #2289: a bool is an int and packs as family 1; a non-int fails the # range comparison or ``to_bytes`` with a bare ``TypeError`` or @@ -254,8 +263,9 @@ def test_read_rejects_an_unknown_byteorder(self) -> None: from pcapkit.utilities.exceptions import ProtocolError raw = _header(2, 'little') + IPV4 - with self.assertRaises(ProtocolError): + with self.assertRaises(ProtocolError) as ctx: Loopback(raw, len(raw), byteorder='native') + self.assertEqual(str(ctx.exception), "Loopback: invalid byte order: 'native'") def test_family_byteorder(self) -> None: from pcapkit.const.reg.linktype import LinkType