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/internet/ipv6_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,12 +578,15 @@ def __post_init__(self, file: 'Optional[IO[bytes] | bytes]' = None, length: 'Opt
def __length_hint__(self) -> 'int':
"""Return an estimated length for the object.

Two octets -- all :rfc:`6564#section-4` guarantees. Annotated ``int``
rather than ``Literal[2]``, for the reason given on :attr:`name`: every
subclass has a longer fixed header and its own ``Literal``.
Eight octets, the shortest header :rfc:`6564#section-4` allows: Hdr
Ext Len counts 8-octet units "not including the first 8 octets", and
:rfc:`8200#section-4` makes every extension header a multiple of 8
octets. Annotated ``int`` rather than ``Literal[8]``, for the reason
given on :attr:`name`: every subclass has its own fixed header and its
own ``Literal``.

"""
return 2
return 8

@classmethod
def __index__(cls) -> 'NoReturn':
Expand Down
74 changes: 74 additions & 0 deletions tests/protocols/internet/test_ipv6_ext_hint_unit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
"""A tail shorter than 8 octets is not an ``IPv6_Ext`` header.

GitHub issue #1178. :meth:`~pcapkit.protocols.protocol.ProtocolBase._parse_next_layer`
keeps a next layer as :class:`~pcapkit.protocols.misc.raw.Raw` when fewer octets
were captured than its ``__length_hint__`` (:issue:`1170`).
:class:`~pcapkit.protocols.internet.ipv6_ext.IPv6_Ext` hinted 2, though
:rfc:`6564#section-4` and :rfc:`8200#section-4` make every extension header at
least 8 octets, so a 2--7 octet tail of a longer declared payload was parsed as
an 8-octet header built partly from octets that were never captured.

"""
from __future__ import annotations

import importlib.util
import unittest

from tests._support import reimport_once_per_class

RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper')
HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS)

#: ``alias`` values that reach the generic fallback: an absent alias, a number
#: IANA has not registered, ``Shim6`` (registered to ``IPv6_Ext`` directly) and
#: ``HOPOPT`` (the code ``IPv6`` substitutes the fallback for).
ALIASES = {'absent': {}, 'unregistered': {'alias': 200}, 'Shim6': {'alias': 140}, 'HOPOPT': {'alias': 0}}

#: The IPv6 payload length the caller declares, longer than any tail below.
DECLARED = 300


def tail(size: int) -> bytes:
"""``size`` octets opening with No Next Header and Hdr Ext Len 0 or 1."""
return bytes([59, 1 if size >= 16 else 0]) + bytes(range(1, size - 1))


@unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed')
class IPv6ExtLengthHintTests(unittest.TestCase):
"""The fallback's hint is the 8-octet minimum header."""

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

def parse(self, data: bytes, **kwargs: int) -> object:
from pcapkit.protocols.internet.ipv6 import IPv6
from pcapkit.protocols.internet.ipv6_ext import IPv6_Ext

outer = bytes([0x60]) + bytes(5) + bytes([59, 64]) + bytes(32)
return IPv6(outer, len(outer))._parse_next_layer(
IPv6_Ext, data, DECLARED, version=6, extension=True, **kwargs)

def test_short_tail_is_raw_and_rebuilds_exactly(self) -> None:
from pcapkit.protocols.misc.raw import Raw

for label, kwargs in ALIASES.items():
for size in range(2, 8):
with self.subTest(alias=label, size=size):
parsed = self.parse(tail(size), **kwargs)
self.assertIsInstance(parsed, Raw)
self.assertEqual(bytes(Raw.from_data(parsed.info)), tail(size))

def test_complete_header_still_parses(self) -> None:
from pcapkit.protocols.internet.ipv6_ext import IPv6_Ext

for label, kwargs in ALIASES.items():
for size in (8, 16):
with self.subTest(alias=label, size=size):
parsed = self.parse(tail(size), **kwargs)
self.assertIsInstance(parsed, IPv6_Ext)
self.assertEqual(parsed.length, size)


if __name__ == '__main__':
unittest.main()
8 changes: 4 additions & 4 deletions tests/protocols/internet/test_ipv6_ext_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,13 +758,13 @@ def test_an_absent_or_unregistered_alias_uses_the_generic_rule(self) -> None:
# ``read`` with no explicit length falls back to ``len(self)``
self.assertEqual(inst.read().length, 16)

def test_length_hint_is_the_two_octets_rfc_6564_guarantees(self) -> None:
"""Two, not a real header length: those two octets are all
:rfc:`6564#section-4` promises of a header this class has never seen.
def test_length_hint_is_the_eight_octet_rfc_6564_minimum(self) -> None:
"""Eight: :rfc:`6564#section-4`'s Hdr Ext Len excludes the first 8
octets, so no conforming header is shorter (:rfc:`8200#section-4`).
"""
from pcapkit.protocols.internet.ipv6_ext import IPv6_Ext

self.assertEqual(object.__new__(IPv6_Ext).__length_hint__(), 2)
self.assertEqual(object.__new__(IPv6_Ext).__length_hint__(), 8)

def test_next_is_now_shared_by_every_subclass(self) -> None:
"""None of the eight declared a ``next`` property before #917, so
Expand Down
Loading