diff --git a/docs/source/contributing/conventions/protocol-member-order.rst b/docs/source/contributing/conventions/protocol-member-order.rst index 31354b2ac7..9d2748a80f 100644 --- a/docs/source/contributing/conventions/protocol-member-order.rst +++ b/docs/source/contributing/conventions/protocol-member-order.rst @@ -50,8 +50,12 @@ by key where there is no enum. **The guard.** :file:`tests/project/test_protocol_member_order_2490_unit.py` holds every protocol class to the groups, ``_make_data()`` first, each ``_read_xxx()`` -before its ``_make_xxx()``, and subgroup b, with a table of known exceptions -(:issue:`2490`). +before its ``_make_xxx()``, subgroups b, c and d, and the order of the registry +maps, with a table of known exceptions (:issue:`2490`). It values a key that is an +enum member or an :class:`int`; a map with a key of another shape, such as the +computed keys of +:attr:`PCAPNG.__option__ `, +it leaves in its own order, which rule c then follows. Example ~~~~~~~ diff --git a/pcapkit/protocols/application/http/protocol.py b/pcapkit/protocols/application/http/protocol.py index be3dfda11c..cbc24ab2d5 100644 --- a/pcapkit/protocols/application/http/protocol.py +++ b/pcapkit/protocols/application/http/protocol.py @@ -805,6 +805,32 @@ def _guess_version(self, length: 'int', **kwargs: 'Any') -> 'HTTP': raise ProtocolError('HTTP: unknown version') + @staticmethod + def _member_of_version(version: 'Any') -> 'Type[HTTP]': + """The member a ``version`` of ``1`` or ``2`` names. + + Args: + version: Version of HTTP. + + Returns: + :class:`~pcapkit.protocols.application.httpv1.protocol.HTTPv1` or + :class:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2`. + + Raises: + ProtocolError: If ``version`` is neither, or is no :obj:`int`: a + :obj:`bool` or a :obj:`float` is refused even where it equals one. + + """ + from pcapkit.protocols.application.httpv1.protocol import HTTPv1 # isort: skip # pylint: disable=import-outside-toplevel + from pcapkit.protocols.application.httpv2.protocol import HTTPv2 # isort: skip # pylint: disable=import-outside-toplevel + + if isinstance(version, int) and not isinstance(version, bool): + if version == 1: + return HTTPv1 + if version == 2: + return HTTPv2 + raise ProtocolError(f'HTTP: invalid version: {version!r}') + @classmethod def _dispatch_version(cls, data: 'bytes', length: 'int') -> 'Type[ProtocolBase]': """Pick the class that parses the HTTP message at the start of ``data``. @@ -860,29 +886,3 @@ def _dispatch_version(cls, data: 'bytes', length: 'int') -> 'Type[ProtocolBase]' if _test_http2_frame(data): return HTTPv2 return Raw - - @staticmethod - def _member_of_version(version: 'Any') -> 'Type[HTTP]': - """The member a ``version`` of ``1`` or ``2`` names. - - Args: - version: Version of HTTP. - - Returns: - :class:`~pcapkit.protocols.application.httpv1.protocol.HTTPv1` or - :class:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2`. - - Raises: - ProtocolError: If ``version`` is neither, or is no :obj:`int`: a - :obj:`bool` or a :obj:`float` is refused even where it equals one. - - """ - from pcapkit.protocols.application.httpv1.protocol import HTTPv1 # isort: skip # pylint: disable=import-outside-toplevel - from pcapkit.protocols.application.httpv2.protocol import HTTPv2 # isort: skip # pylint: disable=import-outside-toplevel - - if isinstance(version, int) and not isinstance(version, bool): - if version == 1: - return HTTPv1 - if version == 2: - return HTTPv2 - raise ProtocolError(f'HTTP: invalid version: {version!r}') diff --git a/pcapkit/protocols/application/httpv1/protocol.py b/pcapkit/protocols/application/httpv1/protocol.py index 10df343830..9b2e2fd791 100644 --- a/pcapkit/protocols/application/httpv1/protocol.py +++ b/pcapkit/protocols/application/httpv1/protocol.py @@ -345,33 +345,6 @@ def _make_data(cls, data: 'Data_HTTP') -> 'dict[str, Any]': # type: ignore[over 'raw_header': getattr(data, 'raw_header', ()), } - def _decode_text(self, text: 'bytes') -> 'tuple[str, Optional[str]]': - """Decode start-line text, recording how to encode it back. - - The text is decoded by :meth:`self.decode - `. Where the result does - not encode back to ``text`` as UTF-8 -- which :meth:`make` uses by - default -- the detected charset is returned with it, or ``'latin-1'`` - (decoding again) if that does not reproduce ``text`` either, so that the - original octets survive a rebuild. - - Args: - text: Raw request URI or status message. - - Returns: - The decoded text and the charset to encode it with, the latter - :obj:`None` for UTF-8. - - """ - value = self.decode(text) - for charset in (None, detect(text)): - try: - if value.encode(charset or 'utf-8') == text: - return value, charset - except (LookupError, UnicodeError): - pass - return text.decode('latin-1'), 'latin-1' - def _read_http_header(self, header: 'bytes') -> 'tuple[Data_Header, OrderedMultiDict[str, str]]': """Read HTTP/1.* header. @@ -640,3 +613,30 @@ def _read_field_line(self, line: 'bytes') -> 'tuple[str, str]': if len(item) != 2: raise ProtocolError('HTTP: invalid format') return self.decode(item[0].strip()), self.decode(item[1].strip()) + + def _decode_text(self, text: 'bytes') -> 'tuple[str, Optional[str]]': + """Decode start-line text, recording how to encode it back. + + The text is decoded by :meth:`self.decode + `. Where the result does + not encode back to ``text`` as UTF-8 -- which :meth:`make` uses by + default -- the detected charset is returned with it, or ``'latin-1'`` + (decoding again) if that does not reproduce ``text`` either, so that the + original octets survive a rebuild. + + Args: + text: Raw request URI or status message. + + Returns: + The decoded text and the charset to encode it with, the latter + :obj:`None` for UTF-8. + + """ + value = self.decode(text) + for charset in (None, detect(text)): + try: + if value.encode(charset or 'utf-8') == text: + return value, charset + except (LookupError, UnicodeError): + pass + return text.decode('latin-1'), 'latin-1' diff --git a/pcapkit/protocols/application/httpv2/protocol.py b/pcapkit/protocols/application/httpv2/protocol.py index cf60e88407..7bc1dd26f9 100644 --- a/pcapkit/protocols/application/httpv2/protocol.py +++ b/pcapkit/protocols/application/httpv2/protocol.py @@ -668,23 +668,6 @@ def _make_http_length(frame: 'bytes | Schema_FrameType', flags: 'Flags') -> 'int return len(frame.fragment) return len(frame.pack()) - @staticmethod - def _read_http_flags(schema: 'Schema_FrameType') -> 'Data_Flags': - """Read the flags of an HTTP/2 frame type that defines none. - - Args: - schema: Parsed frame schema. - - Returns: - Flags with the raw flags octet as ``__value__``. - - """ - flags = Data_Flags() - flags.__update__({ - '__value__': schema.__flags__, - }) - return flags - def _read_http_none(self, schema: 'Schema_UnassignedFrame', *, header: 'Schema_HTTP') -> 'Data_UnassignedFrame': """Read HTTP packet with unassigned type. @@ -1692,6 +1675,23 @@ def _make_http_continuation(self, frame: 'Optional[Data_ContinuationFrame]' = No fragment=fragment, ), flags + @staticmethod + def _read_http_flags(schema: 'Schema_FrameType') -> 'Data_Flags': + """Read the flags of an HTTP/2 frame type that defines none. + + Args: + schema: Parsed frame schema. + + Returns: + Flags with the raw flags octet as ``__value__``. + + """ + flags = Data_Flags() + flags.__update__({ + '__value__': schema.__flags__, + }) + return flags + def _import_next_layer(self, proto: 'int', length: 'Optional[int]' = None, *, # type: ignore[override] packet: 'Optional[dict[str, Any]]' = None) -> 'ProtocolBase': """Import next layer extractor: a further whole frame, or the rest undissected. diff --git a/pcapkit/protocols/application/ospf/protocol.py b/pcapkit/protocols/application/ospf/protocol.py index 68be4d8c3b..f7866f48c7 100644 --- a/pcapkit/protocols/application/ospf/protocol.py +++ b/pcapkit/protocols/application/ospf/protocol.py @@ -421,53 +421,6 @@ def _make_data(cls, data: 'Data_OSPF') -> 'dict[str, Any]': # type: ignore[over 'trailer': data.get('trailer', b''), } - def _read_id_numbers(self, id: 'bytes') -> 'IPv4Address': - """Read router and area IDs. - - Args: - id: ID bytes. - - Returns: - Parsed IDs as an IPv4 address. - - """ - #_byte = self._read_fileng(4) - #_addr = '.'.join(str(_) for _ in _byte) - return ipaddress.ip_address(id) # type: ignore[return-value] - - def _make_id_numbers(self, id: 'IPv4Address | str | bytes | bytearray') -> 'bytes': - """Make router and area IDs. - - Args: - id: ID. - - Returns: - ID bytes. - - Raises: - FieldValueError: If ``id`` is a :obj:`bool` (see - :func:`~pcapkit.corekit.schema.ipaddress.parse_ip_address`). - - Notes: - Latent rather than live: nothing in this module calls this method - (:meth:`make` builds ``router_id``/``area_id`` straight from its - own arguments), so the only caller is a unit test. It is routed - through :func:`~pcapkit.corekit.schema.ipaddress.parse_ip_address` - anyway, so that a future caller passing a :obj:`bool` gets - :exc:`~pcapkit.utilities.exceptions.FieldValueError` rather than a - silent ``0.0.0.1``. - - The description below uses :attr:`self.__class__.__name__ - ` rather than :attr:`self.alias - `, because ``alias`` - here reads ``self._version``, which :meth:`read` only assigns - from the wire -- unavailable to a construction-only instance that - never went through :meth:`read`. - - """ - return cast('IPv4Address', parse_ip_address( - id, f'{self.__class__.__name__}: invalid ID', version=4)).packed - def _read_encrypt_auth(self, schema: 'Schema_CryptographicAuthentication') -> 'Data_CryptographicAuthentication': """Read Authentication field when Cryptographic Authentication is employed, i.e. :attr:`~pcapkit.protocols.application.ospf.data.OSPF.autype` is ``2``. @@ -546,3 +499,50 @@ def _make_encrypt_auth(self, except KeyError as exc: raise ProtocolError(f'OSPF: missing {exc} in auth_data: {auth_data!r}') from None raise ProtocolError(f'OSPF: invalid type for auth_data: {auth_data!r}') + + def _read_id_numbers(self, id: 'bytes') -> 'IPv4Address': + """Read router and area IDs. + + Args: + id: ID bytes. + + Returns: + Parsed IDs as an IPv4 address. + + """ + #_byte = self._read_fileng(4) + #_addr = '.'.join(str(_) for _ in _byte) + return ipaddress.ip_address(id) # type: ignore[return-value] + + def _make_id_numbers(self, id: 'IPv4Address | str | bytes | bytearray') -> 'bytes': + """Make router and area IDs. + + Args: + id: ID. + + Returns: + ID bytes. + + Raises: + FieldValueError: If ``id`` is a :obj:`bool` (see + :func:`~pcapkit.corekit.schema.ipaddress.parse_ip_address`). + + Notes: + Latent rather than live: nothing in this module calls this method + (:meth:`make` builds ``router_id``/``area_id`` straight from its + own arguments), so the only caller is a unit test. It is routed + through :func:`~pcapkit.corekit.schema.ipaddress.parse_ip_address` + anyway, so that a future caller passing a :obj:`bool` gets + :exc:`~pcapkit.utilities.exceptions.FieldValueError` rather than a + silent ``0.0.0.1``. + + The description below uses :attr:`self.__class__.__name__ + ` rather than :attr:`self.alias + `, because ``alias`` + here reads ``self._version``, which :meth:`read` only assigns + from the wire -- unavailable to a construction-only instance that + never went through :meth:`read`. + + """ + return cast('IPv4Address', parse_ip_address( + id, f'{self.__class__.__name__}: invalid ID', version=4)).packed diff --git a/pcapkit/protocols/internet/hip/protocol.py b/pcapkit/protocols/internet/hip/protocol.py index 296e566d2f..dbcbcbe6ae 100644 --- a/pcapkit/protocols/internet/hip/protocol.py +++ b/pcapkit/protocols/internet/hip/protocol.py @@ -513,8 +513,8 @@ class HIP(IPv6_Ext[Data_HIP, Schema_HIP], Internet[Data_HIP, Schema_HIP], Enum_Parameter.HIP_MAC_2: 'hip_mac_2', # [RFC 7401] 61569 Enum_Parameter.HIP_SIGNATURE_2: 'hip_signature_2', # [RFC 7401] 61633 Enum_Parameter.HIP_SIGNATURE: 'hip_signature', # [RFC 7401] 61697 - Enum_Parameter.ECHO_REQUEST_UNSIGNED: 'echo_request_unsigned', # [RFC 7401] 63661 Enum_Parameter.ECHO_RESPONSE_UNSIGNED: 'echo_response_unsigned', # [RFC 7401] 63425 + Enum_Parameter.ECHO_REQUEST_UNSIGNED: 'echo_request_unsigned', # [RFC 7401] 63661 Enum_Parameter.RELAY_FROM: 'relay_from', # [RFC 5770] 63998 Enum_Parameter.RELAY_TO: 'relay_to', # [RFC 5770] 64002 Enum_Parameter.OVERLAY_TTL: 'overlay_ttl', # [RFC 6079] 64011 @@ -1325,28 +1325,6 @@ def _read_param_puzzle(self, schema: 'Schema_PuzzleParameter', *, version: 'int' ) return puzzle - @staticmethod - def _read_puzzle_lifetime(octet: 'int') -> 'Optional[timedelta]': - """Decode a ``PUZZLE`` ``Lifetime`` octet into a duration. - - :rfc:`7401#section-5.2.4` gives the lifetime as ``2^(value - 32)`` - seconds. The result is informational only: :meth:`_make_param_puzzle` - writes back the octet itself, since :class:`~datetime.timedelta` rounds - octets below ``12`` to zero and cannot hold those of ``79`` and above. - - Args: - octet: the ``Lifetime`` octet - - Returns: - The lifetime, or :obj:`None` if it exceeds - :attr:`datetime.timedelta.max`. - - """ - try: - return datetime.timedelta(seconds=2 ** (octet - 32)) - except OverflowError: - return None - def _read_param_solution(self, schema: 'Schema_SolutionParameter', *, version: 'int', options: 'Parameter') -> 'Data_SolutionParameter': # pylint: disable=unused-argument """Read HIP ``SOLUTION`` parameter. @@ -2029,116 +2007,6 @@ def _read_param_echo_request_signed(self, schema: 'Schema_EchoRequestSignedParam ) return echo_request_signed - @staticmethod - def _read_registration_lifetime(octet: 'int') -> 'timedelta': - """Decode a registration ``Lifetime`` octet into a duration. - - :rfc:`8003#section-4.1` encodes the registration lifetimes of - ``REG_INFO``, ``REG_REQUEST`` and ``REG_RESPONSE`` as an exponent: the - octet ``0`` is "a special lifetime duration of 0 (zero) seconds", used - to request and grant cancellation, and any other value stands for - ``2^((lifetime - 64)/8)`` seconds. That spans ``2^(-63/8)`` seconds - (about 4 ms) to ``2^(191/8)`` seconds (about 178 days), all of which - :class:`~datetime.timedelta` holds, so -- unlike - :meth:`_read_puzzle_lifetime` -- no octet overflows and the return is - never :obj:`None`. - - Reading the octet as a count of seconds instead reported every duration - wrongly: an octet of ``64``, which is one second, read as 64 seconds. - See :issue:`2281`. - - Args: - octet: the ``Lifetime`` octet - - Returns: - The lifetime. - - """ - if not octet: - return datetime.timedelta(0) - return datetime.timedelta(seconds=2 ** ((octet - 64) / 8)) - - @staticmethod - def _make_registration_lifetime(code: 'Enum_Parameter', version: 'int', - lifetime: 'timedelta | int | float') -> 'int': - """Encode a registration ``Lifetime`` octet. - - The inverse of :meth:`_read_registration_lifetime`, rounding down to the - nearest representable octet as :meth:`_make_puzzle_lifetime` does for - ``PUZZLE``. A duration of zero is the one exact case: - :rfc:`8003#section-4.1` reserves the octet ``0`` for it. - - Args: - code: parameter code - version: HIP protocol version - lifetime: lifetime, as a :class:`~datetime.timedelta` or a number of - seconds - - Returns: - The ``Lifetime`` octet. - - Raises: - ProtocolError: If ``lifetime`` is a :obj:`bool`, is not a - non-negative and finite duration, or encodes to a value the - one-octet field cannot hold. - - """ - # Keyed on :class:`~datetime.timedelta`, not on :class:`int`: testing - # for ``int`` would send a plain ``float`` down the timedelta branch and - # escape an :exc:`AttributeError`. - seconds = lifetime.total_seconds() if isinstance(lifetime, timedelta) else lifetime - - if (isinstance(seconds, bool) or not isinstance(seconds, (int, float)) - or not 0 <= seconds < math.inf): - raise ProtocolError(f'HIPv{version}: [ParamNo {code}] invalid lifetime: ' - f'{seconds!r} is not a non-negative number of seconds') - # Zero is the RFC's own special case, so it is encoded rather than - # rejected -- which is where this parts company with - # ``_make_puzzle_lifetime``, whose field has no such value. Written as a - # truth test rather than ``== 0`` because ``seconds`` may be a float. - if not seconds: - return 0 - - octet = math.floor(math.log2(seconds) * 8 + 64) - - # NOTE: Not _make_uint, so that the message names the seconds given, and - # since the octet 0 is reserved for a zero duration. - if not 1 <= octet <= 0xFF: - raise ProtocolError(f'HIPv{version}: [ParamNo {code}] invalid lifetime: ' - f'{seconds} seconds encodes to {octet}, outside the ' - 'one-octet Lifetime field') - return octet - - def _make_lifetime_octet(self, code: 'Enum_Parameter', version: 'int', - exponent: 'Optional[int]', - lifetime: 'timedelta | int | float') -> 'int': - """Resolve a registration ``Lifetime`` octet from either input form. - - An explicit ``exponent`` is the octet and wins, as a parsed parameter's - is; otherwise ``lifetime`` is a duration and - :meth:`_make_registration_lifetime` encodes it. Shared by the - :rfc:`8003` registration makers, wherever a ``Lifetime`` octet - appears. - - Args: - code: parameter code - version: HIP protocol version - exponent: the ``Lifetime`` octet itself, if any - lifetime: lifetime, as a :class:`~datetime.timedelta` or a number of - seconds - - Returns: - The ``Lifetime`` octet. - - Raises: - ProtocolError: If ``exponent`` does not fit one octet, or - ``lifetime`` cannot be encoded. - - """ - if exponent is None: - return self._make_registration_lifetime(code, version, lifetime) - return self._make_uint(exponent, bits=8, name='lifetime', alias=f'HIPv{version}') - def _read_param_reg_info(self, schema: 'Schema_RegInfoParameter', *, version: 'int', # pylint: disable=unused-argument options: 'Parameter') -> 'Data_RegInfoParameter': # pylint: disable=unused-argument """Read HIP ``REG_INFO`` parameter. @@ -2915,11 +2783,11 @@ def _read_param_hip_signature(self, schema: 'Schema_HIPSignatureParameter', *, v ) return hip_signature - def _read_param_echo_request_unsigned(self, schema: 'Schema_EchoRequestUnsignedParameter', *, version: 'int', # pylint: disable=unused-argument - options: 'Parameter') -> 'Data_EchoRequestUnsignedParameter': # pylint: disable=unused-argument - """Read HIP ``ECHO_REQUEST_UNSIGNED`` parameter. + def _read_param_echo_response_unsigned(self, schema: 'Schema_EchoResponseUnsignedParameter', *, version: 'int', # pylint: disable=unused-argument + options: 'Parameter') -> 'Data_EchoResponseUnsignedParameter': # pylint: disable=unused-argument + """Read HIP ``ECHO_RESPONSE_UNSIGNED`` parameter. - Structure of HIP ``ECHO_REQUEST_UNSIGNED`` parameter [:rfc:`7401`]: + Structure of HIP ``ECHO_RESPONSE_UNSIGNED`` parameter [:rfc:`7401`]: .. code-block:: text @@ -2940,19 +2808,19 @@ def _read_param_echo_request_unsigned(self, schema: 'Schema_EchoRequestUnsignedP Parsed parameter data. """ - echo_request_unsigned = Data_EchoRequestUnsignedParameter( + echo_response_unsigned = Data_EchoResponseUnsignedParameter( type=schema.type, critical=bool(schema.type & 0b1), length=parameter_total_len(schema.len), opaque=schema.opaque, ) - return echo_request_unsigned + return echo_response_unsigned - def _read_param_echo_response_unsigned(self, schema: 'Schema_EchoResponseUnsignedParameter', *, version: 'int', # pylint: disable=unused-argument - options: 'Parameter') -> 'Data_EchoResponseUnsignedParameter': # pylint: disable=unused-argument - """Read HIP ``ECHO_RESPONSE_UNSIGNED`` parameter. + def _read_param_echo_request_unsigned(self, schema: 'Schema_EchoRequestUnsignedParameter', *, version: 'int', # pylint: disable=unused-argument + options: 'Parameter') -> 'Data_EchoRequestUnsignedParameter': # pylint: disable=unused-argument + """Read HIP ``ECHO_REQUEST_UNSIGNED`` parameter. - Structure of HIP ``ECHO_RESPONSE_UNSIGNED`` parameter [:rfc:`7401`]: + Structure of HIP ``ECHO_REQUEST_UNSIGNED`` parameter [:rfc:`7401`]: .. code-block:: text @@ -2973,13 +2841,13 @@ def _read_param_echo_response_unsigned(self, schema: 'Schema_EchoResponseUnsigne Parsed parameter data. """ - echo_response_unsigned = Data_EchoResponseUnsignedParameter( + echo_request_unsigned = Data_EchoRequestUnsignedParameter( type=schema.type, critical=bool(schema.type & 0b1), length=parameter_total_len(schema.len), opaque=schema.opaque, ) - return echo_response_unsigned + return echo_request_unsigned def _read_param_relay_from(self, schema: 'Schema_RelayFromParameter', *, version: 'int', # pylint: disable=unused-argument options: 'Parameter') -> 'Data_RelayFromParameter': # pylint: disable=unused-argument @@ -3343,36 +3211,6 @@ def _read_param_relay_hmac(self, schema: 'Schema_RelayHMACParameter', *, version ) return relay_hmac - def _make_hip_param_data(self, meth: 'ParameterConstructor', code: 'Enum_Parameter', - param: 'Data_Parameter', *, - version: 'int') -> 'Schema_Parameter': - """Make a HIP parameter from the data model its reader produced. - - The parsed parameter goes to ``meth`` **positionally**. Expanding it as - keyword arguments instead would drop every field whose constructor - keyword is spelled differently -- ``HIP_TRANSFORM`` keeps its suites in - ``suite_id`` and :meth:`_make_param_hip_transform` takes them as - ``suites``, so ``**param`` left the parameter empty (:issue:`2352`). - - Args: - meth: Parameter constructor. - code: Parameter code. - param: Parsed parameter data model. - version: HIP protocol version. - - Returns: - HIP parameter schema. - - .. versionadded:: 1.5.0 - - """ - data = meth(code, param, version=version) - # NOTE: The trailing padding :meth:`_read_hip_param` kept, if any. - padding = getattr(param, 'padding', None) - if isinstance(padding, bytes) and 'padding' in data.__fields__: - data.padding = padding - return data - def _make_param_unassigned(self, code: 'Enum_Parameter', param: 'Optional[Data_UnassignedParameter]' = None, *, # pylint: disable=unused-argument version: 'int', contents: 'bytes' = b'', @@ -3634,185 +3472,52 @@ def _make_locator(locator: 'Optional[Data_Locator]' = None, *, locators=locators, ) - @staticmethod - def _make_puzzle_lifetime(code: 'Enum_Parameter', version: 'int', - lifetime: 'timedelta | int | float') -> 'int': - """Encode a ``PUZZLE`` ``Lifetime`` octet. - - :rfc:`7401#section-5.2.4` gives the puzzle lifetime as ``2^(value - 32)`` - seconds -- and the ``PUZZLE`` section is the only place either RFC defines - that encoding (:rfc:`5201#section-5.2.4` identically), so it applies to - ``PUZZLE`` alone. ``SOLUTION``'s octet at the same offset is - ``Reserved`` (:rfc:`7401#section-5.2.5`) and does not come through here. + def _make_param_puzzle(self, code: 'Enum_Parameter', param: 'Optional[Data_PuzzleParameter]' = None, *, # pylint: disable=unused-argument + version: 'int', + index: 'int' = 0, + lifetime: 'timedelta | int' = 0, + lifetime_exponent: 'Optional[int]' = None, + opaque: 'bytes' = b'\x00\x00', + random: 'int' = 0, + rhash_len: 'Optional[int]' = None, + **kwargs: 'Any') -> 'Schema_PuzzleParameter': + """Make HIP ``PUZZLE`` parameter. Args: code: parameter code + param: parameter data version: HIP protocol version - lifetime: lifetime, as a :class:`~datetime.timedelta` or a number of - seconds + index: #K index + lifetime: lifetime, encoded to the nearest lower ``Lifetime`` octet; + ignored when ``lifetime_exponent`` is given + lifetime_exponent: the ``Lifetime`` octet itself, ``value`` in + :rfc:`7401#section-5.2.4`'s ``2^(value - 32)`` seconds + opaque: opaque data + random: random #I value + rhash_len: on-wire width of ``Random #I``, in bits; defaults to the + width ``param`` was parsed with, or to what ``version`` fixes, or + to ``random``'s own bit length + **kwargs: arbitrary keyword arguments Returns: - The ``Lifetime`` octet. + HIP parameter schema. Raises: - ProtocolError: If ``lifetime`` is a :obj:`bool`, is not a positive - and finite duration, or encodes to a value the one-octet field - cannot hold. + ProtocolError: If ``lifetime_exponent`` does not fit one octet, + ``lifetime`` cannot be encoded, or the width of ``Random #I`` is + invalid; see ``_make_puzzle_lifetime`` and + ``_make_puzzle_field_width``. Also if ``index`` does not fit + one octet, or ``random`` is negative or a :obj:`bool`. + Also if ``opaque`` is not two octets. """ - # Keyed on :class:`~datetime.timedelta`, not on :class:`int`: testing - # for ``int`` would send a plain ``float`` down the timedelta branch and - # escape an :exc:`AttributeError`. - seconds = lifetime.total_seconds() if isinstance(lifetime, timedelta) else lifetime - - # ``math.log2`` raises a bare :exc:`ValueError` at zero and below, and - # ``math.floor`` one at infinity or NaN. Neither is a - # :class:`~pcapkit.utilities.exceptions.BaseError`, so it would escape - # the library's error handling with a message naming neither HIP nor - # the field. - if (isinstance(seconds, bool) or not isinstance(seconds, (int, float)) - or not 0 < seconds < math.inf): - raise ProtocolError(f'HIPv{version}: [ParamNo {code}] invalid lifetime: ' - f'{seconds!r} is not a positive number of seconds') - - octet = math.floor(math.log2(seconds) + 32) - - # NOTE: Not _make_uint, so that the message names the seconds given - # rather than only the octet they encode to. - if not 0 <= octet <= 0xFF: - raise ProtocolError(f'HIPv{version}: [ParamNo {code}] invalid lifetime: ' - f'{seconds} seconds encodes to {octet}, outside the ' - 'one-octet Lifetime field') - return octet - - def _make_puzzle_field_width(self, code: 'Enum_Parameter', version: 'int', - rhash_len: 'Optional[int]', *values: 'int') -> 'int': - """Resolve the on-wire width of a ``PUZZLE``/``SOLUTION`` payload field. - - ``Random #I`` and ``Puzzle solution #J`` are each exactly ``RHASH_len / 8`` - octets wide (:rfc:`7401#section-5.2.4`, :rfc:`7401#section-5.2.5`), where - ``RHASH_len`` is the output length of the Responder's HIT hash algorithm - (:rfc:`7401#section-2.3`). The width is therefore a property of the - association, not of the number that happens to be in the field, and it is - resolved here in that order of authority: - - 1. an explicit ``rhash_len`` argument; - 2. under HIPv1, the constant the RFC fixes -- :rfc:`5201#section-5.2.4` and - :rfc:`5201#section-5.2.5` state both fields as literally 8 bytes and both - ``Length`` values as literally 12 and 20; - 3. the width carried by a parsed parameter, passed in as ``rhash_len`` by - the callers below; - 4. failing all of those, the value's own :meth:`int.bit_length`, rounded up - to whole octets. - - Only case 4 can lose a leading zero octet, and it is the only case where the - width is genuinely unknowable -- a from-scratch HIPv2 build with nothing - declaring it. Using it unconditionally would re-serialise a - ``Length = 20`` ``SOLUTION`` as ``Length = 6`` and build, under HIPv1, - parameters this library's own reader rejects. - - Things this deliberately does *not* reject, each of which looks like an - oversight and is not: - - * ``rhash_len == 0``, i.e. a zero-width payload field. No real hash has a - zero-length output, so no conformant packet carries one -- but it is what - case 4 yields for the default ``random=0``, and it is what a - ``Length = 4`` parameter parses back to. Rejecting it would turn a - degenerate-but-self-consistent case into a failure for callers that pass - no value at all. - * a ``version`` that is neither 1 nor 2, which falls through to case 4 and is - treated as HIPv2. That mirrors :meth:`_read_param_puzzle` and - :meth:`_read_param_solution`, whose guards are likewise written as - ``version == 1`` rather than as an exhaustive check, so the reader and the - builder agree. Validating the version belongs with :meth:`make`'s public - signature, not here. - - Args: - code: parameter code - version: HIP protocol version - rhash_len: declared field width in bits, if any - *values: the field values that must fit - - Returns: - The width of one field, in octets. - - Raises: - ProtocolError: If the declared width is not an :obj:`int`, is a - :obj:`bool`, is negative or wider than ``0xFFFF`` octets, - contradicts the protocol version, is not a whole number of - octets, or is too narrow for ``values``. - - """ - # NOTE: A value that is not an int is sized as nothing here, and the - # caller's _make_uint refuses it. - bits = max((value.bit_length() for value in values if isinstance(value, int)), default=0) - - if rhash_len is not None: - # NOTE: In bits, of a field whose octets a two-octet Length counts. - rhash_len = self._make_uint(rhash_len, bits=16 + 3, name='RHASH_len', alias=f'HIPv{version}') - if version == 1: - if rhash_len is not None and rhash_len != 64: - raise ProtocolError(f'HIPv{version}: [ParamNo {code}] invalid width: ' - f'HIPv1 fixes the field at 64 bits, got {rhash_len}') - rhash_len = 64 - elif rhash_len is None: - rhash_len = 8 * math.ceil(bits / 8) - - if rhash_len < 0 or rhash_len % 8: - raise ProtocolError(f'HIPv{version}: [ParamNo {code}] invalid width: ' - f'RHASH_len must be a non-negative whole number of ' - f'octets, got {rhash_len} bits') - if bits > rhash_len: - raise ProtocolError(f'HIPv{version}: [ParamNo {code}] invalid width: ' - f'a {bits}-bit value does not fit a {rhash_len}-bit field') - return rhash_len // 8 - - def _make_param_puzzle(self, code: 'Enum_Parameter', param: 'Optional[Data_PuzzleParameter]' = None, *, # pylint: disable=unused-argument - version: 'int', - index: 'int' = 0, - lifetime: 'timedelta | int' = 0, - lifetime_exponent: 'Optional[int]' = None, - opaque: 'bytes' = b'\x00\x00', - random: 'int' = 0, - rhash_len: 'Optional[int]' = None, - **kwargs: 'Any') -> 'Schema_PuzzleParameter': - """Make HIP ``PUZZLE`` parameter. - - Args: - code: parameter code - param: parameter data - version: HIP protocol version - index: #K index - lifetime: lifetime, encoded to the nearest lower ``Lifetime`` octet; - ignored when ``lifetime_exponent`` is given - lifetime_exponent: the ``Lifetime`` octet itself, ``value`` in - :rfc:`7401#section-5.2.4`'s ``2^(value - 32)`` seconds - opaque: opaque data - random: random #I value - rhash_len: on-wire width of ``Random #I``, in bits; defaults to the - width ``param`` was parsed with, or to what ``version`` fixes, or - to ``random``'s own bit length - **kwargs: arbitrary keyword arguments - - Returns: - HIP parameter schema. - - Raises: - ProtocolError: If ``lifetime_exponent`` does not fit one octet, - ``lifetime`` cannot be encoded, or the width of ``Random #I`` is - invalid; see ``_make_puzzle_lifetime`` and - ``_make_puzzle_field_width``. Also if ``index`` does not fit - one octet, or ``random`` is negative or a :obj:`bool`. - Also if ``opaque`` is not two octets. - - """ - if param is not None: - index = param.index - lifetime_exponent = param.lifetime_exponent - opaque = param.opaque - random = param.random - if rhash_len is None: - rhash_len = param.rhash_len + if param is not None: + index = param.index + lifetime_exponent = param.lifetime_exponent + opaque = param.opaque + random = param.random + if rhash_len is None: + rhash_len = param.rhash_len if lifetime_exponent is None: lifetime_exponent = self._make_puzzle_lifetime(code, version, lifetime) @@ -5034,61 +4739,6 @@ def _make_param_payload_mic(self, code: 'Enum_Parameter', param: 'Optional[Data_ mic=mic, ) - def _make_identifier_width(self, code: 'Enum_Parameter', version: 'int', - id_len: 'Optional[int]', identifier: 'int') -> 'int': - """Resolve the on-wire width of an opaque ``Identifier`` field. - - ``TRANSACTION_ID`` (:rfc:`6078#section-4.4`) and ``OVERLAY_ID`` - (:rfc:`6079#section-6.1`) both spell their ``Length`` as the "Length of - the Identifier, in octets" over an otherwise opaque value, so the width - is a property of the identifier as issued and not of the number that - happens to be in it. It is resolved here in that order of authority: - - 1. an explicit ``id_len`` argument, which is what a parsed parameter - carries on :attr:`TransactionIDParameter.id_len - `; - 2. failing that, the identifier's own :meth:`int.bit_length`, rounded up - to whole octets. - - Only case 2 can lose a leading zero octet, and it is the only case - where the width is genuinely unknowable -- a from-scratch build with - nothing declaring it. Using it unconditionally re-serialised a - ``Length = 4`` identifier of ``1`` as ``Length = 1`` (:issue:`2271`), - the same way :meth:`_make_puzzle_field_width` guards ``Random #I`` - against :issue:`653`. - - A width of ``0`` is accepted rather than rejected: it is what case 2 - yields for the default identifier of ``0``, and what a ``Length = 0`` - parameter parses back to, so refusing it would fail callers that pass no - value at all. - - Args: - code: parameter code - version: HIP protocol version - id_len: declared field width in octets, if any - identifier: the identifier that must fit - - Returns: - The width of the ``Identifier`` field, in octets. - - Raises: - ProtocolError: If ``identifier`` is negative or needs more octets - than the 2-octet ``Length`` field can count, or the declared - width is negative, too narrow for ``identifier``, or beyond - what that field can hold. - - """ - identifier = self._make_uint(identifier, bits=0xFFFF * 8, name='identifier', - alias=f'HIPv{version}') - if id_len is None: - return self._uint_size(identifier) - - id_len = self._make_uint(id_len, bits=16, name='identifier width', alias=f'HIPv{version}') - if identifier.bit_length() > id_len * 8: - raise ProtocolError(f'HIPv{version}: [ParamNo {code}] invalid identifier width: ' - f'{id_len} octets cannot hold {identifier!r}') - return id_len - def _make_param_transaction_id(self, code: 'Enum_Parameter', param: 'Optional[Data_TransactionIDParameter]' = None, *, # pylint: disable=unused-argument version: 'int', id: 'int' = 0, @@ -5382,11 +5032,11 @@ def _make_param_hip_signature(self, code: 'Enum_Parameter', param: 'Optional[Dat signature=signature, ) - def _make_param_echo_request_unsigned(self, code: 'Enum_Parameter', param: 'Optional[Data_EchoRequestUnsignedParameter]' = None, *, # pylint: disable=unused-argument - version: 'int', - opaque: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_EchoRequestUnsignedParameter': - """Make HIP ``ECHO_REQUEST_UNSIGNED`` parameter. + def _make_param_echo_response_unsigned(self, code: 'Enum_Parameter', param: 'Optional[Data_EchoResponseUnsignedParameter]' = None, *, # pylint: disable=unused-argument + version: 'int', + opaque: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_EchoResponseUnsignedParameter': + """Make HIP ``ECHO_RESPONSE_UNSIGNED`` parameter. Args: code: parameter code @@ -5402,17 +5052,17 @@ def _make_param_echo_request_unsigned(self, code: 'Enum_Parameter', param: 'Opti if param is not None: opaque = param.opaque - return Schema_EchoRequestUnsignedParameter( + return Schema_EchoResponseUnsignedParameter( type=code, len=len(opaque), opaque=opaque, ) - def _make_param_echo_response_unsigned(self, code: 'Enum_Parameter', param: 'Optional[Data_EchoResponseUnsignedParameter]' = None, *, # pylint: disable=unused-argument - version: 'int', - opaque: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_EchoResponseUnsignedParameter': - """Make HIP ``ECHO_RESPONSE_UNSIGNED`` parameter. + def _make_param_echo_request_unsigned(self, code: 'Enum_Parameter', param: 'Optional[Data_EchoRequestUnsignedParameter]' = None, *, # pylint: disable=unused-argument + version: 'int', + opaque: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_EchoRequestUnsignedParameter': + """Make HIP ``ECHO_REQUEST_UNSIGNED`` parameter. Args: code: parameter code @@ -5428,7 +5078,7 @@ def _make_param_echo_response_unsigned(self, code: 'Enum_Parameter', param: 'Opt if param is not None: opaque = param.opaque - return Schema_EchoResponseUnsignedParameter( + return Schema_EchoRequestUnsignedParameter( type=code, len=len(opaque), opaque=opaque, @@ -5729,3 +5379,353 @@ def _make_param_relay_hmac(self, code: 'Enum_Parameter', param: 'Optional[Data_R len=len(hmac), hmac=hmac, ) + + @staticmethod + def _read_puzzle_lifetime(octet: 'int') -> 'Optional[timedelta]': + """Decode a ``PUZZLE`` ``Lifetime`` octet into a duration. + + :rfc:`7401#section-5.2.4` gives the lifetime as ``2^(value - 32)`` + seconds. The result is informational only: :meth:`_make_param_puzzle` + writes back the octet itself, since :class:`~datetime.timedelta` rounds + octets below ``12`` to zero and cannot hold those of ``79`` and above. + + Args: + octet: the ``Lifetime`` octet + + Returns: + The lifetime, or :obj:`None` if it exceeds + :attr:`datetime.timedelta.max`. + + """ + try: + return datetime.timedelta(seconds=2 ** (octet - 32)) + except OverflowError: + return None + + @staticmethod + def _read_registration_lifetime(octet: 'int') -> 'timedelta': + """Decode a registration ``Lifetime`` octet into a duration. + + :rfc:`8003#section-4.1` encodes the registration lifetimes of + ``REG_INFO``, ``REG_REQUEST`` and ``REG_RESPONSE`` as an exponent: the + octet ``0`` is "a special lifetime duration of 0 (zero) seconds", used + to request and grant cancellation, and any other value stands for + ``2^((lifetime - 64)/8)`` seconds. That spans ``2^(-63/8)`` seconds + (about 4 ms) to ``2^(191/8)`` seconds (about 178 days), all of which + :class:`~datetime.timedelta` holds, so -- unlike + :meth:`_read_puzzle_lifetime` -- no octet overflows and the return is + never :obj:`None`. + + Reading the octet as a count of seconds instead reported every duration + wrongly: an octet of ``64``, which is one second, read as 64 seconds. + See :issue:`2281`. + + Args: + octet: the ``Lifetime`` octet + + Returns: + The lifetime. + + """ + if not octet: + return datetime.timedelta(0) + return datetime.timedelta(seconds=2 ** ((octet - 64) / 8)) + + @staticmethod + def _make_registration_lifetime(code: 'Enum_Parameter', version: 'int', + lifetime: 'timedelta | int | float') -> 'int': + """Encode a registration ``Lifetime`` octet. + + The inverse of :meth:`_read_registration_lifetime`, rounding down to the + nearest representable octet as :meth:`_make_puzzle_lifetime` does for + ``PUZZLE``. A duration of zero is the one exact case: + :rfc:`8003#section-4.1` reserves the octet ``0`` for it. + + Args: + code: parameter code + version: HIP protocol version + lifetime: lifetime, as a :class:`~datetime.timedelta` or a number of + seconds + + Returns: + The ``Lifetime`` octet. + + Raises: + ProtocolError: If ``lifetime`` is a :obj:`bool`, is not a + non-negative and finite duration, or encodes to a value the + one-octet field cannot hold. + + """ + # Keyed on :class:`~datetime.timedelta`, not on :class:`int`: testing + # for ``int`` would send a plain ``float`` down the timedelta branch and + # escape an :exc:`AttributeError`. + seconds = lifetime.total_seconds() if isinstance(lifetime, timedelta) else lifetime + + if (isinstance(seconds, bool) or not isinstance(seconds, (int, float)) + or not 0 <= seconds < math.inf): + raise ProtocolError(f'HIPv{version}: [ParamNo {code}] invalid lifetime: ' + f'{seconds!r} is not a non-negative number of seconds') + # Zero is the RFC's own special case, so it is encoded rather than + # rejected -- which is where this parts company with + # ``_make_puzzle_lifetime``, whose field has no such value. Written as a + # truth test rather than ``== 0`` because ``seconds`` may be a float. + if not seconds: + return 0 + + octet = math.floor(math.log2(seconds) * 8 + 64) + + # NOTE: Not _make_uint, so that the message names the seconds given, and + # since the octet 0 is reserved for a zero duration. + if not 1 <= octet <= 0xFF: + raise ProtocolError(f'HIPv{version}: [ParamNo {code}] invalid lifetime: ' + f'{seconds} seconds encodes to {octet}, outside the ' + 'one-octet Lifetime field') + return octet + + def _make_lifetime_octet(self, code: 'Enum_Parameter', version: 'int', + exponent: 'Optional[int]', + lifetime: 'timedelta | int | float') -> 'int': + """Resolve a registration ``Lifetime`` octet from either input form. + + An explicit ``exponent`` is the octet and wins, as a parsed parameter's + is; otherwise ``lifetime`` is a duration and + :meth:`_make_registration_lifetime` encodes it. Shared by the + :rfc:`8003` registration makers, wherever a ``Lifetime`` octet + appears. + + Args: + code: parameter code + version: HIP protocol version + exponent: the ``Lifetime`` octet itself, if any + lifetime: lifetime, as a :class:`~datetime.timedelta` or a number of + seconds + + Returns: + The ``Lifetime`` octet. + + Raises: + ProtocolError: If ``exponent`` does not fit one octet, or + ``lifetime`` cannot be encoded. + + """ + if exponent is None: + return self._make_registration_lifetime(code, version, lifetime) + return self._make_uint(exponent, bits=8, name='lifetime', alias=f'HIPv{version}') + + def _make_hip_param_data(self, meth: 'ParameterConstructor', code: 'Enum_Parameter', + param: 'Data_Parameter', *, + version: 'int') -> 'Schema_Parameter': + """Make a HIP parameter from the data model its reader produced. + + The parsed parameter goes to ``meth`` **positionally**. Expanding it as + keyword arguments instead would drop every field whose constructor + keyword is spelled differently -- ``HIP_TRANSFORM`` keeps its suites in + ``suite_id`` and :meth:`_make_param_hip_transform` takes them as + ``suites``, so ``**param`` left the parameter empty (:issue:`2352`). + + Args: + meth: Parameter constructor. + code: Parameter code. + param: Parsed parameter data model. + version: HIP protocol version. + + Returns: + HIP parameter schema. + + .. versionadded:: 1.5.0 + + """ + data = meth(code, param, version=version) + # NOTE: The trailing padding :meth:`_read_hip_param` kept, if any. + padding = getattr(param, 'padding', None) + if isinstance(padding, bytes) and 'padding' in data.__fields__: + data.padding = padding + return data + + @staticmethod + def _make_puzzle_lifetime(code: 'Enum_Parameter', version: 'int', + lifetime: 'timedelta | int | float') -> 'int': + """Encode a ``PUZZLE`` ``Lifetime`` octet. + + :rfc:`7401#section-5.2.4` gives the puzzle lifetime as ``2^(value - 32)`` + seconds -- and the ``PUZZLE`` section is the only place either RFC defines + that encoding (:rfc:`5201#section-5.2.4` identically), so it applies to + ``PUZZLE`` alone. ``SOLUTION``'s octet at the same offset is + ``Reserved`` (:rfc:`7401#section-5.2.5`) and does not come through here. + + Args: + code: parameter code + version: HIP protocol version + lifetime: lifetime, as a :class:`~datetime.timedelta` or a number of + seconds + + Returns: + The ``Lifetime`` octet. + + Raises: + ProtocolError: If ``lifetime`` is a :obj:`bool`, is not a positive + and finite duration, or encodes to a value the one-octet field + cannot hold. + + """ + # Keyed on :class:`~datetime.timedelta`, not on :class:`int`: testing + # for ``int`` would send a plain ``float`` down the timedelta branch and + # escape an :exc:`AttributeError`. + seconds = lifetime.total_seconds() if isinstance(lifetime, timedelta) else lifetime + + # ``math.log2`` raises a bare :exc:`ValueError` at zero and below, and + # ``math.floor`` one at infinity or NaN. Neither is a + # :class:`~pcapkit.utilities.exceptions.BaseError`, so it would escape + # the library's error handling with a message naming neither HIP nor + # the field. + if (isinstance(seconds, bool) or not isinstance(seconds, (int, float)) + or not 0 < seconds < math.inf): + raise ProtocolError(f'HIPv{version}: [ParamNo {code}] invalid lifetime: ' + f'{seconds!r} is not a positive number of seconds') + + octet = math.floor(math.log2(seconds) + 32) + + # NOTE: Not _make_uint, so that the message names the seconds given + # rather than only the octet they encode to. + if not 0 <= octet <= 0xFF: + raise ProtocolError(f'HIPv{version}: [ParamNo {code}] invalid lifetime: ' + f'{seconds} seconds encodes to {octet}, outside the ' + 'one-octet Lifetime field') + return octet + + def _make_puzzle_field_width(self, code: 'Enum_Parameter', version: 'int', + rhash_len: 'Optional[int]', *values: 'int') -> 'int': + """Resolve the on-wire width of a ``PUZZLE``/``SOLUTION`` payload field. + + ``Random #I`` and ``Puzzle solution #J`` are each exactly ``RHASH_len / 8`` + octets wide (:rfc:`7401#section-5.2.4`, :rfc:`7401#section-5.2.5`), where + ``RHASH_len`` is the output length of the Responder's HIT hash algorithm + (:rfc:`7401#section-2.3`). The width is therefore a property of the + association, not of the number that happens to be in the field, and it is + resolved here in that order of authority: + + 1. an explicit ``rhash_len`` argument; + 2. under HIPv1, the constant the RFC fixes -- :rfc:`5201#section-5.2.4` and + :rfc:`5201#section-5.2.5` state both fields as literally 8 bytes and both + ``Length`` values as literally 12 and 20; + 3. the width carried by a parsed parameter, passed in as ``rhash_len`` by + the callers below; + 4. failing all of those, the value's own :meth:`int.bit_length`, rounded up + to whole octets. + + Only case 4 can lose a leading zero octet, and it is the only case where the + width is genuinely unknowable -- a from-scratch HIPv2 build with nothing + declaring it. Using it unconditionally would re-serialise a + ``Length = 20`` ``SOLUTION`` as ``Length = 6`` and build, under HIPv1, + parameters this library's own reader rejects. + + Things this deliberately does *not* reject, each of which looks like an + oversight and is not: + + * ``rhash_len == 0``, i.e. a zero-width payload field. No real hash has a + zero-length output, so no conformant packet carries one -- but it is what + case 4 yields for the default ``random=0``, and it is what a + ``Length = 4`` parameter parses back to. Rejecting it would turn a + degenerate-but-self-consistent case into a failure for callers that pass + no value at all. + * a ``version`` that is neither 1 nor 2, which falls through to case 4 and is + treated as HIPv2. That mirrors :meth:`_read_param_puzzle` and + :meth:`_read_param_solution`, whose guards are likewise written as + ``version == 1`` rather than as an exhaustive check, so the reader and the + builder agree. Validating the version belongs with :meth:`make`'s public + signature, not here. + + Args: + code: parameter code + version: HIP protocol version + rhash_len: declared field width in bits, if any + *values: the field values that must fit + + Returns: + The width of one field, in octets. + + Raises: + ProtocolError: If the declared width is not an :obj:`int`, is a + :obj:`bool`, is negative or wider than ``0xFFFF`` octets, + contradicts the protocol version, is not a whole number of + octets, or is too narrow for ``values``. + + """ + # NOTE: A value that is not an int is sized as nothing here, and the + # caller's _make_uint refuses it. + bits = max((value.bit_length() for value in values if isinstance(value, int)), default=0) + + if rhash_len is not None: + # NOTE: In bits, of a field whose octets a two-octet Length counts. + rhash_len = self._make_uint(rhash_len, bits=16 + 3, name='RHASH_len', alias=f'HIPv{version}') + if version == 1: + if rhash_len is not None and rhash_len != 64: + raise ProtocolError(f'HIPv{version}: [ParamNo {code}] invalid width: ' + f'HIPv1 fixes the field at 64 bits, got {rhash_len}') + rhash_len = 64 + elif rhash_len is None: + rhash_len = 8 * math.ceil(bits / 8) + + if rhash_len < 0 or rhash_len % 8: + raise ProtocolError(f'HIPv{version}: [ParamNo {code}] invalid width: ' + f'RHASH_len must be a non-negative whole number of ' + f'octets, got {rhash_len} bits') + if bits > rhash_len: + raise ProtocolError(f'HIPv{version}: [ParamNo {code}] invalid width: ' + f'a {bits}-bit value does not fit a {rhash_len}-bit field') + return rhash_len // 8 + + def _make_identifier_width(self, code: 'Enum_Parameter', version: 'int', + id_len: 'Optional[int]', identifier: 'int') -> 'int': + """Resolve the on-wire width of an opaque ``Identifier`` field. + + ``TRANSACTION_ID`` (:rfc:`6078#section-4.4`) and ``OVERLAY_ID`` + (:rfc:`6079#section-6.1`) both spell their ``Length`` as the "Length of + the Identifier, in octets" over an otherwise opaque value, so the width + is a property of the identifier as issued and not of the number that + happens to be in it. It is resolved here in that order of authority: + + 1. an explicit ``id_len`` argument, which is what a parsed parameter + carries on :attr:`TransactionIDParameter.id_len + `; + 2. failing that, the identifier's own :meth:`int.bit_length`, rounded up + to whole octets. + + Only case 2 can lose a leading zero octet, and it is the only case + where the width is genuinely unknowable -- a from-scratch build with + nothing declaring it. Using it unconditionally re-serialised a + ``Length = 4`` identifier of ``1`` as ``Length = 1`` (:issue:`2271`), + the same way :meth:`_make_puzzle_field_width` guards ``Random #I`` + against :issue:`653`. + + A width of ``0`` is accepted rather than rejected: it is what case 2 + yields for the default identifier of ``0``, and what a ``Length = 0`` + parameter parses back to, so refusing it would fail callers that pass no + value at all. + + Args: + code: parameter code + version: HIP protocol version + id_len: declared field width in octets, if any + identifier: the identifier that must fit + + Returns: + The width of the ``Identifier`` field, in octets. + + Raises: + ProtocolError: If ``identifier`` is negative or needs more octets + than the 2-octet ``Length`` field can count, or the declared + width is negative, too narrow for ``identifier``, or beyond + what that field can hold. + + """ + identifier = self._make_uint(identifier, bits=0xFFFF * 8, name='identifier', + alias=f'HIPv{version}') + if id_len is None: + return self._uint_size(identifier) + + id_len = self._make_uint(id_len, bits=16, name='identifier width', alias=f'HIPv{version}') + if identifier.bit_length() > id_len * 8: + raise ProtocolError(f'HIPv{version}: [ParamNo {code}] invalid identifier width: ' + f'{id_len} octets cannot hold {identifier!r}') + return id_len diff --git a/pcapkit/protocols/internet/hopopt/protocol.py b/pcapkit/protocols/internet/hopopt/protocol.py index 035ad80b46..112b71652e 100644 --- a/pcapkit/protocols/internet/hopopt/protocol.py +++ b/pcapkit/protocols/internet/hopopt/protocol.py @@ -268,8 +268,8 @@ class HOPOPT(IPv6_Ext[Data_HOPOPT, Schema_HOPOPT], Enum_Option.CALIPSO: 'calipso', # [RFC 5570] Enum_Option.SMF_DPD: 'smf_dpd', # [RFC 6621] Enum_Option.PDM: 'pdm', # [RFC 8250] 10 - Enum_Option.Quick_Start: 'qs', # [RFC 4782][RFC Errata 2034] 6 Enum_Option.RPL_Option_0x23: 'rpl', # [RFC 9008] + Enum_Option.Quick_Start: 'qs', # [RFC 4782][RFC Errata 2034] 6 Enum_Option.RPL_Option_0x63: 'rpl', # [RFC 6553] Enum_Option.MPL_Option: 'mpl', # [RFC 7731] Enum_Option.ILNP_Nonce: 'ilnp', # [RFC 6744] @@ -567,60 +567,6 @@ def _make_data(cls, data: 'Data_HOPOPT') -> 'dict[str, Any]': # type: ignore[ov 'len': data.length // 8 - 1 if data.get('__short_read__') is not None else None, } - def _read_opt_type(self, kind: 'int') -> 'tuple[int, bool]': - """Read option type field. - - Arguments: - kind (int): option kind value - - Returns: - Extracted HOPOPT option type field information (unknown option - action and change flag), see [:rfc:`8200#section-4.2`]. - - """ - bin_ = bin(kind)[2:].zfill(8) - return int(bin_[:2], base=2), bool(int(bin_[2], base=2)) - - @staticmethod - def _hopopt_option_length(schema_len: 'int') -> 'int': - """Compute a HOPOPT option's whole-option length from its on-the-wire ``Opt Data Len``. - - Per :rfc:`8200#section-4.2`, an option's ``Opt Data Len`` field - (what each ``Schema_*Option.len`` here holds) counts *"the length of - the Option Data field of this option, in octets"* -- i.e. it - excludes the Option Type and Opt Data Len fields themselves, so the - whole option, which is what every ``_read_opt_*`` below reports back - as the parsed option's own ``.length``, is two octets more. This is - the ``+2``/``-2`` mismatch between the two (see ``Data_PadOption.length`` - vs. ``Schema_PadOption.len`` below). The read-side half lives in this - one helper so the arithmetic has a single home. Do NOT drop the - ``+ 2``. - - Section 4.2 is the citation because it defines the TLV option format and - contains the sentence quoted above. The Hop-by-Hop Options header this - class implements is :rfc:`8200#section-4.3`, but it defers option - encoding to Section 4.2 (*"one or more TLV-encoded options, as - described in Section 4.2"*) and its only length field is - ``Hdr Ext Len``, *"the length of the Hop-by-Hop Options header in - 8-octet units, not including the first 8 octets"*: the whole header, a - different quantity from one option's ``Opt Data Len`` in octets. - Section 4.3 remains the reference for the header itself, as in the - ``Note:`` on the whole extension header being a multiple of 8 octets. - - Note that only the *stored-length* read-side call sites are - collected here -- most ``_make_opt_*`` methods recompute the wire - ``Opt Data Len`` from ``len(value)`` rather than reading a parsed - ``.length`` back, so they have nothing to unify against this helper. - - Args: - schema_len: raw ``Opt Data Len`` field value, as read off the wire. - - Returns: - Whole-option length, in octets, including the Type and Opt Data Len fields. - - """ - return schema_len + 2 - def _read_hopopt_options(self, length: 'int') -> 'Option': """Read HOPOPT options. @@ -1139,6 +1085,57 @@ def _read_opt_pdm(self, schema: 'Schema_PDMOption', *, options: 'Option') -> 'Da ) return opt + def _read_opt_rpl(self, schema: 'Schema_RPLOption', *, options: 'Option') -> 'Data_RPLOption': # pylint: disable=unused-argument + """Read HOPOPT Routing Protocol for Low-Power and Lossy Networks (RPL) option. + + Structure of HOPOPT RPL option [:rfc:`6553`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Opt Data Len | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |O|R|F|0|0|0|0|0| RPLInstanceID | SenderRank | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | (sub-TLVs) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + The sub-TLVs, which :rfc:`6553#section-3` says MUST be skipped when + unrecognised, are kept as raw octets in ``sub_tlvs``. + + Args: + schema: parsed parameter schema + options: extracted HOPOPT options + + Returns: + Parsed option data. + + Raises: + ProtocolError: If ``schema.len`` is less than ``4``. + + """ + if schema.len < 4: + raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format') + + opt = Data_RPLOption( + type=schema.type, + action=Enum_OptionAction.get(schema.type >> 6), + change=bool(schema.type & 0b00100000), + length=self._hopopt_option_length(schema.len), + flags=Data_RPLFlags( + down=bool(schema.flags['down']), + rank_err=bool(schema.flags['rank_err']), + fwd_err=bool(schema.flags['fwd_err']), + reserved=schema.flags['reserved'], + ), + id=schema.id, + rank=schema.rank, + sub_tlvs=schema.sub_tlvs, + ) + return opt + def _read_opt_qs(self, schema: 'Schema_QuickStartOption', *, options: 'Option') -> 'Data_QuickStartOption': # pylint: disable=unused-argument """Read HOPOPT Quick Start option. @@ -1230,57 +1227,6 @@ def _read_opt_qs(self, schema: 'Schema_QuickStartOption', *, options: 'Option') raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] unknown QS function: {func}') return opt - def _read_opt_rpl(self, schema: 'Schema_RPLOption', *, options: 'Option') -> 'Data_RPLOption': # pylint: disable=unused-argument - """Read HOPOPT Routing Protocol for Low-Power and Lossy Networks (RPL) option. - - Structure of HOPOPT RPL option [:rfc:`6553`]: - - .. code-block:: text - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option Type | Opt Data Len | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |O|R|F|0|0|0|0|0| RPLInstanceID | SenderRank | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | (sub-TLVs) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - The sub-TLVs, which :rfc:`6553#section-3` says MUST be skipped when - unrecognised, are kept as raw octets in ``sub_tlvs``. - - Args: - schema: parsed parameter schema - options: extracted HOPOPT options - - Returns: - Parsed option data. - - Raises: - ProtocolError: If ``schema.len`` is less than ``4``. - - """ - if schema.len < 4: - raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format') - - opt = Data_RPLOption( - type=schema.type, - action=Enum_OptionAction.get(schema.type >> 6), - change=bool(schema.type & 0b00100000), - length=self._hopopt_option_length(schema.len), - flags=Data_RPLFlags( - down=bool(schema.flags['down']), - rank_err=bool(schema.flags['rank_err']), - fwd_err=bool(schema.flags['fwd_err']), - reserved=schema.flags['reserved'], - ), - id=schema.id, - rank=schema.rank, - sub_tlvs=schema.sub_tlvs, - ) - return opt - def _read_opt_mpl(self, schema: 'Schema_MPLOption', *, options: 'Option') -> 'Data_MPLOption': # pylint: disable=unused-argument """Read HOPOPT Multicast Protocol for Low-Power and Lossy Networks (MPL) option. @@ -1545,45 +1491,6 @@ def _read_opt_ip_dff(self, schema: 'Schema_IPDFFOption', *, options: 'Option') - ) return opt - def _make_pad_options(self, offset: 'int') -> 'tuple[list[Schema_PadOption], int]': - """Make the padding options needed to align the header to 8 octets. - - Args: - offset: Number of octets emitted so far, **counting the two octets - of the fixed header** (``Next Header`` and ``Hdr Ext Len``) as - well as every option already in the list. - - Returns: - Tuple of the padding option schemas and the number of octets they - occupy. - - Note: - It is the *whole* extension header, fixed part included, that has to - be a multiple of 8 octets [:rfc:`8200#section-4.3`], which is why - the alignment is computed from ``offset`` rather than from the - length of the option just emitted. Aligning each option to 8 octets - on its own leaves the options area a multiple of 8, whereas - :meth:`read` sizes it as ``hdr_ext_len * 8 + 6``, two short of a - multiple of 8; the header would declare 2 octets fewer than it - carried. - - A ``PadN`` option spends two octets on its own type and - ``Opt Data Len`` fields before any padding data, so occupying - ``pad_len`` octets means an ``Opt Data Len`` of ``pad_len - 2``. - One octet is padded with a ``Pad1`` option instead, that being the - only form which can occupy a single octet. Two octets are one - ``PadN`` with an ``Opt Data Len`` of ``0``, not two ``Pad1``: - :rfc:`8200#section-4.2` says a ``PadN`` should be used rather than - multiple ``Pad1`` options. - - """ - pad_len = pad_length(offset, 8) - if pad_len == 0: - return [], 0 - if pad_len == 1: - return [self._make_opt_pad(Enum_Option.Pad1, length=0)], pad_len # type: ignore[arg-type] - return [self._make_opt_pad(Enum_Option.PadN, length=pad_len - 2)], pad_len # type: ignore[arg-type] - def _make_opt_none(self, code: 'Enum_Option', opt: 'Optional[Data_UnassignedOption]' = None, *, data: 'bytes' = b'', **kwargs: 'Any') -> 'Schema_UnassignedOption': @@ -1971,6 +1878,66 @@ def _make_opt_pdm(self, code: 'Enum_Option', opt: 'Optional[Data_PDMOption]' = N name='PDM delta time last sent'), ) + def _make_opt_rpl(self, code: 'Enum_Option', opt: 'Optional[Data_RPLOption]' = None, *, + down: 'bool' = False, + rank_err: 'bool' = False, + fwd_err: 'bool' = False, + id: 'int' = 0, + rank: 'int' = 0, + reserved: 'int' = 0, + sub_tlvs: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_RPLOption': + """Make HOPOPT RPL option. + + Args: + code: option type value + opt: option data + down: down flag + rank_err: rank error flag + fwd_err: forwarding error flag + id: RPL instance ID + rank: sender rank + reserved: reserved flag bits, i.e. the low five bits of the flags octet + sub_tlvs: sub-TLVs, as raw octets + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + Raises: + ProtocolError: If ``reserved``, ``id`` or ``rank`` does not fit its + field, or ``sub_tlvs`` does not fit in the option. + + """ + if opt is not None: + down = opt.flags.down + rank_err = opt.flags.rank_err + fwd_err = opt.flags.fwd_err + reserved = opt.flags.reserved + id = opt.id + rank = opt.rank + sub_tlvs = opt.sub_tlvs + + reserved = self._make_uint(reserved, bits=5, name='RPL reserved flags') + id = self._make_uint(id, bits=8, name='RPL instance ID') + rank = self._make_uint(rank, bits=16, name='RPL sender rank') + if len(sub_tlvs) > 251: + raise ProtocolError(f'{self.alias}: [OptNo {code}] too long RPL sub-TLVs: {len(sub_tlvs)}') + + return Schema_RPLOption( + type=code, + len=4 + len(sub_tlvs), + flags={ + 'down': down, + 'rank_err': rank_err, + 'fwd_err': fwd_err, + 'reserved': reserved, + }, + id=id, + rank=rank, + sub_tlvs=sub_tlvs, + ) + def _make_opt_qs(self, code: 'Enum_Option', opt: 'Optional[Data_QuickStartOption | Data_UnassignedOption]' = None, *, # pylint: disable=line-too-long func: 'Enum_QSFunction | int' = Enum_QSFunction.Quick_Start_Request, rate: 'int' = 0, @@ -2055,66 +2022,6 @@ def _make_opt_qs(self, code: 'Enum_Option', opt: 'Optional[Data_QuickStartOption data = bytes([(func_val << 4) | rate_val, ttl_value]) + ((nonce << 2) | reserved).to_bytes(4, 'big') return self._make_opt_none(code, data=data) - def _make_opt_rpl(self, code: 'Enum_Option', opt: 'Optional[Data_RPLOption]' = None, *, - down: 'bool' = False, - rank_err: 'bool' = False, - fwd_err: 'bool' = False, - id: 'int' = 0, - rank: 'int' = 0, - reserved: 'int' = 0, - sub_tlvs: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_RPLOption': - """Make HOPOPT RPL option. - - Args: - code: option type value - opt: option data - down: down flag - rank_err: rank error flag - fwd_err: forwarding error flag - id: RPL instance ID - rank: sender rank - reserved: reserved flag bits, i.e. the low five bits of the flags octet - sub_tlvs: sub-TLVs, as raw octets - **kwargs: arbitrary keyword arguments - - Returns: - Constructed option schema. - - Raises: - ProtocolError: If ``reserved``, ``id`` or ``rank`` does not fit its - field, or ``sub_tlvs`` does not fit in the option. - - """ - if opt is not None: - down = opt.flags.down - rank_err = opt.flags.rank_err - fwd_err = opt.flags.fwd_err - reserved = opt.flags.reserved - id = opt.id - rank = opt.rank - sub_tlvs = opt.sub_tlvs - - reserved = self._make_uint(reserved, bits=5, name='RPL reserved flags') - id = self._make_uint(id, bits=8, name='RPL instance ID') - rank = self._make_uint(rank, bits=16, name='RPL sender rank') - if len(sub_tlvs) > 251: - raise ProtocolError(f'{self.alias}: [OptNo {code}] too long RPL sub-TLVs: {len(sub_tlvs)}') - - return Schema_RPLOption( - type=code, - len=4 + len(sub_tlvs), - flags={ - 'down': down, - 'rank_err': rank_err, - 'fwd_err': fwd_err, - 'reserved': reserved, - }, - id=id, - rank=rank, - sub_tlvs=sub_tlvs, - ) - def _make_opt_mpl(self, code: 'Enum_Option', opt: 'Optional[Data_MPLOption]' = None, *, seed_type: 'Optional[Enum_SeedID | int]' = None, max: 'bool' = False, @@ -2367,3 +2274,96 @@ def _make_opt_ip_dff(self, code: 'Enum_Option', opt: 'Optional[Data_IPDFFOption] }, seq=self._make_uint(seq, bits=16, name='DFF sequence number'), ) + + def _read_opt_type(self, kind: 'int') -> 'tuple[int, bool]': + """Read option type field. + + Arguments: + kind (int): option kind value + + Returns: + Extracted HOPOPT option type field information (unknown option + action and change flag), see [:rfc:`8200#section-4.2`]. + + """ + bin_ = bin(kind)[2:].zfill(8) + return int(bin_[:2], base=2), bool(int(bin_[2], base=2)) + + @staticmethod + def _hopopt_option_length(schema_len: 'int') -> 'int': + """Compute a HOPOPT option's whole-option length from its on-the-wire ``Opt Data Len``. + + Per :rfc:`8200#section-4.2`, an option's ``Opt Data Len`` field + (what each ``Schema_*Option.len`` here holds) counts *"the length of + the Option Data field of this option, in octets"* -- i.e. it + excludes the Option Type and Opt Data Len fields themselves, so the + whole option, which is what every ``_read_opt_*`` below reports back + as the parsed option's own ``.length``, is two octets more. This is + the ``+2``/``-2`` mismatch between the two (see ``Data_PadOption.length`` + vs. ``Schema_PadOption.len`` below). The read-side half lives in this + one helper so the arithmetic has a single home. Do NOT drop the + ``+ 2``. + + Section 4.2 is the citation because it defines the TLV option format and + contains the sentence quoted above. The Hop-by-Hop Options header this + class implements is :rfc:`8200#section-4.3`, but it defers option + encoding to Section 4.2 (*"one or more TLV-encoded options, as + described in Section 4.2"*) and its only length field is + ``Hdr Ext Len``, *"the length of the Hop-by-Hop Options header in + 8-octet units, not including the first 8 octets"*: the whole header, a + different quantity from one option's ``Opt Data Len`` in octets. + Section 4.3 remains the reference for the header itself, as in the + ``Note:`` on the whole extension header being a multiple of 8 octets. + + Note that only the *stored-length* read-side call sites are + collected here -- most ``_make_opt_*`` methods recompute the wire + ``Opt Data Len`` from ``len(value)`` rather than reading a parsed + ``.length`` back, so they have nothing to unify against this helper. + + Args: + schema_len: raw ``Opt Data Len`` field value, as read off the wire. + + Returns: + Whole-option length, in octets, including the Type and Opt Data Len fields. + + """ + return schema_len + 2 + + def _make_pad_options(self, offset: 'int') -> 'tuple[list[Schema_PadOption], int]': + """Make the padding options needed to align the header to 8 octets. + + Args: + offset: Number of octets emitted so far, **counting the two octets + of the fixed header** (``Next Header`` and ``Hdr Ext Len``) as + well as every option already in the list. + + Returns: + Tuple of the padding option schemas and the number of octets they + occupy. + + Note: + It is the *whole* extension header, fixed part included, that has to + be a multiple of 8 octets [:rfc:`8200#section-4.3`], which is why + the alignment is computed from ``offset`` rather than from the + length of the option just emitted. Aligning each option to 8 octets + on its own leaves the options area a multiple of 8, whereas + :meth:`read` sizes it as ``hdr_ext_len * 8 + 6``, two short of a + multiple of 8; the header would declare 2 octets fewer than it + carried. + + A ``PadN`` option spends two octets on its own type and + ``Opt Data Len`` fields before any padding data, so occupying + ``pad_len`` octets means an ``Opt Data Len`` of ``pad_len - 2``. + One octet is padded with a ``Pad1`` option instead, that being the + only form which can occupy a single octet. Two octets are one + ``PadN`` with an ``Opt Data Len`` of ``0``, not two ``Pad1``: + :rfc:`8200#section-4.2` says a ``PadN`` should be used rather than + multiple ``Pad1`` options. + + """ + pad_len = pad_length(offset, 8) + if pad_len == 0: + return [], 0 + if pad_len == 1: + return [self._make_opt_pad(Enum_Option.Pad1, length=0)], pad_len # type: ignore[arg-type] + return [self._make_opt_pad(Enum_Option.PadN, length=pad_len - 2)], pad_len # type: ignore[arg-type] diff --git a/pcapkit/protocols/internet/icmpv4/protocol.py b/pcapkit/protocols/internet/icmpv4/protocol.py index b410b325c9..73a453f063 100644 --- a/pcapkit/protocols/internet/icmpv4/protocol.py +++ b/pcapkit/protocols/internet/icmpv4/protocol.py @@ -117,8 +117,8 @@ class ICMPv4(ICMP[Data_ICMPv4, Schema_ICMPv4], lambda: 'none', { Enum_Type.Echo_Reply: 'echo', - Enum_Type.Echo: 'echo', Enum_Type.Destination_Unreachable: 'destination_unreachable', + Enum_Type.Echo: 'echo', Enum_Type.Time_Exceeded: 'time_exceeded', Enum_Type.Parameter_Problem: 'parameter_problem', }, diff --git a/pcapkit/protocols/internet/icmpv6/protocol.py b/pcapkit/protocols/internet/icmpv6/protocol.py index 37599a5476..97b35c188c 100644 --- a/pcapkit/protocols/internet/icmpv6/protocol.py +++ b/pcapkit/protocols/internet/icmpv6/protocol.py @@ -361,25 +361,6 @@ def _make_data(cls, data: 'Data_ICMPv6') -> 'dict[str, Any]': # type: ignore[ov 'body': data, } - def _verify_pseudo_checksum(self, pseudo: 'Optional[IP_Pseudo]') -> 'None': - """Verify the checksum over the pseudo-header of the enclosing IP layer. - - That layer calls this once parsed, see - :func:`~pcapkit.protocols.misc.checksum.ip_pseudo.protocol.verify_upper`. - - Args: - pseudo: Pseudo-header, or :data:`None` if the IP layer gives none. - - """ - from pcapkit.corekit.checksum.internet import ChecksumStatus, verify_checksum # isort: skip # pylint: disable=import-outside-toplevel - - if pseudo is None: - status = ChecksumStatus.NO_PSEUDO_HEADER - else: - status = verify_checksum(self.alias, self._data, self._info.checksum, - length=pseudo.info.len, prefix=pseudo.data) - self._info.__update__(checksum_status=status) - def _read_data_type_none(self, schema: 'Schema_Unknown', *, header: 'Schema_ICMPv6', length: 'int') -> 'Data_Unknown': """Read ICMPv6 message of unknown type. @@ -724,3 +705,22 @@ def _make_data_type_echo(self, type: 'Enum_Type', message: 'Optional[Data_Echo]' seq = message.seq data = message.data return Schema_Echo(id=id, seq=seq, data=data) + + def _verify_pseudo_checksum(self, pseudo: 'Optional[IP_Pseudo]') -> 'None': + """Verify the checksum over the pseudo-header of the enclosing IP layer. + + That layer calls this once parsed, see + :func:`~pcapkit.protocols.misc.checksum.ip_pseudo.protocol.verify_upper`. + + Args: + pseudo: Pseudo-header, or :data:`None` if the IP layer gives none. + + """ + from pcapkit.corekit.checksum.internet import ChecksumStatus, verify_checksum # isort: skip # pylint: disable=import-outside-toplevel + + if pseudo is None: + status = ChecksumStatus.NO_PSEUDO_HEADER + else: + status = verify_checksum(self.alias, self._data, self._info.checksum, + length=pseudo.info.len, prefix=pseudo.data) + self._info.__update__(checksum_status=status) diff --git a/pcapkit/protocols/internet/internet.py b/pcapkit/protocols/internet/internet.py index 813db6c494..e189592b6b 100644 --- a/pcapkit/protocols/internet/internet.py +++ b/pcapkit/protocols/internet/internet.py @@ -127,13 +127,13 @@ class Internet(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=ab lambda: ModuleDescriptor('pcapkit.protocols.misc.raw.protocol', 'Raw'), { Enum_TransType.HOPOPT: ModuleDescriptor('pcapkit.protocols.internet.hopopt.protocol', 'HOPOPT'), - Enum_TransType.IPv4: ModuleDescriptor('pcapkit.protocols.internet.ipv4.protocol', 'IPv4'), Enum_TransType.ICMP: ModuleDescriptor('pcapkit.protocols.internet.icmpv4.protocol', 'ICMPv4'), + Enum_TransType.IGMP: ModuleDescriptor('pcapkit.protocols.internet.igmp.protocol', 'IGMP'), + Enum_TransType.IPv4: ModuleDescriptor('pcapkit.protocols.internet.ipv4.protocol', 'IPv4'), Enum_TransType.TCP: ModuleDescriptor('pcapkit.protocols.transport.tcp.protocol', 'TCP'), Enum_TransType.UDP: ModuleDescriptor('pcapkit.protocols.transport.udp.protocol', 'UDP'), Enum_TransType.DCCP: ModuleDescriptor('pcapkit.protocols.transport.dccp.protocol', 'DCCP'), Enum_TransType.IPv6: ModuleDescriptor('pcapkit.protocols.internet.ipv6.protocol', 'IPv6'), - Enum_TransType.IPv6_ICMP: ModuleDescriptor('pcapkit.protocols.internet.icmpv6.protocol', 'ICMPv6'), Enum_TransType.IPv6_Route: ModuleDescriptor('pcapkit.protocols.internet.ipv6_route.protocol', 'IPv6_Route'), Enum_TransType.IPv6_Frag: ModuleDescriptor('pcapkit.protocols.internet.ipv6_frag.protocol', 'IPv6_Frag'), # NOTE: a tunnel is placed by its payload, so GRE lives under ``protocols.link``; @@ -141,13 +141,9 @@ class Internet(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=ab Enum_TransType.GRE: ModuleDescriptor('pcapkit.protocols.link.gre.protocol', 'GRE'), Enum_TransType.ESP: ModuleDescriptor('pcapkit.protocols.internet.esp.protocol', 'ESP'), Enum_TransType.AH: ModuleDescriptor('pcapkit.protocols.internet.ah.protocol', 'AH'), - Enum_TransType.IGMP: ModuleDescriptor('pcapkit.protocols.internet.igmp.protocol', 'IGMP'), + Enum_TransType.IPv6_ICMP: ModuleDescriptor('pcapkit.protocols.internet.icmpv6.protocol', 'ICMPv6'), Enum_TransType.IPv6_NoNxt: ModuleDescriptor('pcapkit.protocols.misc.raw.protocol', 'Raw'), Enum_TransType.IPv6_Opts: ModuleDescriptor('pcapkit.protocols.internet.ipv6_opts.protocol', 'IPv6_Opts'), - Enum_TransType.IPX_in_IP: ModuleDescriptor('pcapkit.protocols.internet.ipx.protocol', 'IPX'), - Enum_TransType.Mobility_Header: ModuleDescriptor('pcapkit.protocols.internet.mh.protocol', 'MH'), - Enum_TransType.HIP: ModuleDescriptor('pcapkit.protocols.internet.hip.protocol', 'HIP'), - Enum_TransType.SCTP: ModuleDescriptor('pcapkit.protocols.transport.sctp.protocol', 'SCTP'), # OSPF rides directly on IP, so IANA protocol number 89 is its only # dispatch point. The dissector nonetheless lives under @@ -157,6 +153,11 @@ class Internet(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=ab # deliberately decoupled, see # :doc:`/contributing/conventions/protocol-layer-placement`. Enum_TransType.OSPFIGP: ModuleDescriptor('pcapkit.protocols.application.ospf.protocol', 'OSPF'), + + Enum_TransType.IPX_in_IP: ModuleDescriptor('pcapkit.protocols.internet.ipx.protocol', 'IPX'), + Enum_TransType.SCTP: ModuleDescriptor('pcapkit.protocols.transport.sctp.protocol', 'SCTP'), + Enum_TransType.Mobility_Header: ModuleDescriptor('pcapkit.protocols.internet.mh.protocol', 'MH'), + Enum_TransType.HIP: ModuleDescriptor('pcapkit.protocols.internet.hip.protocol', 'HIP'), }, ) diff --git a/pcapkit/protocols/internet/ipv4/protocol.py b/pcapkit/protocols/internet/ipv4/protocol.py index a02a247d8a..dba1198793 100644 --- a/pcapkit/protocols/internet/ipv4/protocol.py +++ b/pcapkit/protocols/internet/ipv4/protocol.py @@ -273,18 +273,18 @@ class IPv4(IP[Data_IPv4, Schema_IPv4], { Enum_OptionNumber.EOOL: 'eool', # [RFC 791] 0 Enum_OptionNumber.NOP: 'nop', # [RFC 791] 1 + Enum_OptionNumber.RR: 'rr', # [RFC 791] 7 + Enum_OptionNumber.MTUP: 'mtup', # [RFC 1063][RFC 1191] 11 + Enum_OptionNumber.MTUR: 'mtur', # [RFC 1063][RFC 1191] 12 + Enum_OptionNumber.QS: 'qs', # [RFC 4782] 25 + Enum_OptionNumber.TS: 'ts', # [RFC 791] 68 + Enum_OptionNumber.TR: 'tr', # [RFC 1393][RFC 6814] 82 Enum_OptionNumber.SEC: 'sec', # [RFC 1108] 130 Enum_OptionNumber.LSR: 'lsr', # [RFC 791] 131 - Enum_OptionNumber.TS: 'ts', # [RFC 791] 68 Enum_OptionNumber.E_SEC: 'e_sec', # [RFC 1108] 133 - Enum_OptionNumber.RR: 'rr', # [RFC 791] 7 Enum_OptionNumber.SID: 'sid', # [RFC 791][RFC 6814] 136 Enum_OptionNumber.SSR: 'ssr', # [RFC 791] 137 - Enum_OptionNumber.MTUP: 'mtup', # [RFC 1063][RFC 1191] 11 - Enum_OptionNumber.MTUR: 'mtur', # [RFC 1063][RFC 1191] 12 - Enum_OptionNumber.TR: 'tr', # [RFC 1393][RFC 6814] 82 Enum_OptionNumber.RTRALT: 'rtralt', # [RFC 2113] 148 - Enum_OptionNumber.QS: 'qs', # [RFC 4782] 25 }, ) # type: DefaultDict[Enum_OptionNumber | int, str | tuple[OptionParser, OptionConstructor]] @@ -785,39 +785,6 @@ def _make_data(cls, data: 'Data_IPv4') -> 'dict[str, Any]': # type: ignore[over 'trailer': data.get('trailer', b''), } - def _read_ipv4_addr(self) -> 'IPv4Address': - """Read IP address. - - Returns: - Parsed IP address. - - """ - _byte = self._read_fileng(4) - # _addr = '.'.join([str(_) for _ in _byte]) - # return _addr - return ipaddress.ip_address(_byte) # type: ignore[return-value] - - def _read_ipv4_opt_type(self, code: 'int') -> 'Data_OptionType': - """Read option type field. - - Arguments: - code: option kind value - - Returns: - Extracted IPv4 option type, as an object of the option flag (copied - flag), option class, and option number. - - """ - oflg = bool(code >> 7) - ocls = Enum_OptionClass.get((code >> 5) & 0b11) - onum = code & 0b11111 - - return Data_OptionType.from_dict({ - 'change': oflg, - 'class': ocls, - 'number': onum, - }) - def _read_ipv4_options(self, length: 'int') -> 'Option': """Read IPv4 option list. @@ -1066,20 +1033,17 @@ def _read_opt_nop(self, schema: 'Schema_NOPOption', *, options: 'Option') -> 'Da ) return opt - def _read_opt_sec(self, schema: 'Schema_SECOption', *, options: 'Option') -> 'Data_SECOption': # pylint: disable=unused-argument - """Read IPv4 Security (``SEC``) option. + def _read_opt_rr(self, schema: 'Schema_RROption', *, options: 'Option') -> 'Data_RROption': # pylint: disable=unused-argument + """Read IPv4 Record Route (``RR``) option. - Structure of IPv4 Security (``SEC``) option [:rfc:`1108`]: + Structure of IPv4 Record Route (``RR``) option [:rfc:`791`]: .. code-block:: text - +------------+------------+------------+-------------//----------+ - | 10000010 | XXXXXXXX | SSSSSSSS | AAAAAAA[1] AAAAAAA0 | - | | | | [0] | - +------------+------------+------------+-------------//----------+ - TYPE = 130 LENGTH CLASSIFICATION PROTECTION - LEVEL AUTHORITY - FLAGS + +--------+--------+--------+---------//--------+ + |00000111| length | pointer| route data | + +--------+--------+--------+---------//--------+ + Type=7 Arguments: schema: parsed option schema @@ -1089,59 +1053,67 @@ def _read_opt_sec(self, schema: 'Schema_SECOption', *, options: 'Option') -> 'Da Parsed option data. Raises: - ProtocolError: If ``length`` is **LESS THAN** ``3``. - - Notes: - Per :rfc:`1108#section-2.4`, the low-order bit of each protection - authority octet is the field termination indicator: ``0`` on the - final octet, ``1`` when more octets follow. + ProtocolError: If option is malformed. """ - if schema.length < 3: + if schema.length < 3 or (schema.length - 3) % 4 != 0: raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format') + if schema.pointer < 4: + raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format: pointer too small: {schema.pointer}') - if schema.length > 3: - flags = [] # type: list[Enum_ProtectionAuthority] - for base, byte in enumerate(schema.data): - for bit in range(7): - authority = Enum_ProtectionAuthority.get(base * 8 + bit) - if byte & (0x80 >> bit): - if 'Unassigned' in authority.name: - warn(f'{self.alias}: [OptNo {schema.type}] invalid format: unknown protection authority: {authority}', ProtocolWarning) - flags.append(authority) + opt = Data_RROption( + code=schema.type, + type=self._read_ipv4_opt_type(schema.type), + length=schema.length, + pointer=schema.pointer, + route=tuple(schema.route[:(schema.pointer - 4) // 4]), + remaining=tuple(schema.route[(schema.pointer - 4) // 4:]), + ) + return opt - if byte & 0x01 == 0 and base < schema.length - 4: - warn(f'{self.alias}: [OptNo {schema.type}] invalid format: remaining data', ProtocolWarning) + def _read_opt_mtup(self, schema: 'Schema_MTUPOption', *, options: 'Option') -> 'Data_MTUPOption': # pylint: disable=unused-argument + """Read IPv4 MTU Probe (``MTUP``) option. - if schema.data[-1] & 0x01 == 1: - warn(f'{self.alias}: [OptNo {schema.type}] invalid format: ' - f'field termination indicator set on the final octet: {schema.data[-1]!r}', ProtocolWarning) - termination = tuple(bool(byte & 0x01) for byte in schema.data) - else: - flags = [] - termination = () + Structure of IPv4 MTU Probe (``MTUP``) option [:rfc:`1063`][:rfc:`1191`]: - opt = Data_SECOption( + .. code-block:: text + + +--------+--------+--------+--------+ + |00001011|00000100| 2 octet value | + +--------+--------+--------+--------+ + + Arguments: + schema: parsed option schema + options: extracted IPv4 options + + Returns: + Parsed option data. + + Raises: + ProtocolError: If ``length`` is **NOT** ``4``. + + """ + if schema.length != 4: + raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format') + + opt = Data_MTUPOption( code=schema.type, type=self._read_ipv4_opt_type(schema.type), length=schema.length, - level=schema.level, - flags=tuple(flags), - termination=termination, + mtu=schema.mtu, ) - return opt - def _read_opt_lsr(self, schema: 'Schema_LSROption', *, options: 'Option') -> 'Data_LSROption': # pylint: disable=unused-argument - """Read IPv4 Loose Source Route (``LSR``) option. + def _read_opt_mtur(self, schema: 'Schema_MTUROption', *, options: 'Option') -> 'Data_MTUROption': # pylint: disable=unused-argument + """Read IPv4 MTU Reply (``MTUR``) option. - Structure of IPv4 Loose Source Route (``LSR``) option [:rfc:`791`]: + Structure of IPv4 MTU Reply (``MTUR``) option [:rfc:`1063`][:rfc:`1191`]: .. code-block:: text - +--------+--------+--------+---------//--------+ - |10000011| length | pointer| route data | - +--------+--------+--------+---------//--------+ + +--------+--------+--------+--------+ + |00001100|00000100| 2 octet value | + +--------+--------+--------+--------+ Arguments: schema: parsed option schema @@ -1151,24 +1123,110 @@ def _read_opt_lsr(self, schema: 'Schema_LSROption', *, options: 'Option') -> 'Da Parsed option data. Raises: - ProtocolError: If option is malformed. + ProtocolError: If ``length`` is **NOT** ``4``. """ - if schema.length < 3 or (schema.length - 3) % 4 != 0: + if schema.length != 4: raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format') - if schema.pointer < 4: - raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format: pointer too small: {schema.pointer}') - opt = Data_LSROption( + opt = Data_MTUROption( code=schema.type, type=self._read_ipv4_opt_type(schema.type), length=schema.length, - pointer=schema.pointer, - route=tuple(schema.route[:(schema.pointer - 4) // 4]), - remaining=tuple(schema.route[(schema.pointer - 4) // 4:]), + mtu=schema.mtu, ) return opt + def _read_opt_qs(self, schema: 'Schema_QSOption', *, options: 'Option') -> 'Data_QSOption': # pylint: disable=unused-argument + """Read IPv4 Quick Start (``QS``) option. + + Structure of IPv4 Quick Start (``QS``) option [:rfc:`4782`]: + + * A Quick-Start Request + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option | Length=8 | Func. | Rate | QS TTL | + | | | 0000 |Request| | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | QS Nonce | R | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + * Report of Approved Rate + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option | Length=8 | Func. | Rate | Not Used | + | | | 1000 | Report| | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | QS Nonce | R | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed option schema + options: extracted IPv4 options + + Returns: + Parsed option data. + + Raises: + ProtocolError: If the option is malformed. + + Note: + An option whose QS function is neither of the two above has no + defined layout, so the schema keeps it as an unassigned option and + it is parsed as one, see :meth:`_read_opt_unassigned`. That + dispatch is by the schema's own class, decided from the octets a + file read hands it; a + :class:`~pcapkit.protocols.internet.ipv4.schema.QuickStartRequestOption` + or :class:`~pcapkit.protocols.internet.ipv4.schema.QuickStartReportOption` + built directly with an unassigned ``func`` reaches this method + regardless, which is why it still raises below rather than + returning early. + + """ + if isinstance(schema, Schema_UnassignedOption): + return self._read_opt_unassigned(schema, options=options) # type: ignore[return-value] + if schema.length != 8: + raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format') + + func = Enum_QSFunction.get(schema.flags['func']) + if func == Enum_QSFunction.Quick_Start_Request: + schema_req = cast('Schema_QuickStartRequestOption', schema) + + opt = Data_QuickStartRequestOption( + code=schema.type, + type=self._read_ipv4_opt_type(schema.type), + length=schema_req.length, + func=func, + rate=self._read_qs_rate(schema_req.flags['rate']), + ttl=datetime.timedelta(seconds=schema_req.ttl), + nonce=schema_req.nonce['nonce'], + reserved=schema_req.nonce['reserved'], + ) # type: Data_QSOption + elif func == Enum_QSFunction.Report_of_Approved_Rate: + schema_rep = cast('Schema_QuickStartReportOption', schema) + + opt = Data_QuickStartReportOption( + code=schema.type, + type=self._read_ipv4_opt_type(schema.type), + length=schema_rep.length, + func=func, + rate=self._read_qs_rate(schema_rep.flags['rate']), + unused=schema_rep.reserved, + nonce=schema_rep.nonce['nonce'], + reserved=schema_rep.nonce['reserved'], + ) + else: + raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] unknown QS function: {func}') + return opt + def _read_opt_ts(self, schema: 'Schema_TSOption', *, options: 'Option') -> 'Data_TSOption': # pylint: disable=unused-argument """Read IPv4 Time Stamp (``TS``) option. @@ -1216,19 +1274,21 @@ def _read_opt_ts(self, schema: 'Schema_TSOption', *, options: 'Option') -> 'Data ) return opt - def _read_opt_e_sec(self, schema: 'Schema_ESECOption', *, options: 'Option') -> 'Data_ESECOption': # pylint: disable=unused-argument - """Read IPv4 Extended Security (``E-SEC``) option. + def _read_opt_tr(self, schema: 'Schema_TROption', *, options: 'Option') -> 'Data_TROption': # pylint: disable=unused-argument + """Read IPv4 Traceroute (``TR``) option. - Structure of IPv4 Extended Security (``E-SEC``) option [:rfc:`1108`]: + Structure of IPv4 Traceroute (``TR``) option [:rfc:`1393`][:rfc:`6814`]: .. code-block:: text - +------------+------------+------------+-------//-------+ - | 10000101 | 000LLLLL | AAAAAAAA | add sec info | - +------------+------------+------------+-------//-------+ - TYPE = 133 LENGTH ADDITIONAL ADDITIONAL - SECURITY INFO SECURITY - FORMAT CODE INFO + 0 8 16 24 + +-+-+-+-+-+-+-+-+---------------+---------------+---------------+ + |F| C | Number | Length | ID Number | + +-+-+-+-+-+-+-+-+---------------+---------------+---------------+ + | Outbound Hop Count | Return Hop Count | + +---------------+---------------+---------------+---------------+ + | Originator IP Address | + +---------------+---------------+---------------+---------------+ Arguments: schema: parsed option schema @@ -1238,32 +1298,37 @@ def _read_opt_e_sec(self, schema: 'Schema_ESECOption', *, options: 'Option') -> Parsed option data. Raises: - ProtocolError: If ``length`` is **LESS THAN** ``3``. + ProtocolError: If ``length`` is **NOT** ``12``. """ - if schema.length < 3: + if schema.length != 12: raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format') - opt = Data_ESECOption( - code=schema.type, - type=self._read_ipv4_opt_type(schema.type), - length=schema.length, - format=schema.format, - info=schema.info, - ) + opt = Data_TROption.from_dict({ + 'code': schema.type, + 'type': self._read_ipv4_opt_type(schema.type), + 'length': schema.length, + 'id': schema.id, + 'outbound': schema.out, + 'return': schema.ret, + 'originator': schema.origin, + }) return opt - def _read_opt_rr(self, schema: 'Schema_RROption', *, options: 'Option') -> 'Data_RROption': # pylint: disable=unused-argument - """Read IPv4 Record Route (``RR``) option. + def _read_opt_sec(self, schema: 'Schema_SECOption', *, options: 'Option') -> 'Data_SECOption': # pylint: disable=unused-argument + """Read IPv4 Security (``SEC``) option. - Structure of IPv4 Record Route (``RR``) option [:rfc:`791`]: + Structure of IPv4 Security (``SEC``) option [:rfc:`1108`]: .. code-block:: text - +--------+--------+--------+---------//--------+ - |00000111| length | pointer| route data | - +--------+--------+--------+---------//--------+ - Type=7 + +------------+------------+------------+-------------//----------+ + | 10000010 | XXXXXXXX | SSSSSSSS | AAAAAAA[1] AAAAAAA0 | + | | | | [0] | + +------------+------------+------------+-------------//----------+ + TYPE = 130 LENGTH CLASSIFICATION PROTECTION + LEVEL AUTHORITY + FLAGS Arguments: schema: parsed option schema @@ -1273,69 +1338,59 @@ def _read_opt_rr(self, schema: 'Schema_RROption', *, options: 'Option') -> 'Data Parsed option data. Raises: - ProtocolError: If option is malformed. + ProtocolError: If ``length`` is **LESS THAN** ``3``. + + Notes: + Per :rfc:`1108#section-2.4`, the low-order bit of each protection + authority octet is the field termination indicator: ``0`` on the + final octet, ``1`` when more octets follow. """ - if schema.length < 3 or (schema.length - 3) % 4 != 0: + if schema.length < 3: raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format') - if schema.pointer < 4: - raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format: pointer too small: {schema.pointer}') - - opt = Data_RROption( - code=schema.type, - type=self._read_ipv4_opt_type(schema.type), - length=schema.length, - pointer=schema.pointer, - route=tuple(schema.route[:(schema.pointer - 4) // 4]), - remaining=tuple(schema.route[(schema.pointer - 4) // 4:]), - ) - return opt - - def _read_opt_sid(self, schema: 'Schema_SIDOption', *, options: 'Option') -> 'Data_SIDOption': # pylint: disable=unused-argument - """Read IPv4 Stream ID (``SID``) option. - - Structure of IPv4 Stream ID (``SID``) option [:rfc:`791`][:rfc:`6814`]: - - .. code-block:: text - - +--------+--------+--------+--------+ - |10001000|00000010| Stream ID | - +--------+--------+--------+--------+ - Type=136 Length=4 - - Arguments: - schema: parsed option schema - options: extracted IPv4 options - Returns: - Parsed option data. + if schema.length > 3: + flags = [] # type: list[Enum_ProtectionAuthority] + for base, byte in enumerate(schema.data): + for bit in range(7): + authority = Enum_ProtectionAuthority.get(base * 8 + bit) + if byte & (0x80 >> bit): + if 'Unassigned' in authority.name: + warn(f'{self.alias}: [OptNo {schema.type}] invalid format: unknown protection authority: {authority}', ProtocolWarning) + flags.append(authority) - Raises: - ProtocolError: If ``length`` is **NOT** ``4``. + if byte & 0x01 == 0 and base < schema.length - 4: + warn(f'{self.alias}: [OptNo {schema.type}] invalid format: remaining data', ProtocolWarning) - """ - if schema.length != 4: - raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format') + if schema.data[-1] & 0x01 == 1: + warn(f'{self.alias}: [OptNo {schema.type}] invalid format: ' + f'field termination indicator set on the final octet: {schema.data[-1]!r}', ProtocolWarning) + termination = tuple(bool(byte & 0x01) for byte in schema.data) + else: + flags = [] + termination = () - opt = Data_SIDOption( + opt = Data_SECOption( code=schema.type, type=self._read_ipv4_opt_type(schema.type), length=schema.length, - sid=schema.sid, + level=schema.level, + flags=tuple(flags), + termination=termination, ) + return opt - def _read_opt_ssr(self, schema: 'Schema_SSROption', *, options: 'Option') -> 'Data_SSROption': # pylint: disable=unused-argument - """Read IPv4 Strict Source Route (``SSR``) option. + def _read_opt_lsr(self, schema: 'Schema_LSROption', *, options: 'Option') -> 'Data_LSROption': # pylint: disable=unused-argument + """Read IPv4 Loose Source Route (``LSR``) option. - Structure of IPv4 Strict Source Route (``SSR``) option [:rfc:`791`]: + Structure of IPv4 Loose Source Route (``LSR``) option [:rfc:`791`]: .. code-block:: text +--------+--------+--------+---------//--------+ - |10001001| length | pointer| route data | + |10000011| length | pointer| route data | +--------+--------+--------+---------//--------+ - Type=137 Arguments: schema: parsed option schema @@ -1353,7 +1408,7 @@ def _read_opt_ssr(self, schema: 'Schema_SSROption', *, options: 'Option') -> 'Da if schema.pointer < 4: raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format: pointer too small: {schema.pointer}') - opt = Data_SSROption( + opt = Data_LSROption( code=schema.type, type=self._read_ipv4_opt_type(schema.type), length=schema.length, @@ -1363,16 +1418,19 @@ def _read_opt_ssr(self, schema: 'Schema_SSROption', *, options: 'Option') -> 'Da ) return opt - def _read_opt_mtup(self, schema: 'Schema_MTUPOption', *, options: 'Option') -> 'Data_MTUPOption': # pylint: disable=unused-argument - """Read IPv4 MTU Probe (``MTUP``) option. + def _read_opt_e_sec(self, schema: 'Schema_ESECOption', *, options: 'Option') -> 'Data_ESECOption': # pylint: disable=unused-argument + """Read IPv4 Extended Security (``E-SEC``) option. - Structure of IPv4 MTU Probe (``MTUP``) option [:rfc:`1063`][:rfc:`1191`]: + Structure of IPv4 Extended Security (``E-SEC``) option [:rfc:`1108`]: .. code-block:: text - +--------+--------+--------+--------+ - |00001011|00000100| 2 octet value | - +--------+--------+--------+--------+ + +------------+------------+------------+-------//-------+ + | 10000101 | 000LLLLL | AAAAAAAA | add sec info | + +------------+------------+------------+-------//-------+ + TYPE = 133 LENGTH ADDITIONAL ADDITIONAL + SECURITY INFO SECURITY + FORMAT CODE INFO Arguments: schema: parsed option schema @@ -1382,30 +1440,32 @@ def _read_opt_mtup(self, schema: 'Schema_MTUPOption', *, options: 'Option') -> ' Parsed option data. Raises: - ProtocolError: If ``length`` is **NOT** ``4``. + ProtocolError: If ``length`` is **LESS THAN** ``3``. """ - if schema.length != 4: + if schema.length < 3: raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format') - opt = Data_MTUPOption( + opt = Data_ESECOption( code=schema.type, type=self._read_ipv4_opt_type(schema.type), length=schema.length, - mtu=schema.mtu, + format=schema.format, + info=schema.info, ) return opt - def _read_opt_mtur(self, schema: 'Schema_MTUROption', *, options: 'Option') -> 'Data_MTUROption': # pylint: disable=unused-argument - """Read IPv4 MTU Reply (``MTUR``) option. + def _read_opt_sid(self, schema: 'Schema_SIDOption', *, options: 'Option') -> 'Data_SIDOption': # pylint: disable=unused-argument + """Read IPv4 Stream ID (``SID``) option. - Structure of IPv4 MTU Reply (``MTUR``) option [:rfc:`1063`][:rfc:`1191`]: + Structure of IPv4 Stream ID (``SID``) option [:rfc:`791`][:rfc:`6814`]: .. code-block:: text +--------+--------+--------+--------+ - |00001100|00000100| 2 octet value | + |10001000|00000010| Stream ID | +--------+--------+--------+--------+ + Type=136 Length=4 Arguments: schema: parsed option schema @@ -1421,29 +1481,25 @@ def _read_opt_mtur(self, schema: 'Schema_MTUROption', *, options: 'Option') -> ' if schema.length != 4: raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format') - opt = Data_MTUROption( + opt = Data_SIDOption( code=schema.type, type=self._read_ipv4_opt_type(schema.type), length=schema.length, - mtu=schema.mtu, + sid=schema.sid, ) return opt - def _read_opt_tr(self, schema: 'Schema_TROption', *, options: 'Option') -> 'Data_TROption': # pylint: disable=unused-argument - """Read IPv4 Traceroute (``TR``) option. + def _read_opt_ssr(self, schema: 'Schema_SSROption', *, options: 'Option') -> 'Data_SSROption': # pylint: disable=unused-argument + """Read IPv4 Strict Source Route (``SSR``) option. - Structure of IPv4 Traceroute (``TR``) option [:rfc:`1393`][:rfc:`6814`]: + Structure of IPv4 Strict Source Route (``SSR``) option [:rfc:`791`]: .. code-block:: text - 0 8 16 24 - +-+-+-+-+-+-+-+-+---------------+---------------+---------------+ - |F| C | Number | Length | ID Number | - +-+-+-+-+-+-+-+-+---------------+---------------+---------------+ - | Outbound Hop Count | Return Hop Count | - +---------------+---------------+---------------+---------------+ - | Originator IP Address | - +---------------+---------------+---------------+---------------+ + +--------+--------+--------+---------//--------+ + |10001001| length | pointer| route data | + +--------+--------+--------+---------//--------+ + Type=137 Arguments: schema: parsed option schema @@ -1453,21 +1509,22 @@ def _read_opt_tr(self, schema: 'Schema_TROption', *, options: 'Option') -> 'Data Parsed option data. Raises: - ProtocolError: If ``length`` is **NOT** ``12``. + ProtocolError: If option is malformed. """ - if schema.length != 12: + if schema.length < 3 or (schema.length - 3) % 4 != 0: raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format') + if schema.pointer < 4: + raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format: pointer too small: {schema.pointer}') - opt = Data_TROption.from_dict({ - 'code': schema.type, - 'type': self._read_ipv4_opt_type(schema.type), - 'length': schema.length, - 'id': schema.id, - 'outbound': schema.out, - 'return': schema.ret, - 'originator': schema.origin, - }) + opt = Data_SSROption( + code=schema.type, + type=self._read_ipv4_opt_type(schema.type), + length=schema.length, + pointer=schema.pointer, + route=tuple(schema.route[:(schema.pointer - 4) // 4]), + remaining=tuple(schema.route[(schema.pointer - 4) // 4:]), + ) return opt def _read_opt_rtralt(self, schema: 'Schema_RTRALTOption', *, options: 'Option') -> 'Data_RTRALTOption': # pylint: disable=unused-argument @@ -1503,313 +1560,251 @@ def _read_opt_rtralt(self, schema: 'Schema_RTRALTOption', *, options: 'Option') ) return opt - def _read_opt_qs(self, schema: 'Schema_QSOption', *, options: 'Option') -> 'Data_QSOption': # pylint: disable=unused-argument - """Read IPv4 Quick Start (``QS``) option. + def _make_opt_unassigned(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_UnassignedOption]' = None, *, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_Option': + """Make IPv4 unassigned options. - Structure of IPv4 Quick Start (``QS``) option [:rfc:`4782`]: + Args: + kind: option type code + option: option data + data: option payload, i.e. the octets after the ``length`` octet + **kwargs: arbitrary keyword arguments - * A Quick-Start Request + Returns: + Constructed option schema. - .. code-block:: text + """ + if option is not None: + data = option.data - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option | Length=8 | Func. | Rate | QS TTL | - | | | 0000 |Request| | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | QS Nonce | R | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + return Schema_UnassignedOption( + type=kind, + length=len(data) + 2, + data=data, + ) - * Report of Approved Rate + def _make_opt_eool(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_EOOLOption]' = None, + **kwargs: 'Any') -> 'Schema_EOOLOption': + """Make IPv4 End of Option List (``EOOL``) option. - .. code-block:: text + Args: + kind: option type code + option: option data + **kwargs: arbitrary keyword arguments - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option | Length=8 | Func. | Rate | Not Used | - | | | 1000 | Report| | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | QS Nonce | R | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - Arguments: - schema: parsed option schema - options: extracted IPv4 options - - Returns: - Parsed option data. - - Raises: - ProtocolError: If the option is malformed. - - Note: - An option whose QS function is neither of the two above has no - defined layout, so the schema keeps it as an unassigned option and - it is parsed as one, see :meth:`_read_opt_unassigned`. That - dispatch is by the schema's own class, decided from the octets a - file read hands it; a - :class:`~pcapkit.protocols.internet.ipv4.schema.QuickStartRequestOption` - or :class:`~pcapkit.protocols.internet.ipv4.schema.QuickStartReportOption` - built directly with an unassigned ``func`` reaches this method - regardless, which is why it still raises below rather than - returning early. + Returns: + Constructed option schema. """ - if isinstance(schema, Schema_UnassignedOption): - return self._read_opt_unassigned(schema, options=options) # type: ignore[return-value] - if schema.length != 8: - raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format') - - func = Enum_QSFunction.get(schema.flags['func']) - if func == Enum_QSFunction.Quick_Start_Request: - schema_req = cast('Schema_QuickStartRequestOption', schema) - - opt = Data_QuickStartRequestOption( - code=schema.type, - type=self._read_ipv4_opt_type(schema.type), - length=schema_req.length, - func=func, - rate=self._read_qs_rate(schema_req.flags['rate']), - ttl=datetime.timedelta(seconds=schema_req.ttl), - nonce=schema_req.nonce['nonce'], - reserved=schema_req.nonce['reserved'], - ) # type: Data_QSOption - elif func == Enum_QSFunction.Report_of_Approved_Rate: - schema_rep = cast('Schema_QuickStartReportOption', schema) - - opt = Data_QuickStartReportOption( - code=schema.type, - type=self._read_ipv4_opt_type(schema.type), - length=schema_rep.length, - func=func, - rate=self._read_qs_rate(schema_rep.flags['rate']), - unused=schema_rep.reserved, - nonce=schema_rep.nonce['nonce'], - reserved=schema_rep.nonce['reserved'], - ) - else: - raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] unknown QS function: {func}') - return opt + return Schema_EOOLOption( + type=kind, + length=1, + ) - def _make_opt_unassigned(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_UnassignedOption]' = None, *, - data: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_Option': - """Make IPv4 unassigned options. + def _make_opt_nop(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_NOPOption]' = None, + **kwargs: 'Any') -> 'Schema_NOPOption': + """Make IPv4 No Operation (``NOP``) option. Args: kind: option type code option: option data - data: option payload, i.e. the octets after the ``length`` octet **kwargs: arbitrary keyword arguments Returns: Constructed option schema. """ - if option is not None: - data = option.data - - return Schema_UnassignedOption( + return Schema_NOPOption( type=kind, - length=len(data) + 2, - data=data, + length=1, ) - def _make_opt_eool(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_EOOLOption]' = None, - **kwargs: 'Any') -> 'Schema_EOOLOption': - """Make IPv4 End of Option List (``EOOL``) option. + def _make_opt_rr(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_RROption]' = None, *, + counts: 'int' = 10, # reasonable default + route: 'Optional[list[IPv4Address | str | bytes | int]]' = None, + remaining: 'Optional[list[IPv4Address | str | bytes | int]]' = None, + **kwargs: 'Any') -> 'Schema_RROption': + """Make IPv4 Record Route (``RR``) option. Args: kind: option type code option: option data + counts: maximum number of addresses to record + route: list of IPv4 addresses as recorded routes, i.e. the + address slots before the pointer + remaining: list of IPv4 addresses for the address slots at or beyond + the pointer; the slots left over up to ``counts`` are zero **kwargs: arbitrary keyword arguments Returns: Constructed option schema. + Raises: + ProtocolError: If ``counts`` is not a number of slots the + one-octet length can count. + """ - return Schema_EOOLOption( + if option is not None: + slots = list(option.route or ()) # type: list[IPv4Address | str | bytes | int] + slots += getattr(option, 'remaining', ()) + pointer = option.pointer + length = option.length + else: + route = [] if route is None else route + # NOTE: 63 slots fill the one-octet length: 3 + 4 * 63 = 255. + counts = self._make_uint(counts, bits=6, name='address count') + length = 3 + counts * 4 + pointer = 4 + min(len(route), counts) * 4 + + # NOTE: Every address slot of the option is written, the empty ones + # as ``0.0.0.0``. + slots = (list(route[:counts]) + list(remaining or ()))[:counts] + slots += [0] * (counts - len(slots)) + + return Schema_RROption( type=kind, - length=1, + length=length, + pointer=pointer, + route=slots, ) - def _make_opt_nop(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_NOPOption]' = None, - **kwargs: 'Any') -> 'Schema_NOPOption': - """Make IPv4 No Operation (``NOP``) option. + def _make_opt_mtup(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_MTUPOption]' = None, *, + mtu: 'int' = 0, + **kwargs: 'Any') -> 'Schema_MTUPOption': + """Make IPv4 MTU Probe (``MTUP``) option. Args: kind: option type code option: option data + mtu: MTU value **kwargs: arbitrary keyword arguments Returns: Constructed option schema. + Raises: + ProtocolError: If ``mtu`` does not fit in two octets. + """ - return Schema_NOPOption( + if option is not None: + mtu = option.mtu + + return Schema_MTUPOption( type=kind, - length=1, + length=4, + mtu=self._make_uint(mtu, bits=16, name='MTU'), ) - def _make_opt_sec(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_SECOption]' = None, *, - level: 'Enum_ClassificationLevel | int' = Enum_ClassificationLevel.Unclassified, - authorities: 'Optional[list[Enum_ProtectionAuthority]]' = None, - **kwargs: 'Any') -> 'Schema_SECOption': - """Make IPv4 Security (``SEC``) option. + def _make_opt_mtur(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_MTUROption]' = None, *, + mtu: 'int' = 0, + **kwargs: 'Any') -> 'Schema_MTUROption': + """Make IPv4 MTU Reply (``MTUR``) option. Args: kind: option type code option: option data - level: classification level - authorities: list of protection authority flags + mtu: MTU value **kwargs: arbitrary keyword arguments Returns: Constructed option schema. Raises: - ProtocolError: If ``level`` is a name, a :obj:`bool`, or a member - or code that does not fit in one octet, or ``authorities`` - names a bit position that is not a protection authority -- a - negative one, one that :rfc:`1108` reserves as a field - termination indicator, or one past what the one-octet option - length can count. - - Notes: - :rfc:`1108#section-2.4` lays each protection authority octet out as - seven authority bits followed by a *field termination indicator* in - the low-order bit: ``1`` means another octet follows, ``0`` means - this is the last. So the authority numbering skips every position that is a - termination bit -- 7, 15, 23 -- which is what - :meth:`_read_opt_sec` encodes by looping over ``range(7)`` per - octet, and the reason ``Field_Termination_Indicator`` is rejected - here rather than written: the enumeration names it as structure, and - a value written there would be dropped on the way back in. See :issue:`537`. - - The indicators themselves are set from position, except that a - parsed ``option`` whose ``termination`` covers every octet written - gets back the bits it carried, so that a non-conforming indicator - rebuilds as read. + ProtocolError: If ``mtu`` does not fit in two octets. """ - termination = () # type: tuple[bool, ...] if option is not None: - level = option.level - authorities = cast('list[Enum_ProtectionAuthority]', option.flags) - termination = tuple(getattr(option, 'termination', ())) - else: - authorities = [] if authorities is None else authorities - level_val = self._make_index(level, Enum_ClassificationLevel, bits=8, - name='classification level') - - if authorities: - for auth in authorities: - # NOTE: 11 bits keeps the bitmap below 256 octets; the option - # length below draws the exact line. - self._make_uint(auth, bits=11, name='protection authority') - if auth % 8 == 7: - # ``.name`` where there is one, rather than - # ``Enum_ProtectionAuthority.get(auth)``: that call runs - # ``_missing_`` for an unnamed index, which extends the - # enumeration as a side effect. An error path is the last - # place that should mutate a registry. - raise ProtocolError(f'{self.alias}: [OptNo {kind}] invalid protection ' - f'authority: {getattr(auth, "name", auth)} is a field ' - f'termination indicator, not an authority') - - # ``max_auth`` is the highest bit *index*, so the octet count comes - # from the bit *count* one past it. Sizing from the index would put - # a single ``GENSER`` (index 0) in a zero-octet bitmap and raise - # ``IndexError``, and under-size by an octet at every exact - # multiple of eight. - max_auth = max(authorities) - int_len = math.ceil((max_auth + 1) / 8) - else: - int_len = 0 - - # A parsed option keeps its own width, so that octets which carry no - # authority are not dropped on rebuild. - if option is not None: - int_len = max(int_len, option.length - 3) - - if int_len: - data_list = [b'0' for _ in range(int_len * 8)] - for auth in authorities: - data_list[auth] = b'1' - - # The low-order bit of every octet but the last is ``1``, meaning - # "another octet follows"; the last keeps its ``0``, which ends the - # field. A parsed option instead writes back the indicators it - # carried on the wire, conforming or not, so long as they still - # cover every octet written. - if len(termination) == int_len: - for index, bit in enumerate(termination): - data_list[index * 8 + 7] = b'1' if bit else b'0' - else: - for index in range(int_len - 1): - data_list[index * 8 + 7] = b'1' - - data = int(b''.join(data_list), base=2).to_bytes(int_len, 'big', signed=False) - else: - data = b'' + mtu = option.mtu - return Schema_SECOption( + return Schema_MTUROption( type=kind, - length=self._make_uint(3 + len(data), bits=8, name='option length'), - level=level_val, - data=data, + length=4, + mtu=self._make_uint(mtu, bits=16, name='MTU'), ) - def _make_opt_lsr(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_LSROption]' = None, *, - counts: 'int' = 10, # reasonable default - route: 'Optional[list[IPv4Address | str | bytes | int]]' = None, - remaining: 'Optional[list[IPv4Address | str | bytes | int]]' = None, - **kwargs: 'Any') -> 'Schema_LSROption': - """Make IPv4 Loose Source and Record Route (``LSR``) option. + def _make_opt_qs(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_QuickStartRequestOption | Data_QuickStartReportOption | Data_UnassignedOption]' = None, *, + func: 'Enum_QSFunction | int' = Enum_QSFunction.Quick_Start_Request, + rate: 'int' = 0, + ttl: 'timedelta | int' = 0, + unused: 'int' = 0, + nonce: 'int' = 0, + reserved: 'int' = 0, + **kwargs: 'Any') -> 'Schema_QSOption | Schema_UnassignedOption': + """Make IPv4 Quick-Start (``QS``) option. Args: kind: option type code option: option data - counts: maximum number of addresses to record - route: list of IPv4 addresses as recorded routes, i.e. the - address slots before the pointer - remaining: list of IPv4 addresses for the address slots at or beyond - the pointer; the slots left over up to ``counts`` are zero + func: QS function type + rate: rate (in kbps), rounded down to the nearest rate the + four-bit rate code carries (:rfc:`4782#section-3.1`) + ttl: time to live (in seconds) + unused: the not-used fourth octet of a Report of Approved Rate + nonce: nonce value + reserved: reserved bits (``R``) after the nonce **kwargs: arbitrary keyword arguments Returns: - Constructed option schema. + Constructed option schema. An unassigned option schema if ``option`` + is a Quick-Start option parsed as unassigned, see :meth:`_read_opt_qs`, + or if ``func`` is neither of the assigned functions, in which case + the option takes the eight-octet layout of a Quick-Start Request. Raises: - ProtocolError: If ``counts`` is not a number of slots the - one-octet length can count. + ProtocolError: If ``func`` is a name, a :obj:`bool`, or a member + or code that does not fit in four bits, the rate code cannot + carry ``rate``, or ``ttl``, ``unused``, ``nonce`` or + ``reserved`` does not fit its field. """ + if isinstance(option, Data_UnassignedOption): + return cast('Schema_UnassignedOption', self._make_opt_unassigned(kind, option)) if option is not None: - slots = list(option.route or ()) # type: list[IPv4Address | str | bytes | int] - slots += getattr(option, 'remaining', ()) - pointer = option.pointer - length = option.length - else: - route = [] if route is None else route - # NOTE: 63 slots fill the one-octet length: 3 + 4 * 63 = 255. - counts = self._make_uint(counts, bits=6, name='address count') - length = 3 + counts * 4 - pointer = 4 + min(len(route), counts) * 4 - - # NOTE: Every address slot of the option is written, the empty ones - # as ``0.0.0.0``. - slots = (list(route[:counts]) + list(remaining or ()))[:counts] - slots += [0] * (counts - len(slots)) + func = option.func + rate = option.rate + ttl = getattr(option, 'ttl', 0) + unused = getattr(option, 'unused', 0) + nonce = option.nonce + reserved = getattr(option, 'reserved', 0) + func_enum = self._make_index(func, Enum_QSFunction, bits=4, name='QS function') + rate_val = self._make_qs_rate(rate) + ttl_value = ttl if isinstance(ttl, int) else math.floor(ttl.total_seconds()) + nonce = self._make_uint(nonce, bits=30, name='Quick-Start nonce') + reserved = self._make_uint(reserved, bits=2, name='Quick-Start reserved') - return Schema_LSROption( - type=kind, - length=length, - pointer=pointer, - route=slots, - ) + if func_enum == Enum_QSFunction.Quick_Start_Request: + return Schema_QuickStartRequestOption( + type=kind, + length=8, + flags={ + 'func': func_enum, + 'rate': rate_val, + }, + ttl=self._make_uint(ttl_value, bits=8, name='Quick-Start TTL'), + nonce={ + 'nonce': nonce, + 'reserved': reserved, + }, + ) + if func_enum == Enum_QSFunction.Report_of_Approved_Rate: + return Schema_QuickStartReportOption( + type=kind, + length=8, + flags={ + 'func': func_enum, + 'rate': rate_val, + }, + reserved=self._make_uint(unused, bits=8, name='Quick-Start not-used octet'), + nonce={ + 'nonce': nonce, + 'reserved': reserved, + }, + ) + func_val = int(func_enum) + ttl_value = self._make_uint(ttl_value, bits=8, name='Quick-Start TTL') + data = bytes([(func_val << 4) | rate_val, ttl_value]) + ((nonce << 2) | reserved).to_bytes(4, 'big') + return cast('Schema_UnassignedOption', self._make_opt_unassigned(kind, data=data)) def _make_opt_ts(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_TSOption]' = None, *, counts: 'Optional[int]' = None, @@ -1953,46 +1948,161 @@ def _make_opt_ts(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_TSOptio remainder=remainder, ) - def _make_opt_e_sec(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_ESECOption]' = None, *, - format: 'int' = 0, - info: 'Optional[bytes]' = None, - **kwargs: 'Any') -> 'Schema_ESECOption': - """Make IPv4 Extended Security (``E-SEC``) option. + def _make_opt_tr(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_TROption]' = None, *, + id: 'int' = 0, + out: 'int' = 0, + ret: 'int' = 0, + origin: 'IPv4Address | str | bytes | int' = '127.0.0.1', + **kwargs: 'Any') -> 'Schema_TROption': + """Make IPv4 Traceroute (``TR``) option. Args: kind: option type code option: option data - format: additional security information format code - info: additional security information + id: ID number + out: outbound hop count + ret: return hop count + origin: originator IP address **kwargs: arbitrary keyword arguments Returns: Constructed option schema. Raises: - ProtocolError: If ``format`` does not fit in one octet. + ProtocolError: If ``id``, ``out`` or ``ret`` does not fit in two + octets. """ if option is not None: - length = option.length - format = option.format - info = option.info + id = option.id + out = option.outbound + ret = option['return'] + origin = option.originator + + return Schema_TROption( + type=kind, + length=12, + id=self._make_uint(id, bits=16, name='traceroute ID number'), + out=self._make_uint(out, bits=16, name='traceroute outbound hop count'), + ret=self._make_uint(ret, bits=16, name='traceroute return hop count'), + origin=origin, + ) + + def _make_opt_sec(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_SECOption]' = None, *, + level: 'Enum_ClassificationLevel | int' = Enum_ClassificationLevel.Unclassified, + authorities: 'Optional[list[Enum_ProtectionAuthority]]' = None, + **kwargs: 'Any') -> 'Schema_SECOption': + """Make IPv4 Security (``SEC``) option. + + Args: + kind: option type code + option: option data + level: classification level + authorities: list of protection authority flags + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + Raises: + ProtocolError: If ``level`` is a name, a :obj:`bool`, or a member + or code that does not fit in one octet, or ``authorities`` + names a bit position that is not a protection authority -- a + negative one, one that :rfc:`1108` reserves as a field + termination indicator, or one past what the one-octet option + length can count. + + Notes: + :rfc:`1108#section-2.4` lays each protection authority octet out as + seven authority bits followed by a *field termination indicator* in + the low-order bit: ``1`` means another octet follows, ``0`` means + this is the last. So the authority numbering skips every position that is a + termination bit -- 7, 15, 23 -- which is what + :meth:`_read_opt_sec` encodes by looping over ``range(7)`` per + octet, and the reason ``Field_Termination_Indicator`` is rejected + here rather than written: the enumeration names it as structure, and + a value written there would be dropped on the way back in. See :issue:`537`. + + The indicators themselves are set from position, except that a + parsed ``option`` whose ``termination`` covers every octet written + gets back the bits it carried, so that a non-conforming indicator + rebuilds as read. + + """ + termination = () # type: tuple[bool, ...] + if option is not None: + level = option.level + authorities = cast('list[Enum_ProtectionAuthority]', option.flags) + termination = tuple(getattr(option, 'termination', ())) + else: + authorities = [] if authorities is None else authorities + level_val = self._make_index(level, Enum_ClassificationLevel, bits=8, + name='classification level') + + if authorities: + for auth in authorities: + # NOTE: 11 bits keeps the bitmap below 256 octets; the option + # length below draws the exact line. + self._make_uint(auth, bits=11, name='protection authority') + if auth % 8 == 7: + # ``.name`` where there is one, rather than + # ``Enum_ProtectionAuthority.get(auth)``: that call runs + # ``_missing_`` for an unnamed index, which extends the + # enumeration as a side effect. An error path is the last + # place that should mutate a registry. + raise ProtocolError(f'{self.alias}: [OptNo {kind}] invalid protection ' + f'authority: {getattr(auth, "name", auth)} is a field ' + f'termination indicator, not an authority') + + # ``max_auth`` is the highest bit *index*, so the octet count comes + # from the bit *count* one past it. Sizing from the index would put + # a single ``GENSER`` (index 0) in a zero-octet bitmap and raise + # ``IndexError``, and under-size by an octet at every exact + # multiple of eight. + max_auth = max(authorities) + int_len = math.ceil((max_auth + 1) / 8) + else: + int_len = 0 + + # A parsed option keeps its own width, so that octets which carry no + # authority are not dropped on rebuild. + if option is not None: + int_len = max(int_len, option.length - 3) + + if int_len: + data_list = [b'0' for _ in range(int_len * 8)] + for auth in authorities: + data_list[auth] = b'1' + + # The low-order bit of every octet but the last is ``1``, meaning + # "another octet follows"; the last keeps its ``0``, which ends the + # field. A parsed option instead writes back the indicators it + # carried on the wire, conforming or not, so long as they still + # cover every octet written. + if len(termination) == int_len: + for index, bit in enumerate(termination): + data_list[index * 8 + 7] = b'1' if bit else b'0' + else: + for index in range(int_len - 1): + data_list[index * 8 + 7] = b'1' + + data = int(b''.join(data_list), base=2).to_bytes(int_len, 'big', signed=False) else: - length = (3 + len(info)) if info is not None else 3 + data = b'' - return Schema_ESECOption( + return Schema_SECOption( type=kind, - length=length, - format=self._make_uint(format, bits=8, name='E-SEC format code'), - info=info, + length=self._make_uint(3 + len(data), bits=8, name='option length'), + level=level_val, + data=data, ) - def _make_opt_rr(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_RROption]' = None, *, - counts: 'int' = 10, # reasonable default - route: 'Optional[list[IPv4Address | str | bytes | int]]' = None, - remaining: 'Optional[list[IPv4Address | str | bytes | int]]' = None, - **kwargs: 'Any') -> 'Schema_RROption': - """Make IPv4 Record Route (``RR``) option. + def _make_opt_lsr(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_LSROption]' = None, *, + counts: 'int' = 10, # reasonable default + route: 'Optional[list[IPv4Address | str | bytes | int]]' = None, + remaining: 'Optional[list[IPv4Address | str | bytes | int]]' = None, + **kwargs: 'Any') -> 'Schema_LSROption': + """Make IPv4 Loose Source and Record Route (``LSR``) option. Args: kind: option type code @@ -2029,13 +2139,47 @@ def _make_opt_rr(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_RROptio slots = (list(route[:counts]) + list(remaining or ()))[:counts] slots += [0] * (counts - len(slots)) - return Schema_RROption( + return Schema_LSROption( type=kind, length=length, pointer=pointer, route=slots, ) + def _make_opt_e_sec(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_ESECOption]' = None, *, + format: 'int' = 0, + info: 'Optional[bytes]' = None, + **kwargs: 'Any') -> 'Schema_ESECOption': + """Make IPv4 Extended Security (``E-SEC``) option. + + Args: + kind: option type code + option: option data + format: additional security information format code + info: additional security information + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + Raises: + ProtocolError: If ``format`` does not fit in one octet. + + """ + if option is not None: + length = option.length + format = option.format + info = option.info + else: + length = (3 + len(info)) if info is not None else 3 + + return Schema_ESECOption( + type=kind, + length=length, + format=self._make_uint(format, bits=8, name='E-SEC format code'), + info=info, + ) + def _make_opt_sid(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_SIDOption]' = None, *, sid: 'int' = 0, **kwargs: 'Any') -> 'Schema_SIDOption': @@ -2112,100 +2256,6 @@ def _make_opt_ssr(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_SSROpt route=slots, ) - def _make_opt_mtup(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_MTUPOption]' = None, *, - mtu: 'int' = 0, - **kwargs: 'Any') -> 'Schema_MTUPOption': - """Make IPv4 MTU Probe (``MTUP``) option. - - Args: - kind: option type code - option: option data - mtu: MTU value - **kwargs: arbitrary keyword arguments - - Returns: - Constructed option schema. - - Raises: - ProtocolError: If ``mtu`` does not fit in two octets. - - """ - if option is not None: - mtu = option.mtu - - return Schema_MTUPOption( - type=kind, - length=4, - mtu=self._make_uint(mtu, bits=16, name='MTU'), - ) - - def _make_opt_mtur(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_MTUROption]' = None, *, - mtu: 'int' = 0, - **kwargs: 'Any') -> 'Schema_MTUROption': - """Make IPv4 MTU Reply (``MTUR``) option. - - Args: - kind: option type code - option: option data - mtu: MTU value - **kwargs: arbitrary keyword arguments - - Returns: - Constructed option schema. - - Raises: - ProtocolError: If ``mtu`` does not fit in two octets. - - """ - if option is not None: - mtu = option.mtu - - return Schema_MTUROption( - type=kind, - length=4, - mtu=self._make_uint(mtu, bits=16, name='MTU'), - ) - - def _make_opt_tr(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_TROption]' = None, *, - id: 'int' = 0, - out: 'int' = 0, - ret: 'int' = 0, - origin: 'IPv4Address | str | bytes | int' = '127.0.0.1', - **kwargs: 'Any') -> 'Schema_TROption': - """Make IPv4 Traceroute (``TR``) option. - - Args: - kind: option type code - option: option data - id: ID number - out: outbound hop count - ret: return hop count - origin: originator IP address - **kwargs: arbitrary keyword arguments - - Returns: - Constructed option schema. - - Raises: - ProtocolError: If ``id``, ``out`` or ``ret`` does not fit in two - octets. - - """ - if option is not None: - id = option.id - out = option.outbound - ret = option['return'] - origin = option.originator - - return Schema_TROption( - type=kind, - length=12, - id=self._make_uint(id, bits=16, name='traceroute ID number'), - out=self._make_uint(out, bits=16, name='traceroute outbound hop count'), - ret=self._make_uint(ret, bits=16, name='traceroute return hop count'), - origin=origin, - ) - def _make_opt_rtralt(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_RTRALTOption]' = None, *, alert: 'Enum_RouterAlert | int' = Enum_RouterAlert.Aggregated_Reservation_Nesting_Level_0, **kwargs: 'Any') -> 'Schema_RTRALTOption': @@ -2234,85 +2284,35 @@ def _make_opt_rtralt(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_RTR alert=alert_val, ) - def _make_opt_qs(self, kind: 'Enum_OptionNumber', option: 'Optional[Data_QuickStartRequestOption | Data_QuickStartReportOption | Data_UnassignedOption]' = None, *, - func: 'Enum_QSFunction | int' = Enum_QSFunction.Quick_Start_Request, - rate: 'int' = 0, - ttl: 'timedelta | int' = 0, - unused: 'int' = 0, - nonce: 'int' = 0, - reserved: 'int' = 0, - **kwargs: 'Any') -> 'Schema_QSOption | Schema_UnassignedOption': - """Make IPv4 Quick-Start (``QS``) option. - - Args: - kind: option type code - option: option data - func: QS function type - rate: rate (in kbps), rounded down to the nearest rate the - four-bit rate code carries (:rfc:`4782#section-3.1`) - ttl: time to live (in seconds) - unused: the not-used fourth octet of a Report of Approved Rate - nonce: nonce value - reserved: reserved bits (``R``) after the nonce - **kwargs: arbitrary keyword arguments + def _read_ipv4_addr(self) -> 'IPv4Address': + """Read IP address. Returns: - Constructed option schema. An unassigned option schema if ``option`` - is a Quick-Start option parsed as unassigned, see :meth:`_read_opt_qs`, - or if ``func`` is neither of the assigned functions, in which case - the option takes the eight-octet layout of a Quick-Start Request. + Parsed IP address. - Raises: - ProtocolError: If ``func`` is a name, a :obj:`bool`, or a member - or code that does not fit in four bits, the rate code cannot - carry ``rate``, or ``ttl``, ``unused``, ``nonce`` or - ``reserved`` does not fit its field. + """ + _byte = self._read_fileng(4) + # _addr = '.'.join([str(_) for _ in _byte]) + # return _addr + return ipaddress.ip_address(_byte) # type: ignore[return-value] + + def _read_ipv4_opt_type(self, code: 'int') -> 'Data_OptionType': + """Read option type field. + + Arguments: + code: option kind value + + Returns: + Extracted IPv4 option type, as an object of the option flag (copied + flag), option class, and option number. """ - if isinstance(option, Data_UnassignedOption): - return cast('Schema_UnassignedOption', self._make_opt_unassigned(kind, option)) - if option is not None: - func = option.func - rate = option.rate - ttl = getattr(option, 'ttl', 0) - unused = getattr(option, 'unused', 0) - nonce = option.nonce - reserved = getattr(option, 'reserved', 0) - func_enum = self._make_index(func, Enum_QSFunction, bits=4, name='QS function') - rate_val = self._make_qs_rate(rate) - ttl_value = ttl if isinstance(ttl, int) else math.floor(ttl.total_seconds()) - nonce = self._make_uint(nonce, bits=30, name='Quick-Start nonce') - reserved = self._make_uint(reserved, bits=2, name='Quick-Start reserved') + oflg = bool(code >> 7) + ocls = Enum_OptionClass.get((code >> 5) & 0b11) + onum = code & 0b11111 - if func_enum == Enum_QSFunction.Quick_Start_Request: - return Schema_QuickStartRequestOption( - type=kind, - length=8, - flags={ - 'func': func_enum, - 'rate': rate_val, - }, - ttl=self._make_uint(ttl_value, bits=8, name='Quick-Start TTL'), - nonce={ - 'nonce': nonce, - 'reserved': reserved, - }, - ) - if func_enum == Enum_QSFunction.Report_of_Approved_Rate: - return Schema_QuickStartReportOption( - type=kind, - length=8, - flags={ - 'func': func_enum, - 'rate': rate_val, - }, - reserved=self._make_uint(unused, bits=8, name='Quick-Start not-used octet'), - nonce={ - 'nonce': nonce, - 'reserved': reserved, - }, - ) - func_val = int(func_enum) - ttl_value = self._make_uint(ttl_value, bits=8, name='Quick-Start TTL') - data = bytes([(func_val << 4) | rate_val, ttl_value]) + ((nonce << 2) | reserved).to_bytes(4, 'big') - return cast('Schema_UnassignedOption', self._make_opt_unassigned(kind, data=data)) + return Data_OptionType.from_dict({ + 'change': oflg, + 'class': ocls, + 'number': onum, + }) diff --git a/pcapkit/protocols/internet/ipv6/protocol.py b/pcapkit/protocols/internet/ipv6/protocol.py index 837b646d7d..a4c300ecf0 100644 --- a/pcapkit/protocols/internet/ipv6/protocol.py +++ b/pcapkit/protocols/internet/ipv6/protocol.py @@ -471,59 +471,6 @@ def _make_data(cls, data: 'Data_IPv6') -> 'dict[str, Any]': # type: ignore[over 'trailer': data.get('trailer', b''), } - @staticmethod - def _exthdr_key(alias: 'str') -> 'str': - """Key an extension header's info is written under in :class:`Data_IPv6`. - - Args: - alias: :attr:`~pcapkit.protocols.protocol.Protocol.alias` of - the extension header's parser. - - Returns: - The key; :meth:`str.lstrip` strips a character *set*, which every - parser's alias is chosen to suit. - - """ - return alias.lstrip('IPv6-').lower() - - @staticmethod - def _is_first_fragment(info: 'Mapping[str, Any]') -> 'bool': - """Whether a Fragment header's info is that of a first fragment. - - Args: - info: info of a Fragment header, as parsed or from :meth:`Info.to_dict - ` - - Returns: - :data:`True` at a Fragment Offset of zero. - - """ - return info.get('offset') == 0 - - def _read_ip_hextet(self) -> 'tuple[int, int, int]': - """Read the first four octets of IPv6. - - Returns: - Parsed data of those four octets: version number, traffic class - and flow label. - - """ - _htet = self._read_fileng(4).hex() - _vers = int(_htet[0], base=16) # version number (6) - _tcls = int(_htet[1:3], base=16) # traffic class - _flow = int(_htet[3:], base=16) # flow label - - return (_vers, _tcls, _flow) - - def _read_ip_addr(self) -> 'IPv6Address': - """Read IP address. - - Returns: - Parsed IP address. - - """ - return ipaddress.ip_address(self._read_fileng(16)) # type: ignore[return-value] - def _decode_next_layer(self, ipv6: 'Data_IPv6', proto: 'Optional[int]' = None, # type: ignore[override] # pylint: disable=arguments-differ,arguments-renamed length: 'Optional[int]' = None, *, packet: 'Optional[dict[str, Any]]' = None) -> 'Data_IPv6': # pylint: disable=arguments-differ """Decode next layer extractor. @@ -701,6 +648,59 @@ def _decode_next_layer(self, ipv6: 'Data_IPv6', proto: 'Optional[int]' = None, finally: frame_depth.reset(token) + @staticmethod + def _exthdr_key(alias: 'str') -> 'str': + """Key an extension header's info is written under in :class:`Data_IPv6`. + + Args: + alias: :attr:`~pcapkit.protocols.protocol.Protocol.alias` of + the extension header's parser. + + Returns: + The key; :meth:`str.lstrip` strips a character *set*, which every + parser's alias is chosen to suit. + + """ + return alias.lstrip('IPv6-').lower() + + @staticmethod + def _is_first_fragment(info: 'Mapping[str, Any]') -> 'bool': + """Whether a Fragment header's info is that of a first fragment. + + Args: + info: info of a Fragment header, as parsed or from :meth:`Info.to_dict + ` + + Returns: + :data:`True` at a Fragment Offset of zero. + + """ + return info.get('offset') == 0 + + def _read_ip_hextet(self) -> 'tuple[int, int, int]': + """Read the first four octets of IPv6. + + Returns: + Parsed data of those four octets: version number, traffic class + and flow label. + + """ + _htet = self._read_fileng(4).hex() + _vers = int(_htet[0], base=16) # version number (6) + _tcls = int(_htet[1:3], base=16) # traffic class + _flow = int(_htet[3:], base=16) # flow label + + return (_vers, _tcls, _flow) + + def _read_ip_addr(self) -> 'IPv6Address': + """Read IP address. + + Returns: + Parsed IP address. + + """ + return ipaddress.ip_address(self._read_fileng(16)) # type: ignore[return-value] + @beholder # type: ignore[arg-type] def _import_next_layer(self, proto: 'int', length: 'Optional[int]' = None, *, # pylint: disable=arguments-differ packet: 'Optional[dict[str, Any]]' = None, version: 'Literal[4, 6]' = 4, diff --git a/pcapkit/protocols/internet/ipv6_opts/protocol.py b/pcapkit/protocols/internet/ipv6_opts/protocol.py index bbe5222c54..c902226d0a 100644 --- a/pcapkit/protocols/internet/ipv6_opts/protocol.py +++ b/pcapkit/protocols/internet/ipv6_opts/protocol.py @@ -272,8 +272,8 @@ class IPv6_Opts(IPv6_Ext[Data_IPv6_Opts, Schema_IPv6_Opts], Enum_Option.CALIPSO: 'calipso', # [RFC 5570] Enum_Option.SMF_DPD: 'smf_dpd', # [RFC 6621] Enum_Option.PDM: 'pdm', # [RFC 8250] 10 - Enum_Option.Quick_Start: 'qs', # [RFC 4782][RFC Errata 2034] 6 Enum_Option.RPL_Option_0x23: 'rpl', # [RFC 9008] + Enum_Option.Quick_Start: 'qs', # [RFC 4782][RFC Errata 2034] 6 Enum_Option.RPL_Option_0x63: 'rpl', # [RFC 6553] Enum_Option.MPL_Option: 'mpl', # [RFC 7731] Enum_Option.ILNP_Nonce: 'ilnp', # [RFC 6744] @@ -559,56 +559,6 @@ def _make_data(cls, data: 'Data_IPv6_Opts') -> 'dict[str, Any]': # type: ignore 'len': data.length // 8 - 1 if data.get('__short_read__') is not None else None, } - def _read_opt_type(self, kind: 'int') -> 'tuple[int, bool]': - """Read option type field. - - Arguments: - kind (int): option kind value - - Returns: - Extracted IPv6-Opts option type field information (unknown option - action and change flag), see [:rfc:`8200#section-4.2`]. - - """ - bin_ = bin(kind)[2:].zfill(8) - return int(bin_[:2], base=2), bool(int(bin_[2], base=2)) - - @staticmethod - def _ipv6_opts_option_length(schema_len: 'int') -> 'int': - """Compute an IPv6-Opts option's whole-option length from its on-the-wire ``Opt Data Len``. - - Per :rfc:`8200#section-4.2`, an option's ``Opt Data Len`` field - (what each ``Schema_*Option.len`` here holds) counts *"the length of - the Option Data field of this option, in octets"* -- i.e. it - excludes the Option Type and Opt Data Len fields themselves, so the - whole option, which is what every ``_read_opt_*`` below reports back - as the parsed option's own ``.length``, is two octets more. This is - the ``+2``/``-2`` mismatch between the two (see ``Data_PadOption.length`` - vs. ``Schema_PadOption.len`` below). The read-side half lives in this - one helper so the arithmetic has a single home. Do NOT drop the - ``+ 2``. - - Section 4.2 is the citation because it defines the TLV option format and - contains the sentence quoted above. IPv6-Opts itself is the Destination - Options header of :rfc:`8200#section-4.6`, which carries those TLVs but - says nothing about their internal length arithmetic. Section 4.3 is the - Hop-by-Hop Options header, a different header from the one this class - implements. - - Note that only the *stored-length* read-side call sites are - collected here -- most ``_make_opt_*`` methods recompute the wire - ``Opt Data Len`` from ``len(value)`` rather than reading a parsed - ``.length`` back, so they have nothing to unify against this helper. - - Args: - schema_len: raw ``Opt Data Len`` field value, as read off the wire. - - Returns: - Whole-option length, in octets, including the Type and Opt Data Len fields. - - """ - return schema_len + 2 - def _read_ipv6_opts(self, length: 'int') -> 'Option': """Read IPv6-Opts options. @@ -1127,6 +1077,57 @@ def _read_opt_pdm(self, schema: 'Schema_PDMOption', *, options: 'Option') -> 'Da ) return opt + def _read_opt_rpl(self, schema: 'Schema_RPLOption', *, options: 'Option') -> 'Data_RPLOption': # pylint: disable=unused-argument + """Read IPv6-Opts Routing Protocol for Low-Power and Lossy Networks (RPL) option. + + Structure of IPv6-Opts RPL option [:rfc:`6553`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Opt Data Len | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |O|R|F|0|0|0|0|0| RPLInstanceID | SenderRank | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | (sub-TLVs) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + The sub-TLVs, which :rfc:`6553#section-3` says MUST be skipped when + unrecognised, are kept as raw octets in ``sub_tlvs``. + + Args: + schema: parsed parameter schema + options: extracted IPv6-Opts options + + Returns: + Parsed option data. + + Raises: + ProtocolError: If ``schema.len`` is less than ``4``. + + """ + if schema.len < 4: + raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format') + + opt = Data_RPLOption( + type=schema.type, + action=Enum_OptionAction.get(schema.type >> 6), + change=bool(schema.type & 0b00100000), + length=self._ipv6_opts_option_length(schema.len), + flags=Data_RPLFlags( + down=bool(schema.flags['down']), + rank_err=bool(schema.flags['rank_err']), + fwd_err=bool(schema.flags['fwd_err']), + reserved=schema.flags['reserved'], + ), + id=schema.id, + rank=schema.rank, + sub_tlvs=schema.sub_tlvs, + ) + return opt + def _read_opt_qs(self, schema: 'Schema_QuickStartOption', *, options: 'Option') -> 'Data_QuickStartOption': # pylint: disable=unused-argument """Read IPv6-Opts Quick Start option. @@ -1218,57 +1219,6 @@ def _read_opt_qs(self, schema: 'Schema_QuickStartOption', *, options: 'Option') raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] unknown QS function: {func}') return opt - def _read_opt_rpl(self, schema: 'Schema_RPLOption', *, options: 'Option') -> 'Data_RPLOption': # pylint: disable=unused-argument - """Read IPv6-Opts Routing Protocol for Low-Power and Lossy Networks (RPL) option. - - Structure of IPv6-Opts RPL option [:rfc:`6553`]: - - .. code-block:: text - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option Type | Opt Data Len | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |O|R|F|0|0|0|0|0| RPLInstanceID | SenderRank | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | (sub-TLVs) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - The sub-TLVs, which :rfc:`6553#section-3` says MUST be skipped when - unrecognised, are kept as raw octets in ``sub_tlvs``. - - Args: - schema: parsed parameter schema - options: extracted IPv6-Opts options - - Returns: - Parsed option data. - - Raises: - ProtocolError: If ``schema.len`` is less than ``4``. - - """ - if schema.len < 4: - raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format') - - opt = Data_RPLOption( - type=schema.type, - action=Enum_OptionAction.get(schema.type >> 6), - change=bool(schema.type & 0b00100000), - length=self._ipv6_opts_option_length(schema.len), - flags=Data_RPLFlags( - down=bool(schema.flags['down']), - rank_err=bool(schema.flags['rank_err']), - fwd_err=bool(schema.flags['fwd_err']), - reserved=schema.flags['reserved'], - ), - id=schema.id, - rank=schema.rank, - sub_tlvs=schema.sub_tlvs, - ) - return opt - def _read_opt_mpl(self, schema: 'Schema_MPLOption', *, options: 'Option') -> 'Data_MPLOption': # pylint: disable=unused-argument """Read IPv6-Opts Multicast Protocol for Low-Power and Lossy Networks (MPL) option. @@ -1533,45 +1483,6 @@ def _read_opt_ip_dff(self, schema: 'Schema_IPDFFOption', *, options: 'Option') - ) return opt - def _make_pad_options(self, offset: 'int') -> 'tuple[list[Schema_PadOption], int]': - """Make the padding options needed to align the header to 8 octets. - - Args: - offset: Number of octets emitted so far, **counting the two octets - of the fixed header** (``Next Header`` and ``Hdr Ext Len``) as - well as every option already in the list. - - Returns: - Tuple of the padding option schemas and the number of octets they - occupy. - - Note: - It is the *whole* extension header, fixed part included, that has to - be a multiple of 8 octets [:rfc:`8200#section-4.6`] -- which is why - the alignment is computed from ``offset`` rather than from the - length of the option that has just been emitted. Aligning each - option to 8 octets on its own leaves the options area a multiple of - 8 octets long, whereas :meth:`read` sizes it as - ``hdr_ext_len * 8 + 6`` -- two short of a multiple of 8 -- so the - header would declare 2 octets fewer than it carried. - - A ``PadN`` option spends two octets on its own type and - ``Opt Data Len`` fields before any padding data, so occupying - ``pad_len`` octets means an ``Opt Data Len`` of ``pad_len - 2``. - One octet is padded with a ``Pad1`` option instead, that being the - only form which can occupy a single octet. Two octets are one - ``PadN`` with an ``Opt Data Len`` of ``0``, not two ``Pad1``: - :rfc:`8200#section-4.2` says a ``PadN`` should be used rather than - multiple ``Pad1`` options. - - """ - pad_len = pad_length(offset, 8) - if pad_len == 0: - return [], 0 - if pad_len == 1: - return [self._make_opt_pad(Enum_Option.Pad1, length=0)], pad_len # type: ignore[arg-type] - return [self._make_opt_pad(Enum_Option.PadN, length=pad_len - 2)], pad_len # type: ignore[arg-type] - def _make_opt_none(self, code: 'Enum_Option', opt: 'Optional[Data_UnassignedOption]' = None, *, data: 'bytes' = b'', **kwargs: 'Any') -> 'Schema_UnassignedOption': @@ -1959,6 +1870,66 @@ def _make_opt_pdm(self, code: 'Enum_Option', opt: 'Optional[Data_PDMOption]' = N name='PDM delta time last sent'), ) + def _make_opt_rpl(self, code: 'Enum_Option', opt: 'Optional[Data_RPLOption]' = None, *, + down: 'bool' = False, + rank_err: 'bool' = False, + fwd_err: 'bool' = False, + id: 'int' = 0, + rank: 'int' = 0, + reserved: 'int' = 0, + sub_tlvs: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_RPLOption': + """Make IPv6-Opts RPL option. + + Args: + code: option type value + opt: option data + down: down flag + rank_err: rank error flag + fwd_err: forwarding error flag + id: RPL instance ID + rank: sender rank + reserved: reserved flag bits, i.e. the low five bits of the flags octet + sub_tlvs: sub-TLVs, as raw octets + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + Raises: + ProtocolError: If ``reserved``, ``id`` or ``rank`` does not fit its + field, or ``sub_tlvs`` does not fit in the option. + + """ + if opt is not None: + down = opt.flags.down + rank_err = opt.flags.rank_err + fwd_err = opt.flags.fwd_err + reserved = opt.flags.reserved + id = opt.id + rank = opt.rank + sub_tlvs = opt.sub_tlvs + + reserved = self._make_uint(reserved, bits=5, name='RPL reserved flags') + id = self._make_uint(id, bits=8, name='RPL instance ID') + rank = self._make_uint(rank, bits=16, name='RPL sender rank') + if len(sub_tlvs) > 251: + raise ProtocolError(f'{self.alias}: [OptNo {code}] too long RPL sub-TLVs: {len(sub_tlvs)}') + + return Schema_RPLOption( + type=code, + len=4 + len(sub_tlvs), + flags={ + 'down': down, + 'rank_err': rank_err, + 'fwd_err': fwd_err, + 'reserved': reserved, + }, + id=id, + rank=rank, + sub_tlvs=sub_tlvs, + ) + def _make_opt_qs(self, code: 'Enum_Option', opt: 'Optional[Data_QuickStartOption | Data_UnassignedOption]' = None, *, # pylint: disable=line-too-long func: 'Enum_QSFunction | int' = Enum_QSFunction.Quick_Start_Request, rate: 'int' = 0, @@ -2043,66 +2014,6 @@ def _make_opt_qs(self, code: 'Enum_Option', opt: 'Optional[Data_QuickStartOption data = bytes([(func_val << 4) | rate_val, ttl_value]) + ((nonce << 2) | reserved).to_bytes(4, 'big') return self._make_opt_none(code, data=data) - def _make_opt_rpl(self, code: 'Enum_Option', opt: 'Optional[Data_RPLOption]' = None, *, - down: 'bool' = False, - rank_err: 'bool' = False, - fwd_err: 'bool' = False, - id: 'int' = 0, - rank: 'int' = 0, - reserved: 'int' = 0, - sub_tlvs: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_RPLOption': - """Make IPv6-Opts RPL option. - - Args: - code: option type value - opt: option data - down: down flag - rank_err: rank error flag - fwd_err: forwarding error flag - id: RPL instance ID - rank: sender rank - reserved: reserved flag bits, i.e. the low five bits of the flags octet - sub_tlvs: sub-TLVs, as raw octets - **kwargs: arbitrary keyword arguments - - Returns: - Constructed option schema. - - Raises: - ProtocolError: If ``reserved``, ``id`` or ``rank`` does not fit its - field, or ``sub_tlvs`` does not fit in the option. - - """ - if opt is not None: - down = opt.flags.down - rank_err = opt.flags.rank_err - fwd_err = opt.flags.fwd_err - reserved = opt.flags.reserved - id = opt.id - rank = opt.rank - sub_tlvs = opt.sub_tlvs - - reserved = self._make_uint(reserved, bits=5, name='RPL reserved flags') - id = self._make_uint(id, bits=8, name='RPL instance ID') - rank = self._make_uint(rank, bits=16, name='RPL sender rank') - if len(sub_tlvs) > 251: - raise ProtocolError(f'{self.alias}: [OptNo {code}] too long RPL sub-TLVs: {len(sub_tlvs)}') - - return Schema_RPLOption( - type=code, - len=4 + len(sub_tlvs), - flags={ - 'down': down, - 'rank_err': rank_err, - 'fwd_err': fwd_err, - 'reserved': reserved, - }, - id=id, - rank=rank, - sub_tlvs=sub_tlvs, - ) - def _make_opt_mpl(self, code: 'Enum_Option', opt: 'Optional[Data_MPLOption]' = None, *, seed_type: 'Optional[Enum_SeedID | int]' = None, max: 'bool' = False, @@ -2355,3 +2266,92 @@ def _make_opt_ip_dff(self, code: 'Enum_Option', opt: 'Optional[Data_IPDFFOption] }, seq=self._make_uint(seq, bits=16, name='DFF sequence number'), ) + + def _read_opt_type(self, kind: 'int') -> 'tuple[int, bool]': + """Read option type field. + + Arguments: + kind (int): option kind value + + Returns: + Extracted IPv6-Opts option type field information (unknown option + action and change flag), see [:rfc:`8200#section-4.2`]. + + """ + bin_ = bin(kind)[2:].zfill(8) + return int(bin_[:2], base=2), bool(int(bin_[2], base=2)) + + @staticmethod + def _ipv6_opts_option_length(schema_len: 'int') -> 'int': + """Compute an IPv6-Opts option's whole-option length from its on-the-wire ``Opt Data Len``. + + Per :rfc:`8200#section-4.2`, an option's ``Opt Data Len`` field + (what each ``Schema_*Option.len`` here holds) counts *"the length of + the Option Data field of this option, in octets"* -- i.e. it + excludes the Option Type and Opt Data Len fields themselves, so the + whole option, which is what every ``_read_opt_*`` below reports back + as the parsed option's own ``.length``, is two octets more. This is + the ``+2``/``-2`` mismatch between the two (see ``Data_PadOption.length`` + vs. ``Schema_PadOption.len`` below). The read-side half lives in this + one helper so the arithmetic has a single home. Do NOT drop the + ``+ 2``. + + Section 4.2 is the citation because it defines the TLV option format and + contains the sentence quoted above. IPv6-Opts itself is the Destination + Options header of :rfc:`8200#section-4.6`, which carries those TLVs but + says nothing about their internal length arithmetic. Section 4.3 is the + Hop-by-Hop Options header, a different header from the one this class + implements. + + Note that only the *stored-length* read-side call sites are + collected here -- most ``_make_opt_*`` methods recompute the wire + ``Opt Data Len`` from ``len(value)`` rather than reading a parsed + ``.length`` back, so they have nothing to unify against this helper. + + Args: + schema_len: raw ``Opt Data Len`` field value, as read off the wire. + + Returns: + Whole-option length, in octets, including the Type and Opt Data Len fields. + + """ + return schema_len + 2 + + def _make_pad_options(self, offset: 'int') -> 'tuple[list[Schema_PadOption], int]': + """Make the padding options needed to align the header to 8 octets. + + Args: + offset: Number of octets emitted so far, **counting the two octets + of the fixed header** (``Next Header`` and ``Hdr Ext Len``) as + well as every option already in the list. + + Returns: + Tuple of the padding option schemas and the number of octets they + occupy. + + Note: + It is the *whole* extension header, fixed part included, that has to + be a multiple of 8 octets [:rfc:`8200#section-4.6`] -- which is why + the alignment is computed from ``offset`` rather than from the + length of the option that has just been emitted. Aligning each + option to 8 octets on its own leaves the options area a multiple of + 8 octets long, whereas :meth:`read` sizes it as + ``hdr_ext_len * 8 + 6`` -- two short of a multiple of 8 -- so the + header would declare 2 octets fewer than it carried. + + A ``PadN`` option spends two octets on its own type and + ``Opt Data Len`` fields before any padding data, so occupying + ``pad_len`` octets means an ``Opt Data Len`` of ``pad_len - 2``. + One octet is padded with a ``Pad1`` option instead, that being the + only form which can occupy a single octet. Two octets are one + ``PadN`` with an ``Opt Data Len`` of ``0``, not two ``Pad1``: + :rfc:`8200#section-4.2` says a ``PadN`` should be used rather than + multiple ``Pad1`` options. + + """ + pad_len = pad_length(offset, 8) + if pad_len == 0: + return [], 0 + if pad_len == 1: + return [self._make_opt_pad(Enum_Option.Pad1, length=0)], pad_len # type: ignore[arg-type] + return [self._make_opt_pad(Enum_Option.PadN, length=pad_len - 2)], pad_len # type: ignore[arg-type] diff --git a/pcapkit/protocols/internet/ipx/protocol.py b/pcapkit/protocols/internet/ipx/protocol.py index 7eb0179f19..9c2176eecb 100644 --- a/pcapkit/protocols/internet/ipx/protocol.py +++ b/pcapkit/protocols/internet/ipx/protocol.py @@ -326,20 +326,6 @@ def _make_data(cls, data: 'Data_IPX') -> 'dict[str, Any]': # type: ignore[overr 'trailer': data.get('trailer', b''), } - def _next_exthdr_depth(self, proto: 'Optional[int]', extension: 'bool' = False) -> 'int': # pylint: disable=unused-argument - """Place the next layer in its chain of IPv6 extension headers. - - Arguments: - proto: next layer IPX Packet Type - extension: if the next layer is parsed as an extension header - - Returns: - Always ``0``: an IPX Packet Type is not an IP protocol number, so it - never names an IPv6 extension header (:issue:`2169`). - - """ - return 0 - def _read_ipx_address(self, addr: 'bytes') -> 'Data_Address': """Read IPX address field. @@ -387,3 +373,17 @@ def _make_ipx_address(addr: 'Data_Address') -> 'bytes': return (bytes.fromhex(addr.network.replace(':', '')) + bytes.fromhex(addr.node.replace('-', '')) + int(addr.socket).to_bytes(2, 'big', signed=False)) + + def _next_exthdr_depth(self, proto: 'Optional[int]', extension: 'bool' = False) -> 'int': # pylint: disable=unused-argument + """Place the next layer in its chain of IPv6 extension headers. + + Arguments: + proto: next layer IPX Packet Type + extension: if the next layer is parsed as an extension header + + Returns: + Always ``0``: an IPX Packet Type is not an IP protocol number, so it + never names an IPv6 extension header (:issue:`2169`). + + """ + return 0 diff --git a/pcapkit/protocols/internet/mh/protocol.py b/pcapkit/protocols/internet/mh/protocol.py index 11532c2100..1882d3df4d 100644 --- a/pcapkit/protocols/internet/mh/protocol.py +++ b/pcapkit/protocols/internet/mh/protocol.py @@ -1212,10 +1212,10 @@ class MH(IPv6_Ext[Data_MH, Schema_MH], Enum_Packet.Fast_Binding_Acknowledgment: 'fback', Enum_Packet.Fast_Neighbor_Advertisement: 'fna', Enum_Packet.Experimental_Mobility_Header: 'emh', - Enum_Packet.Handover_Initiate_Message: 'hi', - Enum_Packet.Handover_Acknowledge_Message: 'hack', Enum_Packet.Home_Agent_Switch_Message: 'has', Enum_Packet.Heartbeat_Message: 'hb', + Enum_Packet.Handover_Initiate_Message: 'hi', + Enum_Packet.Handover_Acknowledge_Message: 'hack', Enum_Packet.Binding_Revocation_Message: 'brm', Enum_Packet.Localized_Routing_Initiation: 'lri', Enum_Packet.Localized_Routing_Acknowledgment: 'lra', @@ -1251,12 +1251,11 @@ class MH(IPv6_Ext[Data_MH, Schema_MH], Enum_Option.Permanent_Home_Keygen_Token: 'phkt', Enum_Option.Care_of_Test_Init: 'ct_init', Enum_Option.Care_of_Test: 'ct', - Enum_Option.Experimental_Mobility_Option: 'exp', - Enum_Option.Binding_Authorization_Data_for_FMIPv6: 'badf', - Enum_Option.Mobility_Header_IPv6_Address_Prefix: 'ipv6_ap', Enum_Option.DNS_UPDATE_TYPE: 'dns', + Enum_Option.Experimental_Mobility_Option: 'exp', Enum_Option.Vendor_Specific_Mobility_Option: 'vendor', Enum_Option.Service_Selection_Mobility_Option: 'service', + Enum_Option.Binding_Authorization_Data_for_FMIPv6: 'badf', Enum_Option.Home_Network_Prefix_Option: 'hnp', Enum_Option.Handoff_Indicator_Option: 'hi', Enum_Option.Access_Technology_Type_Option: 'att', @@ -1269,6 +1268,7 @@ class MH(IPv6_Ext[Data_MH, Schema_MH], Enum_Option.NAT_Detection: 'nat', Enum_Option.IPv4_Care_of_Address: 'ipv4_coa', Enum_Option.GRE_Key_Option: 'gre', + Enum_Option.Mobility_Header_IPv6_Address_Prefix: 'ipv6_ap', Enum_Option.Binding_Identifier: 'bid', Enum_Option.IPv4_Home_Address_Request: 'ipv4_hoa_req', Enum_Option.IPv4_Home_Address_Reply: 'ipv4_hoa_rep', @@ -1708,30 +1708,6 @@ def _make_data(cls, data: 'Data_MH') -> 'dict[str, Any]': # type: ignore[overri 'payload': cls._make_payload(data), } - @staticmethod - def _mh_message_length(header_len: 'int') -> 'int': - """Compute the total on-the-wire MH header length for a given ``Header Len``. - - Per :rfc:`6275#section-6.1.1`, ``Header Len`` is *"the length of the - Mobility Header, in units of 8 octets, excluding the first 8 - octets"* -- i.e. the total header is ``8 + 8 * header_len`` octets, - or equivalently ``(header_len + 1) * 8``. Every ``_read_msg_*`` - below reports this value as the parsed message's own ``.length``, - which :meth:`~pcapkit.protocols.internet.mh.protocol.MH.read` subtracts from the - outer packet length to find the next layer's length. It is the inverse - of :meth:`make`'s ``(len(data_val) + 6) // 8 - 1``. Do NOT drop the - ``+ 1``: the units either side of it differ (octets vs. 8-octet - units), and dropping the offset silently reinterprets the field. - - Args: - header_len: raw ``Header Len`` field value, as read off the wire. - - Returns: - Total length, in octets, of the on-the-wire MH header. - - """ - return (header_len + 1) * 8 - def _pad_mh_message(self, data: 'Schema_Packet | bytes') -> 'Schema_Packet | bytes': """Pad an MH message so that the Mobility Header aligns to 8 octets. @@ -2386,31 +2362,40 @@ def _read_msg_emh(self, schema: 'Schema_ExperimentalMessage', *, ) return data - def _read_msg_hi(self, schema: 'Schema_HandoverInitiateMessage', *, - header: 'Schema_MH') -> 'Data_HandoverInitiateMessage': - """Read MH handover initiate (HI) message type. + def _read_msg_has(self, schema: 'Schema_HomeAgentSwitchMessage', *, + header: 'Schema_MH') -> 'Data_HomeAgentSwitchMessage': + """Read MH home agent switch (HAS) message type. - Structure of MH Handover Initiate Message [:rfc:`5568#section-6.2.1.1`, - :rfc:`5949#section-6.1.1`]: + Structure of MH Home Agent Switch Message [:rfc:`5142#section-4`]: .. code-block:: text +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Sequence # | + |# of Addresses | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + . . + . Home Agent Addresses . + . . + + + + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |S|U|P|F| Resv | Code | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ . | | + + + + . . . Mobility Options . . . + + + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Note: - :rfc:`5568` defines only the ``S`` and ``U`` flags; the ``P`` and - ``F`` flags, and code values ``2`` and ``3``, are added by - :rfc:`5949#section-6.1.1`. All of these flags are registered in the IANA - *Handover Initiate Flags* registry, hence all are parsed here. + The ``# of Addresses`` field is not carried into the data model, + since it is exactly ``len(addresses)`` and a second copy of a length + is a second thing to keep in step. An empty list is meaningful rather + than degenerate: :rfc:`5142#section-4` uses a count of zero to tell + the mobile node to run home agent discovery instead. Args: schema: Parsed message type schema. @@ -2420,49 +2405,46 @@ def _read_msg_hi(self, schema: 'Schema_HandoverInitiateMessage', *, Parsed message type data. """ - data = Data_HandoverInitiateMessage( + if schema.count != len(schema.addresses): + raise ProtocolError(f'{self.alias}: [Type {header.type}] invalid format') + + data = Data_HomeAgentSwitchMessage( next=header.next, length=self._mh_message_length(header.length), type=header.type, reserved=header.reserved, chksum=header.chksum, - seq=schema.seq, - assign=bool(schema.flags['S']), - buffer=bool(schema.flags['U']), - proxy=bool(schema.flags['P']), - forward=bool(schema.flags['F']), - flags_reserved=schema.flags['reserved'], - code=schema.code, + msg_reserved=schema.reserved, + addresses=tuple(schema.addresses), options=self._read_mh_options(schema.options), ) return data - def _read_msg_hack(self, schema: 'Schema_HandoverAcknowledgeMessage', *, - header: 'Schema_MH') -> 'Data_HandoverAcknowledgeMessage': - """Read MH handover acknowledge (HAck) message type. + def _read_msg_hb(self, schema: 'Schema_HeartbeatMessage', *, + header: 'Schema_MH') -> 'Data_HeartbeatMessage': + """Read MH heartbeat message type. - Structure of MH Handover Acknowledge Message [:rfc:`5568#section-6.2.1.2`, - :rfc:`5949#section-6.1.2`]: + Structure of MH Heartbeat Message [:rfc:`5847#section-3.3`]: .. code-block:: text +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Sequence # | + | Reserved |U|R| + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Sequence Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |U|P|F| Reserved| Code | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ . | | + . . . Mobility Options . . . | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Note: - :rfc:`5568` reserves the whole first octet of the message data; the - ``U``, ``P`` and ``F`` flags carved out of it, and code values ``5``, - ``6``, ``131`` and ``132``, are added by :rfc:`5949#section-6.1.2`. - All of these flags are registered in the IANA *Handover Acknowledge - Flags* registry, hence all are parsed here. + The ``U`` and ``R`` flags are the two **least** significant bits of + the first half-word, not the most significant ones the other mobility + messages put their flags in [:rfc:`5847#section-3.3`]. The sequence + number is 32 bits wide, also unlike the 16-bit ones elsewhere. Args: schema: Parsed message type schema. @@ -2472,56 +2454,45 @@ def _read_msg_hack(self, schema: 'Schema_HandoverAcknowledgeMessage', *, Parsed message type data. """ - data = Data_HandoverAcknowledgeMessage( + data = Data_HeartbeatMessage( next=header.next, length=self._mh_message_length(header.length), type=header.type, reserved=header.reserved, chksum=header.chksum, - seq=schema.seq, - buffer=bool(schema.flags['U']), - proxy=bool(schema.flags['P']), - forward=bool(schema.flags['F']), + unsolicited=bool(schema.flags['U']), + response=bool(schema.flags['R']), flags_reserved=schema.flags['reserved'], - code=schema.code, + seq=schema.seq, options=self._read_mh_options(schema.options), ) return data - def _read_msg_has(self, schema: 'Schema_HomeAgentSwitchMessage', *, - header: 'Schema_MH') -> 'Data_HomeAgentSwitchMessage': - """Read MH home agent switch (HAS) message type. + def _read_msg_hi(self, schema: 'Schema_HandoverInitiateMessage', *, + header: 'Schema_MH') -> 'Data_HandoverInitiateMessage': + """Read MH handover initiate (HI) message type. - Structure of MH Home Agent Switch Message [:rfc:`5142#section-4`]: + Structure of MH Handover Initiate Message [:rfc:`5568#section-6.2.1.1`, + :rfc:`5949#section-6.1.1`]: .. code-block:: text +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |# of Addresses | Reserved | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - + + - . . - . Home Agent Addresses . - . . - + + - | | + | Sequence # | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |S|U|P|F| Resv | Code | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ . | | - + + - . . . Mobility Options . . . - + + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Note: - The ``# of Addresses`` field is not carried into the data model, - since it is exactly ``len(addresses)`` and a second copy of a length - is a second thing to keep in step. An empty list is meaningful rather - than degenerate: :rfc:`5142#section-4` uses a count of zero to tell - the mobile node to run home agent discovery instead. + :rfc:`5568` defines only the ``S`` and ``U`` flags; the ``P`` and + ``F`` flags, and code values ``2`` and ``3``, are added by + :rfc:`5949#section-6.1.1`. All of these flags are registered in the IANA + *Handover Initiate Flags* registry, hence all are parsed here. Args: schema: Parsed message type schema. @@ -2531,46 +2502,49 @@ def _read_msg_has(self, schema: 'Schema_HomeAgentSwitchMessage', *, Parsed message type data. """ - if schema.count != len(schema.addresses): - raise ProtocolError(f'{self.alias}: [Type {header.type}] invalid format') - - data = Data_HomeAgentSwitchMessage( + data = Data_HandoverInitiateMessage( next=header.next, length=self._mh_message_length(header.length), type=header.type, reserved=header.reserved, chksum=header.chksum, - msg_reserved=schema.reserved, - addresses=tuple(schema.addresses), + seq=schema.seq, + assign=bool(schema.flags['S']), + buffer=bool(schema.flags['U']), + proxy=bool(schema.flags['P']), + forward=bool(schema.flags['F']), + flags_reserved=schema.flags['reserved'], + code=schema.code, options=self._read_mh_options(schema.options), ) return data - def _read_msg_hb(self, schema: 'Schema_HeartbeatMessage', *, - header: 'Schema_MH') -> 'Data_HeartbeatMessage': - """Read MH heartbeat message type. + def _read_msg_hack(self, schema: 'Schema_HandoverAcknowledgeMessage', *, + header: 'Schema_MH') -> 'Data_HandoverAcknowledgeMessage': + """Read MH handover acknowledge (HAck) message type. - Structure of MH Heartbeat Message [:rfc:`5847#section-3.3`]: + Structure of MH Handover Acknowledge Message [:rfc:`5568#section-6.2.1.2`, + :rfc:`5949#section-6.1.2`]: .. code-block:: text +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Reserved |U|R| - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Sequence Number | + | Sequence # | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |U|P|F| Reserved| Code | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ . | | - . . . Mobility Options . . . | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Note: - The ``U`` and ``R`` flags are the two **least** significant bits of - the first half-word, not the most significant ones the other mobility - messages put their flags in [:rfc:`5847#section-3.3`]. The sequence - number is 32 bits wide, also unlike the 16-bit ones elsewhere. + :rfc:`5568` reserves the whole first octet of the message data; the + ``U``, ``P`` and ``F`` flags carved out of it, and code values ``5``, + ``6``, ``131`` and ``132``, are added by :rfc:`5949#section-6.1.2`. + All of these flags are registered in the IANA *Handover Acknowledge + Flags* registry, hence all are parsed here. Args: schema: Parsed message type schema. @@ -2580,16 +2554,18 @@ def _read_msg_hb(self, schema: 'Schema_HeartbeatMessage', *, Parsed message type data. """ - data = Data_HeartbeatMessage( + data = Data_HandoverAcknowledgeMessage( next=header.next, length=self._mh_message_length(header.length), type=header.type, reserved=header.reserved, chksum=header.chksum, - unsolicited=bool(schema.flags['U']), - response=bool(schema.flags['R']), - flags_reserved=schema.flags['reserved'], seq=schema.seq, + buffer=bool(schema.flags['U']), + proxy=bool(schema.flags['P']), + forward=bool(schema.flags['F']), + flags_reserved=schema.flags['reserved'], + code=schema.code, options=self._read_mh_options(schema.options), ) return data @@ -3001,153 +2977,32 @@ def _read_msg_sr(self, schema: 'Schema_SubscriptionResponseMessage', *, ) return data - @staticmethod - def _mh_option_length(schema_length: 'int') -> 'int': - """Compute an MH option's whole-option length from its on-the-wire ``Option Length``. - - Per :rfc:`6275#section-6.2`, an MH option's ``Option Length`` field - (what each ``Schema_*Option.length`` here holds) counts *"the length - of the option, in octets, excluding the Option Type and Option - Length fields"* -- so the whole option, which is what every - ``_read_opt_*`` below reports back as the parsed option's own - ``.length``, is two octets more. The read-side call sites share this - helper so the arithmetic lives in one place. The exceptions are the - options whose ``Option Length`` is not that octet count: a ``Pad1`` - option, which has no ``Option Length`` field at all (see the ``Note:`` - on :meth:`_read_opt_pad` below), the BADF option, whose length leaves - out its SPI (see :meth:`_read_opt_badf`), and the multicast mobility - and acknowledgement options, whose length counts 32-bit words (see - :meth:`_read_opt_mcast`). Do NOT drop the ``+ 2``. - - The ``+ 2`` is specific to an :rfc:`6275#section-6.2` mobility - option, whose Option Type and Option Length are one octet each. It - is **not** a universal contract for everything this class parses: - a CGA extension's Extension Type and Extension Data Length are two - octets each [:rfc:`4581#section-2`], so those readers use - :meth:`_mh_extension_length` instead. Reusing this helper for them - would report every parsed CGA extension two octets short. - - Note that only the *stored-length* read-side call sites are - collected here -- most ``_make_opt_*`` methods recompute the wire - ``Option Length`` from ``len(value)`` rather than reading a parsed - ``.length`` back, so they have nothing to unify against this helper. + def _read_opt_none(self, schema: 'Schema_UnassignedOption', *, + options: 'Option') -> 'Data_UnassignedOption': + """Read MH unassigned option. Args: - schema_length: raw ``Option Length`` field value, as read off the wire. + schema: Parsed option schema. + options: Parsed MH options. Returns: - Whole-option length, in octets, including the Option Type and - Option Length fields. + Constructed option data. """ - return schema_length + 2 + data = Data_UnassignedOption( + type=schema.type, + length=self._mh_option_length(schema.length), + data=schema.data, + ) + return data - def _read_mh_options(self, options_schema: 'list[Schema_Option]') -> 'Option': - """Read MH options. + def _read_opt_pad(self, schema: 'Schema_PadOption', *, + options: 'Option') -> 'Data_PadOption': + """Read MH padding option. - Structure of MH option [:rfc:`6275`]: + Structure of MH padding option [:rfc:`6275`]: - .. code-block:: text - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option Type | Option Length | Option Data... - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - Args: - options_schema: Parsed MH options. - - Returns: - Parsed MH options data. - - Raises: - ProtocolError: If an option read by a ``_read_opt_*`` method has fields - that do not take exactly the octets its ``Option Length`` declares - [:rfc:`6275#section-6.2.1`]. A parser registered as a callable - reports its own length, and is not checked. Or if the capture cut - an option short and its parser refuses the zeros it lacks, or its - maker does not write the octets captured back. - - .. versionchanged:: 1.6.0 - - * Such an option is refused, rather than read from octets outside it - and rebuilt with the length its fields give (:issue:`2684`). - * An option the capture cut short is read, its missing octets as - zeros, if its maker writes the octets captured back - (:issue:`2694`). - - """ - options = OrderedMultiDict() # type: Option - - for schema in options_schema: - type = schema.type - name = self._lookup_registry(self.__option__, type) - - if isinstance(name, str): - meth_name = f'_read_opt_{name}' - meth = cast('OptionParser', - getattr(self, meth_name, self._read_opt_none)) - maker = cast('OptionConstructor', - getattr(self, f'_make_opt_{name}', self._make_opt_none)) - else: - meth, maker = name - - # NOTE: The capture cut this option short, as its short read records, so - # the octets it lacks read as zeros and it is short of its length. It - # stands if its parser takes those zeros and its maker writes the octets - # captured back (#2694); otherwise it is refused, as an option running - # past the data is (#1325). - if '__short_read__' in schema.__dict__: - try: - with CaughtByCaller(BaseError): - data = meth(schema, options=options) - made = maker(type, data).pack()[:len(schema)] - except BaseError as error: - raise ProtocolError(f'{self.alias}: [OptNo {type}] option cut short: {error}') from error - if made != bytes(schema): - raise ProtocolError(f'{self.alias}: [OptNo {type}] option cut short does not ' - 'rebuild as captured') - else: - data = meth(schema, options=options) - - # NOTE: Fields are read at their own widths, so a length they do not - # fill exactly reads octets not the option's, and rebuilds as another - # (#2684). - if isinstance(name, str) and len(schema) != data.length: - raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid option length: {schema.length}') - - # record option data - options.add(type, data) - - return options - - def _read_opt_none(self, schema: 'Schema_UnassignedOption', *, - options: 'Option') -> 'Data_UnassignedOption': - """Read MH unassigned option. - - Args: - schema: Parsed option schema. - options: Parsed MH options. - - Returns: - Constructed option data. - - """ - data = Data_UnassignedOption( - type=schema.type, - length=self._mh_option_length(schema.length), - data=schema.data, - ) - return data - - def _read_opt_pad(self, schema: 'Schema_PadOption', *, - options: 'Option') -> 'Data_PadOption': - """Read MH padding option. - - Structure of MH padding option [:rfc:`6275`]: - - * ``Pad1`` option: + * ``Pad1`` option: .. code-block:: text @@ -3760,6 +3615,54 @@ def _read_opt_ct(self, schema: 'Schema_CareofTestOption', *, ) return data + def _read_opt_dns(self, schema: 'Schema_DNSUpdateOption', *, + options: 'Option') -> 'Data_DNSUpdateOption': + """Read MH DNS-UPDATE-TYPE option. + + Structure of MH DNS-UPDATE-TYPE option [:rfc:`5026#section-8.1`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Option Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Status |R| Reserved | MN identity (FQDN) ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The status field draws from the *Status Codes (DNS Update Mobility + Option)* registry that :rfc:`5026#section-10` creates, **not** from + the general mobility status codes, which is why it is typed + :class:`~pcapkit.const.mh.dns_status_code.DNSStatusCode`. + + The identity is kept as :obj:`bytes` rather than decoded to + :class:`str`, because the RFC says only "FQDN format" without saying + whether that means the presentation form or the length-prefixed + label form of :rfc:`1035`. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length < 2: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_DNSUpdateOption( + type=schema.type, + length=self._mh_option_length(schema.length), + status=schema.status, + remove=bool(schema.flags['R']), + reserved=schema.flags['reserved'], + identity=schema.identity, + ) + return data + def _read_opt_exp(self, schema: 'Schema_ExperimentalMobilityOption', *, options: 'Option') -> 'Data_ExperimentalMobilityOption': """Read MH experimental mobility option. @@ -3789,6 +3692,89 @@ def _read_opt_exp(self, schema: 'Schema_ExperimentalMobilityOption', *, ) return data + def _read_opt_vendor(self, schema: 'Schema_VendorSpecificOption', *, + options: 'Option') -> 'Data_VendorSpecificOption': + """Read MH vendor specific mobility option. + + Structure of MH Vendor Specific mobility option [:rfc:`5094#section-3`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Vendor ID | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Sub-Type | Data....... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + The vendor ID is an SMI Network Management Private Enterprise Number, + whose space is unbounded, so it is not enumerated. The sub-type is + administered by that vendor rather than by IANA, so it is not + enumerated either. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length < 5: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_VendorSpecificOption( + type=schema.type, + length=self._mh_option_length(schema.length), + vendor=schema.vendor, + subtype=schema.subtype, + data=schema.data, + ) + return data + + def _read_opt_service(self, schema: 'Schema_ServiceSelectionOption', *, + options: 'Option') -> 'Data_ServiceSelectionOption': + """Read MH service selection mobility option. + + Structure of MH Service Selection mobility option [:rfc:`5149#section-3`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type = 20 | Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identifier... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + Unlike the DNS identity of :meth:`_read_opt_dns`, this identifier is + decoded to :class:`str`: :rfc:`5149#section-3` states outright that it + is UTF-8 and NFKC-normalised. A length of zero is invalid. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length == 0: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_ServiceSelectionOption( + type=schema.type, + length=self._mh_option_length(schema.length), + identifier=schema.identifier, + ) + return data + def _read_opt_badf(self, schema: 'Schema_BADFOption', *, options: 'Option') -> 'Data_BADFOption': """Read MH binding authorization data for FMIPv6 (BADF) option. @@ -3844,211 +3830,23 @@ def _read_opt_badf(self, schema: 'Schema_BADFOption', *, ) return data - def _read_opt_ipv6_ap(self, schema: 'Schema_IPv6AddressPrefixOption', *, - options: 'Option') -> 'Data_IPv6AddressPrefixOption': - """Read MH mobility header IPv6 address/prefix option. + def _read_opt_hnp(self, schema: 'Schema_HomeNetworkPrefixOption', *, + options: 'Option') -> 'Data_HomeNetworkPrefixOption': + """Read MH home network prefix option. - Structure of MH Mobility Header IPv6 Address/Prefix option - [:rfc:`5568#section-6.4.2`]: + Structure of MH Home Network Prefix option [:rfc:`5213#section-8.3`]: .. code-block:: text 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Length | Option-Code | Prefix Length | + | Type | Length | Reserved | Prefix Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | + + | | - + IPv6 Address/Prefix + - | | - + + - | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - The option code identifies which address the option carries, see - :class:`~pcapkit.protocols.internet.mh.protocol.IPv6AddressPrefixCode`. It, too, - is defined inline by :rfc:`5568#section-6.4.2` with no IANA registry - behind it, hence the enumeration is local to this module. - - Note: - :rfc:`5568` prints the type as ``17``, which is the *neighbor - discovery* option type of its sibling in - :rfc:`5568#section-6.4.1`. Errata ID 1816 (verified) corrects it to - the IANA-assigned mobility option type ``34``, which is what this - handler is registered against. - - Args: - schema: Parsed option schema. - options: Parsed MH options. - - Returns: - Constructed option data. - - """ - if schema.length != 18: - raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - if schema.prefix_length > 128: - raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - - data = Data_IPv6AddressPrefixOption( - type=schema.type, - length=self._mh_option_length(schema.length), - code=IPv6AddressPrefixCode(schema.code), - prefix_length=schema.prefix_length, - address=schema.address, - ) - return data - - def _read_opt_dns(self, schema: 'Schema_DNSUpdateOption', *, - options: 'Option') -> 'Data_DNSUpdateOption': - """Read MH DNS-UPDATE-TYPE option. - - Structure of MH DNS-UPDATE-TYPE option [:rfc:`5026#section-8.1`]: - - .. code-block:: text - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option Type | Option Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Status |R| Reserved | MN identity (FQDN) ... - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - Note: - The status field draws from the *Status Codes (DNS Update Mobility - Option)* registry that :rfc:`5026#section-10` creates, **not** from - the general mobility status codes, which is why it is typed - :class:`~pcapkit.const.mh.dns_status_code.DNSStatusCode`. - - The identity is kept as :obj:`bytes` rather than decoded to - :class:`str`, because the RFC says only "FQDN format" without saying - whether that means the presentation form or the length-prefixed - label form of :rfc:`1035`. - - Args: - schema: Parsed option schema. - options: Parsed MH options. - - Returns: - Constructed option data. - - """ - if schema.length < 2: - raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - - data = Data_DNSUpdateOption( - type=schema.type, - length=self._mh_option_length(schema.length), - status=schema.status, - remove=bool(schema.flags['R']), - reserved=schema.flags['reserved'], - identity=schema.identity, - ) - return data - - def _read_opt_vendor(self, schema: 'Schema_VendorSpecificOption', *, - options: 'Option') -> 'Data_VendorSpecificOption': - """Read MH vendor specific mobility option. - - Structure of MH Vendor Specific mobility option [:rfc:`5094#section-3`]: - - .. code-block:: text - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Vendor ID | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Sub-Type | Data....... - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - Note: - The vendor ID is an SMI Network Management Private Enterprise Number, - whose space is unbounded, so it is not enumerated. The sub-type is - administered by that vendor rather than by IANA, so it is not - enumerated either. - - Args: - schema: Parsed option schema. - options: Parsed MH options. - - Returns: - Constructed option data. - - """ - if schema.length < 5: - raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - - data = Data_VendorSpecificOption( - type=schema.type, - length=self._mh_option_length(schema.length), - vendor=schema.vendor, - subtype=schema.subtype, - data=schema.data, - ) - return data - - def _read_opt_service(self, schema: 'Schema_ServiceSelectionOption', *, - options: 'Option') -> 'Data_ServiceSelectionOption': - """Read MH service selection mobility option. - - Structure of MH Service Selection mobility option [:rfc:`5149#section-3`]: - - .. code-block:: text - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type = 20 | Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Identifier... - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - Note: - Unlike the DNS identity of :meth:`_read_opt_dns`, this identifier is - decoded to :class:`str`: :rfc:`5149#section-3` states outright that it - is UTF-8 and NFKC-normalised. A length of zero is invalid. - - Args: - schema: Parsed option schema. - options: Parsed MH options. - - Returns: - Constructed option data. - - """ - if schema.length == 0: - raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') - - data = Data_ServiceSelectionOption( - type=schema.type, - length=self._mh_option_length(schema.length), - identifier=schema.identifier, - ) - return data - - def _read_opt_hnp(self, schema: 'Schema_HomeNetworkPrefixOption', *, - options: 'Option') -> 'Data_HomeNetworkPrefixOption': - """Read MH home network prefix option. - - Structure of MH Home Network Prefix option [:rfc:`5213#section-8.3`]: - - .. code-block:: text - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Length | Reserved | Prefix Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - + + - | | - + Home Network Prefix + + + Home Network Prefix + | | + + | | @@ -4559,6 +4357,63 @@ def _read_opt_gre(self, schema: 'Schema_GREKeyOption', *, ) return data + def _read_opt_ipv6_ap(self, schema: 'Schema_IPv6AddressPrefixOption', *, + options: 'Option') -> 'Data_IPv6AddressPrefixOption': + """Read MH mobility header IPv6 address/prefix option. + + Structure of MH Mobility Header IPv6 Address/Prefix option + [:rfc:`5568#section-6.4.2`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Option-Code | Prefix Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + + + | | + + IPv6 Address/Prefix + + | | + + + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + The option code identifies which address the option carries, see + :class:`~pcapkit.protocols.internet.mh.protocol.IPv6AddressPrefixCode`. It, too, + is defined inline by :rfc:`5568#section-6.4.2` with no IANA registry + behind it, hence the enumeration is local to this module. + + Note: + :rfc:`5568` prints the type as ``17``, which is the *neighbor + discovery* option type of its sibling in + :rfc:`5568#section-6.4.1`. Errata ID 1816 (verified) corrects it to + the IANA-assigned mobility option type ``34``, which is what this + handler is registered against. + + Args: + schema: Parsed option schema. + options: Parsed MH options. + + Returns: + Constructed option data. + + """ + if schema.length != 18: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + if schema.prefix_length > 128: + raise ProtocolError(f'{self.alias}: [Opt {schema.type}] invalid format') + + data = Data_IPv6AddressPrefixOption( + type=schema.type, + length=self._mh_option_length(schema.length), + code=IPv6AddressPrefixCode(schema.code), + prefix_length=schema.prefix_length, + address=schema.address, + ) + return data + def _read_opt_bid(self, schema: 'Schema_BindingIdentifierOption', *, options: 'Option') -> 'Data_BindingIdentifierOption': """Read MH binding identifier (BID) option. @@ -4998,135 +4853,24 @@ def _read_opt_fs(self, schema: 'Schema_FlowSummaryOption', *, ) return data - def _read_fid_suboptions( - self, suboptions_schema: 'list[Schema_FlowIdentificationSuboption]', - ) -> 'FlowIDSuboption': - """Read MH flow identification sub-options. + def _read_opt_fid(self, schema: 'Schema_FlowIdentificationOption', *, + options: 'Option') -> 'Data_FlowIdentificationOption': + """Read MH flow identification mobility option. - Structure of MH flow identification sub-option [:rfc:`6089#section-4.2.1`]: + Structure of MH Flow Identification mobility option + [:rfc:`6089#section-4.2`]: .. code-block:: text 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Sub-Opt Type |Sub-Opt Length | Sub-Option Data... + | Option Type | Option Len | FID | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - These are sub-options of one mobility option rather than mobility options - in their own right, so they are dispatched here rather than through - :attr:`self.__option__ `. - - Note: - Dispatch is on the sub-option **type code** rather than on - ``isinstance`` of the schema class: the code is on the wire and the - registry keys on it, so it is the cheaper discriminator. A class test - would also be sound, since every - :class:`~pcapkit.protocols.schema.Schema` subclass carries its - own :class:`abc.ABCMeta` cache (:issue:`439`). The same reasoning - applies to :meth:`_read_ani_suboptions`, - :meth:`_read_qos_attributes`, :meth:`_read_lcmp_suboptions` and their - constructor counterparts. - - Args: - suboptions_schema: Parsed sub-option schemas. - - Returns: - Parsed sub-option data. - - Raises: - ProtocolError: If a sub-option's fields do not take exactly the octets - its ``Sub-Opt Length`` declares [:rfc:`6089#section-4.2.1`], or a - target care-of address is neither IPv4 nor IPv6 - [:rfc:`7109#section-6.2.2`], as the binding identifier option's - is refused. - - .. versionchanged:: 1.6.0 - - Such a sub-option is refused, as a mobility option is by - :meth:`_read_mh_options`, rather than read from octets outside it or - left unable to rebuild (:issue:`2684`). - - """ - suboptions = OrderedMultiDict() # type: FlowIDSuboption - - for schema in suboptions_schema: - code = schema.type - - if code in (Enum_FlowIDSuboption.Pad, Enum_FlowIDSuboption.PadN): - pad = cast('Schema_PadFlowIdentificationSuboption', schema) - size = 1 if code == Enum_FlowIDSuboption.Pad else self._mh_option_length(pad.length) - data = Data_PadFlowIdentificationSuboption( - type=code, - length=size, - data=pad.data, - ) # type: Data_FlowIdentificationSuboption - elif code == Enum_FlowIDSuboption.BID_Reference: - bid_ref = cast('Schema_BIDReferenceSuboption', schema) - data = Data_BIDReferenceSuboption( - type=code, - length=self._mh_option_length(bid_ref.length), - bid=tuple(bid_ref.bid), - ) - elif code == Enum_FlowIDSuboption.Traffic_Selector: - selector = cast('Schema_TrafficSelectorSuboption', schema) - data = Data_TrafficSelectorSuboption( - type=code, - length=self._mh_option_length(selector.length), - ts_format=selector.ts_format, - reserved=selector.reserved, - selector=selector.selector, - ) - elif code == Enum_FlowIDSuboption.Flow_Binding_Action: - action = cast('Schema_FlowBindingActionSuboption', schema) - data = Data_FlowBindingActionSuboption( - type=code, - length=self._mh_option_length(action.length), - reserved=action.reserved, - action=action.action, - ) - elif code == Enum_FlowIDSuboption.Target_Care_of_Address: - target = cast('Schema_TargetCareofAddressSuboption', schema) - if target.length not in (6, 18): - raise ProtocolError(f'{self.alias}: [FID SubOpt {code}] invalid sub-option length: {target.length}') - data = Data_TargetCareofAddressSuboption( - type=code, - length=self._mh_option_length(target.length), - reserved=target.reserved, - address=target.address, - ) - else: - unknown = cast('Schema_UnassignedFlowIdentificationSuboption', schema) - data = Data_UnassignedFlowIdentificationSuboption( - type=code, - length=self._mh_option_length(unknown.length), - data=unknown.data, - ) - - if len(schema) != data.length: - raise ProtocolError(f'{self.alias}: [FID SubOpt {code}] invalid sub-option length: {schema.length}') - suboptions.add(code, data) - - return suboptions - - def _read_opt_fid(self, schema: 'Schema_FlowIdentificationOption', *, - options: 'Option') -> 'Data_FlowIdentificationOption': - """Read MH flow identification mobility option. - - Structure of MH Flow Identification mobility option - [:rfc:`6089#section-4.2`]: - - .. code-block:: text - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option Type | Option Len | FID | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | FID-PRI | Reserved | Status | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Sub-options (optional) ... - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | FID-PRI | Reserved | Status | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Sub-options (optional) ... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Note: The status draws from the *Flow Identification Mobility Option Status @@ -5409,122 +5153,6 @@ def _read_opt_mag_addr(self, schema: 'Schema_MAGIPv6AddressOption', *, ) return data - def _read_ani_suboptions( - self, suboptions_schema: 'list[Schema_ANISuboption]', - ) -> 'ANISuboption': - """Read MH access network identifier sub-options. - - Structure of MH access network identifier sub-option - [:rfc:`6757#section-3.1`]: - - .. code-block:: text - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | ANI Type | ANI Length | Option Data ~ - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - The geo-location degrees of :rfc:`6757#section-3.1.2` are 24-bit - **two's-complement** fixed-point values with nine integer bits. A - :class:`~pcapkit.corekit.fields.strings.BitField` reads them unsigned, so - the sign is applied here by :meth:`_read_sint`, and both the decoded - degrees and the raw signed integers are recorded -- the latter so that - the exact wire value survives a round trip through the data model. - - Note: - Dispatch is on the sub-option type code rather than on ``isinstance`` - of the schema class, for the reason given in - :meth:`_read_fid_suboptions`. - - Args: - suboptions_schema: Parsed sub-option schemas. - - Returns: - Parsed sub-option data. - - Raises: - ProtocolError: If a sub-option's fields do not take exactly the octets - its ``ANI Length`` declares [:rfc:`6757#section-3.1`]. - - .. versionchanged:: 1.6.0 - - Such a sub-option is refused, as a mobility option is by - :meth:`_read_mh_options` (:issue:`2684`). - - """ - suboptions = OrderedMultiDict() # type: ANISuboption - - for schema in suboptions_schema: - code = schema.type - - if code == Enum_ANISuboption.Network_Identifier: - net = cast('Schema_ANINetworkIdentifierSuboption', schema) - data = Data_ANINetworkIdentifierSuboption( - type=code, - length=self._mh_option_length(net.length), - utf8=bool(net.flags['E']), - reserved=net.flags['reserved'], - net_name=net.net_name, - ap_name=net.ap_name, - ) # type: Data_ANISuboption - elif code == Enum_ANISuboption.Geo_Location: - geo = cast('Schema_ANIGeoLocationSuboption', schema) - raw_lat = self._read_sint(geo.location['latitude'], 24) - raw_lon = self._read_sint(geo.location['longitude'], 24) - data = Data_ANIGeoLocationSuboption( - type=code, - length=self._mh_option_length(geo.length), - latitude=raw_lat / 2 ** 15, - longitude=raw_lon / 2 ** 15, - raw_latitude=raw_lat, - raw_longitude=raw_lon, - ) - elif code == Enum_ANISuboption.Operator_Identifier: - operator = cast('Schema_ANIOperatorIdentifierSuboption', schema) - data = Data_ANIOperatorIdentifierSuboption( - type=code, - length=self._mh_option_length(operator.length), - op_id_type=operator.op_id_type, - identifier=operator.identifier, - ) - elif code == Enum_ANISuboption.Civic_Location: - civic = cast('Schema_ANICivicLocationSuboption', schema) - data = Data_ANICivicLocationSuboption( - type=code, - length=self._mh_option_length(civic.length), - format=civic.format, - reserved=civic.reserved, - location=civic.location, - ) - elif code == Enum_ANISuboption.MAG_Group_Identifier: - group = cast('Schema_ANIMAGGroupIdentifierSuboption', schema) - data = Data_ANIMAGGroupIdentifierSuboption( - type=code, - length=self._mh_option_length(group.length), - group_id=group.group_id, - ) - elif code == Enum_ANISuboption.ANI_Update_Timer: - timer = cast('Schema_ANIUpdateTimerSuboption', schema) - data = Data_ANIUpdateTimerSuboption( - type=code, - length=self._mh_option_length(timer.length), - timer=datetime.timedelta(seconds=timer.timer * 4), - ) - else: - unknown = cast('Schema_UnassignedANISuboption', schema) - data = Data_UnassignedANISuboption( - type=code, - length=self._mh_option_length(unknown.length), - data=unknown.data, - ) - - if len(schema) != data.length: - raise ProtocolError(f'{self.alias}: [ANI SubOpt {code}] invalid sub-option length: {schema.length}') - suboptions.add(code, data) - - return suboptions - def _read_opt_ani(self, schema: 'Schema_AccessNetworkIdentifierOption', *, options: 'Option') -> 'Data_AccessNetworkIdentifierOption': """Read MH access network identifier option. @@ -5819,148 +5447,36 @@ def _read_opt_ams6(self, schema: 'Schema_ActiveMulticastSubscriptionIPv6Option', ) return data - def _read_qos_attributes( - self, attributes_schema: 'list[Schema_QoSAttribute]', - ) -> 'QoSAttribute': - """Read MH quality-of-service attributes. + def _read_opt_qos(self, schema: 'Schema_QualityOfServiceOption', *, + options: 'Option') -> 'Data_QualityOfServiceOption': + """Read MH quality-of-service option. - Structure of MH quality-of-service attribute [:rfc:`7222#section-4.2`]: + Structure of MH Quality-of-Service option [:rfc:`7222#section-4.1`]: .. code-block:: text 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Length | Value ~ + | Type | Length | SR-ID | TC | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | OC | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + ~ QoS Attribute(s) ~ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Note: - Every bit rate here is in **bits** per second [:rfc:`7222#section-4.2.1`], - not the kilobytes per second of the load information option's capacity - fields. - - Dispatch is on the attribute type code rather than on ``isinstance`` of - the schema class, for the reason given in - :meth:`_read_fid_suboptions`. + The traffic class octet is a 6-bit differentiated services code point + with two reserved bits below it, which the receiver ignores + [:rfc:`7222#section-4.1`]; both are carried into the data model, so + that the octet rebuilds as captured. Args: - attributes_schema: Parsed attribute schemas. + schema: Parsed option schema. + options: Parsed MH options. Returns: - Parsed attribute data. - - Raises: - ProtocolError: If an attribute's fields do not take exactly the octets - its ``Length`` declares [:rfc:`7222#section-4.2`]. - - .. versionchanged:: 1.6.0 - - Such an attribute is refused, as a mobility option is by - :meth:`_read_mh_options` (:issue:`2684`). - - """ - attributes = OrderedMultiDict() # type: QoSAttribute - - for schema in attributes_schema: - code = schema.type - - if code in (Enum_QoSAttribute.Per_Session_Agg_Max_DL_Bit_Rate, - Enum_QoSAttribute.Per_Session_Agg_Max_UL_Bit_Rate): - session = cast('Schema_PerSessionBitRateAttribute', schema) - data = Data_PerSessionBitRateAttribute( - type=code, - length=self._mh_option_length(session.length), - service=bool(session.flags['S']), - exclude=bool(session.flags['E']), - reserved=session.flags['reserved'], - rate=session.rate, - ) # type: Data_QoSAttribute - elif code in (Enum_QoSAttribute.Per_MN_Agg_Max_DL_Bit_Rate, - Enum_QoSAttribute.Per_MN_Agg_Max_UL_Bit_Rate, - Enum_QoSAttribute.Aggregate_Max_DL_Bit_Rate, - Enum_QoSAttribute.Aggregate_Max_UL_Bit_Rate, - Enum_QoSAttribute.Guaranteed_DL_Bit_Rate, - Enum_QoSAttribute.Guaranteed_UL_Bit_Rate): - rate = cast('Schema_BitRateAttribute', schema) - data = Data_BitRateAttribute( - type=code, - length=self._mh_option_length(rate.length), - reserved=rate.reserved, - rate=rate.rate, - ) - elif code == Enum_QoSAttribute.Allocation_Retention_Priority: - arp = cast('Schema_AllocationRetentionPriorityAttribute', schema) - data = Data_AllocationRetentionPriorityAttribute( - type=code, - length=self._mh_option_length(arp.length), - reserved=arp.reserved, - priority_level=arp.priority['PL'], - preemption_capability=arp.priority['PC'], - preemption_vulnerability=arp.priority['PV'], - ) - elif code == Enum_QoSAttribute.QoS_Traffic_Selector: - selector = cast('Schema_QoSTrafficSelectorAttribute', schema) - data = Data_QoSTrafficSelectorAttribute( - type=code, - length=self._mh_option_length(selector.length), - reserved=selector.reserved, - ts_format=selector.ts_format, - selector=selector.selector, - ) - elif code == Enum_QoSAttribute.QoS_Vendor_Specific_Attribute: - vendor = cast('Schema_QoSVendorSpecificAttribute', schema) - data = Data_QoSVendorSpecificAttribute( - type=code, - length=self._mh_option_length(vendor.length), - reserved=vendor.reserved, - vendor=vendor.vendor, - subtype=vendor.subtype, - data=vendor.data, - ) - else: - unknown = cast('Schema_UnassignedQoSAttribute', schema) - data = Data_UnassignedQoSAttribute( - type=code, - length=self._mh_option_length(unknown.length), - data=unknown.data, - ) - - if len(schema) != data.length: - raise ProtocolError(f'{self.alias}: [QoS Attr {code}] invalid attribute length: {schema.length}') - attributes.add(code, data) - - return attributes - - def _read_opt_qos(self, schema: 'Schema_QualityOfServiceOption', *, - options: 'Option') -> 'Data_QualityOfServiceOption': - """Read MH quality-of-service option. - - Structure of MH Quality-of-Service option [:rfc:`7222#section-4.1`]: - - .. code-block:: text - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type | Length | SR-ID | TC | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | OC | Reserved | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - ~ QoS Attribute(s) ~ - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - Note: - The traffic class octet is a 6-bit differentiated services code point - with two reserved bits below it, which the receiver ignores - [:rfc:`7222#section-4.1`]; both are carried into the data model, so - that the octet rebuilds as captured. - - Args: - schema: Parsed option schema. - options: Parsed MH options. - - Returns: - Constructed option data. + Constructed option data. .. versionchanged:: 1.6.0 @@ -6121,86 +5637,6 @@ def _read_opt_mcast_ack(self, schema: 'Schema_MulticastAcknowledgementOption', * ) return data - def _read_lcmp_suboptions( - self, suboptions_schema: 'list[Schema_LMAControlledMAGSuboption]', - ) -> 'LMAControlledMAGSuboption': - """Read MH LMA-controlled MAG parameters sub-options. - - Structure of MH LMA-controlled MAG parameters sub-option - [:rfc:`8127#section-3.1`]: - - .. code-block:: text - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | LCMP Type | LCMP Length | Sub-Option Data ~ - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - Note: - Only the re-registration start time is in units of 4 seconds; every - other interval in these two sub-options is in whole seconds - [:rfc:`8127#section-3.1.1`, :rfc:`8127#section-3.1.2`]. - - Dispatch is on the sub-option type code rather than on ``isinstance`` - of the schema class, for the reason given in - :meth:`_read_fid_suboptions`. - - Args: - suboptions_schema: Parsed sub-option schemas. - - Returns: - Parsed sub-option data. - - Raises: - ProtocolError: If a sub-option's fields do not take exactly the octets - its ``LCMP Length`` declares [:rfc:`8127#section-3.1`]. - - .. versionchanged:: 1.6.0 - - Such a sub-option is refused, as a mobility option is by - :meth:`_read_mh_options` (:issue:`2684`). - - """ - suboptions = OrderedMultiDict() # type: LMAControlledMAGSuboption - - for schema in suboptions_schema: - code = schema.type - - if code == Enum_LMAControlledMAGSuboption.Binding_Re_registration_Control: - rereg = cast('Schema_BindingReregistrationControlSuboption', schema) - data = Data_BindingReregistrationControlSuboption( - type=code, - length=self._mh_option_length(rereg.length), - start_time=datetime.timedelta(seconds=rereg.start_time * 4), - initial_retransmission=datetime.timedelta( - seconds=rereg.initial_retransmission), - max_retransmission=datetime.timedelta(seconds=rereg.max_retransmission), - ) # type: Data_LMAControlledMAGSuboption - elif code == Enum_LMAControlledMAGSuboption.Heartbeat_Control: - heartbeat = cast('Schema_HeartbeatControlSuboption', schema) - data = Data_HeartbeatControlSuboption( - type=code, - length=self._mh_option_length(heartbeat.length), - interval=datetime.timedelta(seconds=heartbeat.interval), - retransmission_delay=datetime.timedelta( - seconds=heartbeat.retransmission_delay), - max_retransmissions=heartbeat.max_retransmissions, - ) - else: - unknown = cast('Schema_UnassignedLMAControlledMAGSuboption', schema) - data = Data_UnassignedLMAControlledMAGSuboption( - type=code, - length=self._mh_option_length(unknown.length), - data=unknown.data, - ) - - if len(schema) != data.length: - raise ProtocolError(f'{self.alias}: [LCMP SubOpt {code}] invalid sub-option length: {schema.length}') - suboptions.add(code, data) - - return suboptions - def _read_opt_lcmp(self, schema: 'Schema_LMAControlledMAGParametersOption', *, options: 'Option') -> 'Data_LMAControlledMAGParametersOption': """Read MH LMA-controlled MAG parameters option. @@ -6608,43 +6044,30 @@ def _read_opt_dlif_lladdr(self, schema: 'Schema_DLIFLinkLayerAddressOption', *, ) return data - @staticmethod - def _mh_extension_length(schema_length: 'int') -> 'int': - """Compute a CGA extension's whole-structure length from its on-the-wire ``Extension Data Length``. - - This is the CGA-extension counterpart of :meth:`_mh_option_length`, and - it deliberately adds ``4`` rather than ``2``. A CGA extension is **not** - an :rfc:`6275#section-6.2` mobility option: per :rfc:`4581#section-2`, - which defines the TLV format and formally updates :rfc:`3972`, its - ``Extension Type`` is a *"16-bit identifier of the type of the Extension - Field"* and its ``Extension Data Length`` a *"16-bit unsigned integer. - Length of the Extension Data field of this option, in octets"*. So the - fixed header is two 2-octet fields, not two 1-octet ones, and the whole - structure is ``4`` octets more than the stored length -- a fact - :rfc:`5535#section-5` states from the other direction for the one - non-experimental assigned type, whose ``Ext Len`` is the *"[l]ength of - the Extension in octets, not including the first 4 octets"*. - - Passing these lengths through :meth:`_mh_option_length` would report - every parsed CGA extension two octets short: an 8-octet extension with - an ``Extension Data Length`` of ``4`` would come back as ``6``. The write - side is unaffected, since :meth:`_make_cga_extensions` measures - ``len(schema.pack())`` instead. + def _read_ext_none(self, schema: 'Schema_UnknownExtension', *, + extensions: 'Extension') -> 'Data_UnknownExtension': + """Read unknown CGA extension. Args: - schema_length: raw ``Extension Data Length`` field value, as read off the wire. + schema: Parsed extension schema. + extensions: Parsed MH CGA extensions. Returns: - Whole-extension length, in octets, including the Extension Type and - Extension Data Length fields. + Constructed extension data. """ - return schema_length + 4 + data = Data_UnknownExtension( + type=schema.type, + length=self._mh_extension_length(schema.length), + data=schema.data, + ) + return data - def _read_cga_extensions(self, extensions_schema: 'list[Schema_CGAExtension]') -> 'Extension': - """Read CGA extensions. + def _read_ext_multiprefix(self, schema: 'Schema_MultiPrefixExtension', *, + extensions: 'Extension') -> 'Data_MultiPrefixExtension': + """Read multi-prefix CGA extension. - Structure of CGA extensions [:rfc:`4581`]: + Structure of Multi-Prefix CGA extension [:rfc:`5535`]: .. code-block:: text @@ -6653,107 +6076,31 @@ def _read_cga_extensions(self, extensions_schema: 'list[Schema_CGAExtension]') - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Extension Type | Extension Data Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |P| Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | - ~ Extension Data ~ + + Prefix[1] + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + Prefix[2] + + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + . . . + . . . + . . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + + Prefix[n] + | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - extensions_schema: Parsed CGA extensions. + schema: Parsed extension schema. + extensions: Parsed MH CGA extensions. Returns: - Parsed CGA extensions data. - - Raises: - ProtocolError: If an extension read by a ``_read_ext_*`` method has - fields that do not take exactly the octets its ``Extension Data - Length`` declares [:rfc:`4581#section-2`]; a parser registered as - a callable is not checked. - - .. versionchanged:: 1.6.0 - - Such an extension is refused, as a mobility option is by - :meth:`_read_mh_options` (:issue:`2684`). - - """ - extensions = OrderedMultiDict() # type: Extension - - for schema in extensions_schema: - type = schema.type - name = self._lookup_registry(self.__extension__, type) - - if isinstance(name, str): - meth_name = f'_read_ext_{name}' - meth = cast('ExtensionParser', - getattr(self, meth_name, self._read_ext_none)) - else: - meth = name[0] - data = meth(schema, extensions=extensions) - if isinstance(name, str) and len(schema) != data.length: - raise ProtocolError(f'{self.alias}: [CGA Ext {type}] invalid extension length: {schema.length}') - - # record extension data - extensions.add(type, data) - - return extensions - - def _read_ext_none(self, schema: 'Schema_UnknownExtension', *, - extensions: 'Extension') -> 'Data_UnknownExtension': - """Read unknown CGA extension. - - Args: - schema: Parsed extension schema. - extensions: Parsed MH CGA extensions. - - Returns: - Constructed extension data. - - """ - data = Data_UnknownExtension( - type=schema.type, - length=self._mh_extension_length(schema.length), - data=schema.data, - ) - return data - - def _read_ext_multiprefix(self, schema: 'Schema_MultiPrefixExtension', *, - extensions: 'Extension') -> 'Data_MultiPrefixExtension': - """Read multi-prefix CGA extension. - - Structure of Multi-Prefix CGA extension [:rfc:`5535`]: - - .. code-block:: text - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Extension Type | Extension Data Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |P| Reserved | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - + Prefix[1] + - | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - + Prefix[2] + - | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - . . . - . . . - . . . - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | | - + Prefix[n] + - | | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - Args: - schema: Parsed extension schema. - extensions: Parsed MH CGA extensions. - - Returns: - Constructed extension data. + Constructed extension data. """ data = Data_MultiPrefixExtension( @@ -7400,6 +6747,88 @@ def _make_msg_emh(self, message: 'Optional[Data_ExperimentalMessage]' = None, *, data=data, ) + def _make_msg_has(self, message: 'Optional[Data_HomeAgentSwitchMessage]' = None, *, + addresses: 'Optional[list[IPv6Address | bytes | str | int]]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long + msg_reserved: 'bytes' = b'\x00', + **kwargs: 'Any') -> 'Schema_HomeAgentSwitchMessage': + """Make MH home agent switch (HAS) message type. + + Args: + message: Message data model. + addresses: Alternate home agent addresses. An empty list is valid and + asks the mobile node to run home agent discovery. + options: Mobility options. + msg_reserved: The reserved octet of the message body as captured. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + Raises: + ProtocolError: If ``msg_reserved`` is not one octet. + + """ + if message is not None: + msg_reserved = self._fill_short_read(getattr(message, 'msg_reserved', b'\x00'), + size=1, model=message, field='data', offset=1) + addresses = cast('list[IPv6Address | bytes | str | int]', list(message.addresses)) + options = message.options + else: + addresses = addresses or [] + options = options or [] + + return Schema_HomeAgentSwitchMessage( + count=len(addresses), + reserved=self._make_octets(msg_reserved, size=1, name='reserved octets'), + addresses=addresses, + options=self._make_mh_options(options), + ) + + def _make_msg_hb(self, message: 'Optional[Data_HeartbeatMessage]' = None, *, + unsolicited: 'bool' = False, + response: 'bool' = False, + seq: 'int' = 0, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long + flags_reserved: 'int' = 0, + **kwargs: 'Any') -> 'Schema_HeartbeatMessage': + """Make MH heartbeat message type. + + Args: + message: Message data model. + unsolicited: Unsolicited flag. + response: Response flag; unset makes this a heartbeat request. + seq: Sequence number. + options: Mobility options. + flags_reserved: Reserved bits, i.e. the 14 bits before the ``U`` flag. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed message type. + + Raises: + ProtocolError: If ``seq`` or ``flags_reserved`` does not fit its field. + + """ + if message is not None: + unsolicited = message.unsolicited + response = message.response + flags_reserved = message.flags_reserved + seq = message.seq + options = message.options + else: + options = options or [] + + return Schema_HeartbeatMessage( + flags={ + 'U': unsolicited, + 'R': response, + 'reserved': self._make_uint(flags_reserved, bits=14, name='reserved bits'), + }, + seq=self._make_uint(seq, bits=32, name='sequence number'), + options=self._make_mh_options(options), + ) + def _make_msg_hi(self, message: 'Optional[Data_HandoverInitiateMessage]' = None, *, seq: 'int' = 0, assign: 'bool' = False, @@ -7514,88 +6943,6 @@ def _make_msg_hack(self, message: 'Optional[Data_HandoverAcknowledgeMessage]' = options=self._make_mh_options(options), ) - def _make_msg_has(self, message: 'Optional[Data_HomeAgentSwitchMessage]' = None, *, - addresses: 'Optional[list[IPv6Address | bytes | str | int]]' = None, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long - msg_reserved: 'bytes' = b'\x00', - **kwargs: 'Any') -> 'Schema_HomeAgentSwitchMessage': - """Make MH home agent switch (HAS) message type. - - Args: - message: Message data model. - addresses: Alternate home agent addresses. An empty list is valid and - asks the mobile node to run home agent discovery. - options: Mobility options. - msg_reserved: The reserved octet of the message body as captured. - **kwargs: Arbitrary keyword arguments. - - Returns: - Constructed message type. - - Raises: - ProtocolError: If ``msg_reserved`` is not one octet. - - """ - if message is not None: - msg_reserved = self._fill_short_read(getattr(message, 'msg_reserved', b'\x00'), - size=1, model=message, field='data', offset=1) - addresses = cast('list[IPv6Address | bytes | str | int]', list(message.addresses)) - options = message.options - else: - addresses = addresses or [] - options = options or [] - - return Schema_HomeAgentSwitchMessage( - count=len(addresses), - reserved=self._make_octets(msg_reserved, size=1, name='reserved octets'), - addresses=addresses, - options=self._make_mh_options(options), - ) - - def _make_msg_hb(self, message: 'Optional[Data_HeartbeatMessage]' = None, *, - unsolicited: 'bool' = False, - response: 'bool' = False, - seq: 'int' = 0, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long - flags_reserved: 'int' = 0, - **kwargs: 'Any') -> 'Schema_HeartbeatMessage': - """Make MH heartbeat message type. - - Args: - message: Message data model. - unsolicited: Unsolicited flag. - response: Response flag; unset makes this a heartbeat request. - seq: Sequence number. - options: Mobility options. - flags_reserved: Reserved bits, i.e. the 14 bits before the ``U`` flag. - **kwargs: Arbitrary keyword arguments. - - Returns: - Constructed message type. - - Raises: - ProtocolError: If ``seq`` or ``flags_reserved`` does not fit its field. - - """ - if message is not None: - unsolicited = message.unsolicited - response = message.response - flags_reserved = message.flags_reserved - seq = message.seq - options = message.options - else: - options = options or [] - - return Schema_HeartbeatMessage( - flags={ - 'U': unsolicited, - 'R': response, - 'reserved': self._make_uint(flags_reserved, bits=14, name='reserved bits'), - }, - seq=self._make_uint(seq, bits=32, name='sequence number'), - options=self._make_mh_options(options), - ) - def _make_msg_brm(self, message: 'Optional[Data_BindingRevocationMessage]' = None, *, br_type: 'Enum_BindingRevocation | int' = Enum_BindingRevocation.Binding_Revocation_Indication, code: 'Enum_RevocationTrigger | Enum_RevocationStatusCode | int' = Enum_RevocationTrigger.Unspecified, # pylint: disable=line-too-long @@ -7995,102 +7342,6 @@ def _make_msg_sr(self, message: 'Optional[Data_SubscriptionResponseMessage]' = N options=self._make_mh_options(options), ) - def _make_pad_options(self, offset: 'int') -> 'tuple[list[Schema_PadOption], int]': - """Make the padding options needed to align the header to 8 octets. - - Args: - offset: Number of octets emitted so far, **counting the six octets - of the fixed header** (``Payload Proto``, ``Header Len``, - ``MH Type``, ``Reserved`` and ``Checksum``) as well as the whole - message data. - - Returns: - Tuple of the padding option schemas and the number of octets they - occupy. - - Note: - The Mobility Header is a multiple of 8 octets and - :attr:`~pcapkit.protocols.internet.mh.schema.MH.length` counts those - units less one [:rfc:`6275#section-6.1.1`]. Its fixed part is - **six** octets, not the two of an IPv6 options header, so it is - ``len(data) + 6`` that has to be aligned -- which means the message - data is well formed only when it is 2 octets past a multiple of 8, - a different modulus from the one the extension headers use. - - A ``PadN`` option spends two octets on its own type and - ``Option Length`` fields before any padding data, so occupying - ``pad_len`` octets means an ``Option Length`` of ``pad_len - 2``. - One octet is padded with a ``Pad1`` option instead, that being the - only form which can occupy a single octet. Two octets are one - ``PadN`` with an ``Option Length`` of ``0``, not two ``Pad1``: - :rfc:`6275#section-6.2.2` says a ``PadN`` should be used rather than - multiple ``Pad1`` options. - - """ - pad_len = pad_length(offset, 8) - if pad_len == 0: - return [], 0 - if pad_len == 1: - return [self._make_opt_pad(Enum_Option.Pad1, length=0)], pad_len # type: ignore[arg-type] - return [self._make_opt_pad(Enum_Option.PadN, length=pad_len - 2)], pad_len # type: ignore[arg-type] - - def _make_mh_options(self, options: 'Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]') -> 'list[Schema_Option | bytes]': # pylint: disable=line-too-long - """Make options for MH. - - Args: - options: MH options. - - Returns: - Mobility options list. - - Raises: - ProtocolError: If the type of an option, given in a ``(type, - kwargs)`` pair or as the key of parsed data, is a name, a - :obj:`bool` or a code its field cannot carry, or if an option - given as raw octets is empty (:issue:`2723`). - - """ - if isinstance(options, list): - options_list = [] # type: list[Schema_Option | bytes] - for schema in options: - if isinstance(schema, bytes): - if not schema: - raise ProtocolError(f'{self.alias}: invalid option: {schema!r} holds no type octet') - code = Enum_Option.get(int.from_bytes(schema[0:1], 'big', signed=False)) - - data = schema # type: Schema_Option | bytes - elif isinstance(schema, Schema): - data = schema - else: - kind, args = cast('tuple[Enum_Option | int, dict[str, Any]]', schema) - code = self._make_index(kind, Enum_Option, bits=8, name='option type') - name = self._lookup_registry(self.__option__, code) - if isinstance(name, str): - meth_name = f'_make_opt_{name}' - meth = cast('OptionConstructor', - getattr(self, meth_name, self._make_opt_none)) - else: - meth = name[1] - data = meth(code, **args) - - options_list.append(data) - return options_list - - options_list = [] - for key, option in options.items(multi=True): - code = self._make_index(key, Enum_Option, bits=8, name='option type') - name = self._lookup_registry(self.__option__, code) - if isinstance(name, str): - meth_name = f'_make_opt_{name}' - meth = cast('OptionConstructor', - getattr(self, meth_name, self._make_opt_none)) - else: - meth = name[1] - - data = meth(code, option) - options_list.append(data) - return options_list - def _make_opt_none(self, type: 'Enum_Option', option: 'Optional[Data_UnassignedOption]' = None, *, data: 'bytes' = b'', **kwargs: 'Any') -> 'Schema_UnassignedOption': @@ -8870,120 +8121,6 @@ def _make_opt_ct(self, type: 'Enum_Option', option: 'Optional[Data_CareofTestOpt token=self._make_octets(token, size=8, name='token'), ) - def _make_opt_exp(self, type: 'Enum_Option', option: 'Optional[Data_ExperimentalMobilityOption]' = None, *, - data: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_ExperimentalMobilityOption': - """Make MH experimental mobility option. - - Args: - type: Option type. - option: Option data model. - data: Experimental data. - **kwargs: Arbitrary keyword arguments. - - Returns: - Constructed option schema. - - Raises: - ProtocolError: If the option is too long for its one-octet - ``Option Length``. - - """ - if option is not None: - data = option.data - - return Schema_ExperimentalMobilityOption( - type=type, - length=self._make_uint(len(data), bits=8, name='Option Length'), - data=data, - ) - - def _make_opt_badf(self, type: 'Enum_Option', option: 'Optional[Data_BADFOption]' = None, *, - spi: 'int' = 0, - data: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_BADFOption': - """Make MH binding authorization data for FMIPv6 (BADF) option. - - Args: - type: Option type. - option: Option data model. - spi: Security parameter index. - data: Authenticator. - **kwargs: Arbitrary keyword arguments. - - Returns: - Constructed option schema. - - Raises: - ProtocolError: If ``data`` is empty, since the ``length`` field then - reads as ``0``, which :meth:`_read_opt_badf` rejects; or if - ``spi`` does not fit in four octets. - Also if the option is too long for its one-octet ``Option Length``. - - Note: - The ``length`` field counts the authenticator only, excluding the - SPI, see :meth:`_read_opt_badf`. - - """ - if option is not None: - spi = option.spi - data = option.data - - if not data: - raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid data: ' - f'must be at least one octet, not {data!r}') - - return Schema_BADFOption( - type=type, - length=self._make_uint(len(data), bits=8, name='Option Length'), - spi=self._make_uint(spi, bits=32, name='SPI'), - data=data, - ) - - def _make_opt_ipv6_ap(self, type: 'Enum_Option', option: 'Optional[Data_IPv6AddressPrefixOption]' = None, *, - code: 'IPv6AddressPrefixCode | int' = IPv6AddressPrefixCode.New_Care_of_Address, - prefix_length: 'int' = 128, - address: 'bytes | str | int | IPv6Address' = '::', - **kwargs: 'Any') -> 'Schema_IPv6AddressPrefixOption': - """Make MH mobility header IPv6 address/prefix option. - - Args: - type: Option type. - option: Option data model. - code: Option code, see - :class:`~pcapkit.protocols.internet.mh.protocol.IPv6AddressPrefixCode`. - prefix_length: Prefix length. - address: IPv6 address/prefix. - **kwargs: Arbitrary keyword arguments. - - Returns: - Constructed option schema. - - Raises: - ProtocolError: If ``prefix_length`` is negative, a :obj:`bool`, - or longer than the address, or ``code`` is a name, a - :obj:`bool` or a code its field cannot carry. - - """ - if option is not None: - code_val = option.code - prefix_length = option.prefix_length - address = option.address - else: - code_val = self._make_index(code, IPv6AddressPrefixCode, bits=8, name='code') - - prefix_length = self._make_uint(prefix_length, bits=8, name='prefix length') - if prefix_length > 128: - raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid prefix length: {prefix_length}') - - return Schema_IPv6AddressPrefixOption( - type=type, - length=18, - code=code_val, - prefix_length=prefix_length, - address=address, - ) - def _make_opt_dns(self, type: 'Enum_Option', option: 'Optional[Data_DNSUpdateOption]' = None, *, status: 'Enum_DNSStatusCode | int' = Enum_DNSStatusCode.DNS_update_performed, remove: 'bool' = False, @@ -9029,6 +8166,34 @@ def _make_opt_dns(self, type: 'Enum_Option', option: 'Optional[Data_DNSUpdateOpt identity=identity, ) + def _make_opt_exp(self, type: 'Enum_Option', option: 'Optional[Data_ExperimentalMobilityOption]' = None, *, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_ExperimentalMobilityOption': + """Make MH experimental mobility option. + + Args: + type: Option type. + option: Option data model. + data: Experimental data. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + Raises: + ProtocolError: If the option is too long for its one-octet + ``Option Length``. + + """ + if option is not None: + data = option.data + + return Schema_ExperimentalMobilityOption( + type=type, + length=self._make_uint(len(data), bits=8, name='Option Length'), + data=data, + ) + def _make_opt_vendor(self, type: 'Enum_Option', option: 'Optional[Data_VendorSpecificOption]' = None, *, vendor: 'int' = 0, subtype: 'int' = 0, @@ -9106,30 +8271,72 @@ def _make_opt_service(self, type: 'Enum_Option', option: 'Optional[Data_ServiceS identifier=identifier, ) - def _make_opt_hnp(self, type: 'Enum_Option', option: 'Optional[Data_HomeNetworkPrefixOption]' = None, *, - prefix_length: 'int' = 64, - prefix: 'bytes | str | int | IPv6Address' = '::', - reserved: 'bytes' = b'\x00', - **kwargs: 'Any') -> 'Schema_HomeNetworkPrefixOption': - """Make MH home network prefix option. + def _make_opt_badf(self, type: 'Enum_Option', option: 'Optional[Data_BADFOption]' = None, *, + spi: 'int' = 0, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_BADFOption': + """Make MH binding authorization data for FMIPv6 (BADF) option. Args: type: Option type. option: Option data model. - prefix_length: Prefix length. - prefix: Home network prefix. - reserved: The reserved octet as captured. + spi: Security parameter index. + data: Authenticator. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. Raises: - ProtocolError: If ``prefix_length`` is negative, a :obj:`bool`, - or does not fit in one octet. Also if ``reserved`` is not one octet. - - Note: - The prefix length is checked against its 8-bit field alone, as + ProtocolError: If ``data`` is empty, since the ``length`` field then + reads as ``0``, which :meth:`_read_opt_badf` rejects; or if + ``spi`` does not fit in four octets. + Also if the option is too long for its one-octet ``Option Length``. + + Note: + The ``length`` field counts the authenticator only, excluding the + SPI, see :meth:`_read_opt_badf`. + + """ + if option is not None: + spi = option.spi + data = option.data + + if not data: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid data: ' + f'must be at least one octet, not {data!r}') + + return Schema_BADFOption( + type=type, + length=self._make_uint(len(data), bits=8, name='Option Length'), + spi=self._make_uint(spi, bits=32, name='SPI'), + data=data, + ) + + def _make_opt_hnp(self, type: 'Enum_Option', option: 'Optional[Data_HomeNetworkPrefixOption]' = None, *, + prefix_length: 'int' = 64, + prefix: 'bytes | str | int | IPv6Address' = '::', + reserved: 'bytes' = b'\x00', + **kwargs: 'Any') -> 'Schema_HomeNetworkPrefixOption': + """Make MH home network prefix option. + + Args: + type: Option type. + option: Option data model. + prefix_length: Prefix length. + prefix: Home network prefix. + reserved: The reserved octet as captured. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + Raises: + ProtocolError: If ``prefix_length`` is negative, a :obj:`bool`, + or does not fit in one octet. Also if ``reserved`` is not one octet. + + Note: + The prefix length is checked against its 8-bit field alone, as :rfc:`5213#section-5.3.2` has a local mobility anchor answer a prefix it cannot authorise with a status rather than discard the option, so whatever the reader keeps is written back. @@ -9562,6 +8769,50 @@ def _make_opt_gre(self, type: 'Enum_Option', option: 'Optional[Data_GREKeyOption key=None if key is None else self._make_uint(key, bits=32, name='GRE key'), ) + def _make_opt_ipv6_ap(self, type: 'Enum_Option', option: 'Optional[Data_IPv6AddressPrefixOption]' = None, *, + code: 'IPv6AddressPrefixCode | int' = IPv6AddressPrefixCode.New_Care_of_Address, + prefix_length: 'int' = 128, + address: 'bytes | str | int | IPv6Address' = '::', + **kwargs: 'Any') -> 'Schema_IPv6AddressPrefixOption': + """Make MH mobility header IPv6 address/prefix option. + + Args: + type: Option type. + option: Option data model. + code: Option code, see + :class:`~pcapkit.protocols.internet.mh.protocol.IPv6AddressPrefixCode`. + prefix_length: Prefix length. + address: IPv6 address/prefix. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + Raises: + ProtocolError: If ``prefix_length`` is negative, a :obj:`bool`, + or longer than the address, or ``code`` is a name, a + :obj:`bool` or a code its field cannot carry. + + """ + if option is not None: + code_val = option.code + prefix_length = option.prefix_length + address = option.address + else: + code_val = self._make_index(code, IPv6AddressPrefixCode, bits=8, name='code') + + prefix_length = self._make_uint(prefix_length, bits=8, name='prefix length') + if prefix_length > 128: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid prefix length: {prefix_length}') + + return Schema_IPv6AddressPrefixOption( + type=type, + length=18, + code=code_val, + prefix_length=prefix_length, + address=address, + ) + def _make_opt_bid(self, type: 'Enum_Option', option: 'Optional[Data_BindingIdentifierOption]' = None, *, bid: 'int' = 1, status: 'Enum_StatusCode | int' = Enum_StatusCode.Binding_Update_accepted_Proxy_Binding_Update_accepted, # pylint: disable=line-too-long @@ -10049,155 +9300,6 @@ def _make_opt_fs(self, type: 'Enum_Option', option: 'Optional[Data_FlowSummaryOp fid=fid, ) - def _make_fid_suboptions( - self, - suboptions: 'FlowIDSuboption | list[Schema_FlowIdentificationSuboption | tuple[Enum_FlowIDSuboption | int, dict[str, Any]] | bytes]', # pylint: disable=line-too-long - ) -> 'list[Schema_FlowIdentificationSuboption | bytes]': - """Make MH flow identification sub-options. - - Args: - suboptions: Sub-options, as parsed data, schemas, ``(type, kwargs)`` - pairs, or raw octets. - - Returns: - Sub-option schema list. - - Raises: - ProtocolError: If the type of a sub-option, given in a ``(type, - kwargs)`` pair or as the key of parsed data, is a name, a - :obj:`bool` or a code its field cannot carry, or if a - sub-option given as raw octets is empty (:issue:`2723`). - - """ - if isinstance(suboptions, list): - entries = [] # type: list[Schema_FlowIdentificationSuboption | bytes] - for item in suboptions: - if isinstance(item, bytes) and not item: - raise ProtocolError(f'{self.alias}: invalid sub-option: {item!r} holds no type octet') - if isinstance(item, (bytes, Schema)): - entries.append(cast('Schema_FlowIdentificationSuboption | bytes', item)) - else: - kind, args = cast('tuple[Enum_FlowIDSuboption | int, dict[str, Any]]', item) - code = self._make_index(kind, Enum_FlowIDSuboption, bits=8, name='sub-option type') - entries.append(self._make_fid_suboption(code, **args)) - return entries - - return [self._make_fid_suboption( - self._make_index(key, Enum_FlowIDSuboption, bits=8, name='sub-option type'), option) - for key, option in suboptions.items(multi=True)] - - def _make_fid_suboption(self, code: 'Enum_FlowIDSuboption', - option: 'Optional[Data_FlowIdentificationSuboption]' = None, - **kwargs: 'Any') -> 'Schema_FlowIdentificationSuboption': - """Make one MH flow identification sub-option. - - Args: - code: Sub-option type. - option: Sub-option data model. - **kwargs: Sub-option fields, when no data model is given. - - Returns: - Constructed sub-option schema. - - Raises: - ProtocolError: If an entry of a ``BID_Reference`` sub-option's ``bid`` does - not fit its field, a ``ts_format`` or ``action`` is a name, a - :obj:`bool` or a code its field cannot carry, or a sub-option's - ``reserved`` is not as many octets as its field. - - Note: - The data model parameter is named ``option`` rather than ``data`` - to match :meth:`_make_opt_fid` and the rest of this module -- and - because ``data`` would **shadow a field**. The traffic selector and - unassigned sub-options both carry a field of their own called - ``data``, so a caller's ``data=`` would bind to the model parameter - instead of landing in ``**kwargs``, and the fallback that reads - ``data`` from ``**kwargs`` would never see it: the payload would be - silently dropped and the length written as though it were empty, - with no exception. - - Dispatch is on ``code`` rather than on ``isinstance`` of the schema or - data class, for the reason given in :meth:`_read_fid_suboptions`. - - """ - if code in (Enum_FlowIDSuboption.Pad, Enum_FlowIDSuboption.PadN): - if code == Enum_FlowIDSuboption.Pad: - pad_len = 0 - elif option is not None: - pad_len = option.length - 2 - else: - pad_len = cast('int', kwargs.get('length', 0)) - if option is not None: - pad_data = getattr(option, 'data', b'') - else: - pad_data = cast('bytes', kwargs.get('data', b'')) - return Schema_PadFlowIdentificationSuboption(type=code, length=pad_len, data=pad_data) - - if code == Enum_FlowIDSuboption.BID_Reference: - if option is not None: - bid = list(cast('Data_BIDReferenceSuboption', option).bid) - else: - bid = cast('list[int]', kwargs.get('bid') or []) - for item in bid: - self._make_uint(item, bits=16, name='binding identifier') - return Schema_BIDReferenceSuboption(type=code, length=len(bid) * 2, bid=bid) - - if code == Enum_FlowIDSuboption.Traffic_Selector: - if option is not None: - selector_opt = cast('Data_TrafficSelectorSuboption', option) - ts_format = selector_opt.ts_format # type: Enum_TrafficSelector | int - selector = selector_opt.selector - reserved = getattr(selector_opt, 'reserved', b'\x00') - else: - ts_format = self._make_index( - kwargs.get('ts_format', Enum_TrafficSelector.IPv6_Binary_Traffic_Selector), - Enum_TrafficSelector, bits=8, name='TS format') - selector = cast('bytes', kwargs.get('selector', b'')) - reserved = cast('bytes', kwargs.get('reserved', b'\x00')) - return Schema_TrafficSelectorSuboption( - type=code, length=2 + len(selector), - ts_format=cast('Enum_TrafficSelector', ts_format), - reserved=self._make_octets(reserved, size=1, name='reserved octets'), - selector=selector) - - if code == Enum_FlowIDSuboption.Flow_Binding_Action: - if option is not None: - action = cast('Data_FlowBindingActionSuboption', - option).action # type: Enum_FlowBindingAction | int - reserved = getattr(option, 'reserved', b'\x00') - else: - action = self._make_index(kwargs.get('action', Enum_FlowBindingAction.Add), - Enum_FlowBindingAction, bits=8, name='action') - reserved = cast('bytes', kwargs.get('reserved', b'\x00')) - return Schema_FlowBindingActionSuboption( - type=code, length=2, reserved=self._make_octets(reserved, size=1, name='reserved octets'), - action=cast('Enum_FlowBindingAction', action)) - - if code == Enum_FlowIDSuboption.Target_Care_of_Address: - if option is not None: - address = cast('Data_TargetCareofAddressSuboption', option).address # type: Any - reserved = getattr(option, 'reserved', b'\x00\x00') - else: - address = kwargs.get('address', '::') - reserved = cast('bytes', kwargs.get('reserved', b'\x00\x00')) - # NOTE: Through ``parse_ip_address`` because the sub-option length - # below is derived from the family here, ahead of the schema, so a - # bare ``ipaddress.ip_address`` would launder a ``bool`` past the - # schema's guard, packing ``address=True`` as ``0506000000000001``. - addr = parse_ip_address( - address, f'{self.alias}: [OptNo {code}] invalid target care-of address') - return Schema_TargetCareofAddressSuboption( - type=code, length=6 if addr.version == 4 else 18, - reserved=self._make_octets(reserved, size=2, name='reserved octets'), - address=addr) - - if option is not None: - payload = cast('Data_UnassignedFlowIdentificationSuboption', option).data - else: - payload = cast('bytes', kwargs.get('data', b'')) - return Schema_UnassignedFlowIdentificationSuboption( - type=code, length=len(payload), data=payload) - def _make_opt_fid(self, type: 'Enum_Option', option: 'Optional[Data_FlowIdentificationOption]' = None, *, fid: 'int' = 1, fid_pri: 'int' = 1, @@ -10473,165 +9575,10 @@ def _make_opt_mag_addr(self, type: 'Enum_Option', option: 'Optional[Data_MAGIPv6 address=address, ) - def _make_ani_suboptions( - self, - suboptions: 'ANISuboption | list[Schema_ANISuboption | tuple[Enum_ANISuboption | int, dict[str, Any]] | bytes]', - ) -> 'list[Schema_ANISuboption | bytes]': - """Make MH access network identifier sub-options. - - Args: - suboptions: Sub-options, as parsed data, schemas, ``(type, kwargs)`` - pairs, or raw octets. - - Returns: - Sub-option schema list. - - Raises: - ProtocolError: If the type of a sub-option, given in a ``(type, - kwargs)`` pair or as the key of parsed data, is a name, a - :obj:`bool` or a code its field cannot carry, or if a - sub-option given as raw octets is empty (:issue:`2723`). - - """ - if isinstance(suboptions, list): - entries = [] # type: list[Schema_ANISuboption | bytes] - for item in suboptions: - if isinstance(item, bytes) and not item: - raise ProtocolError(f'{self.alias}: invalid sub-option: {item!r} holds no type octet') - if isinstance(item, (bytes, Schema)): - entries.append(cast('Schema_ANISuboption | bytes', item)) - else: - kind, args = cast('tuple[Enum_ANISuboption | int, dict[str, Any]]', item) - code = self._make_index(kind, Enum_ANISuboption, bits=8, name='sub-option type') - entries.append(self._make_ani_suboption(code, **args)) - return entries - - return [self._make_ani_suboption( - self._make_index(key, Enum_ANISuboption, bits=8, name='sub-option type'), option) - for key, option in suboptions.items(multi=True)] - - def _make_ani_suboption(self, code: 'Enum_ANISuboption', - option: 'Optional[Data_ANISuboption]' = None, - **kwargs: 'Any') -> 'Schema_ANISuboption': - """Make one MH access network identifier sub-option. - - Args: - code: Sub-option type. - option: Sub-option data model. - **kwargs: Sub-option fields, when no data model is given. - - Returns: - Constructed sub-option schema. - - Raises: - ProtocolError: If a raw geo-location degree does not fit in 24 - bits as two's complement, or a sub-option's ``reserved``, - ``format``, ``group_id`` or ``timer`` does not fit its field, or - ``op_id_type`` is a name, a :obj:`bool` or a code its field - cannot carry. - - Note: - The geo-location degrees are re-encoded from the **raw** signed - integers rather than from the decoded floats, since a float cannot - always be converted back to the same 24-bit fixed-point value, and - :meth:`_make_sint` gives back the bits :meth:`_read_sint` read. - - The data model parameter is named ``option`` rather than ``data`` so - that it cannot shadow a sub-option field of that name -- see - :meth:`_make_fid_suboption`. Dispatch is on ``code`` - rather than on ``isinstance``, for the reason given in - :meth:`_read_fid_suboptions`. - - """ - if code == Enum_ANISuboption.Network_Identifier: - if option is not None: - net = cast('Data_ANINetworkIdentifierSuboption', option) - utf8, net_name, ap_name = net.utf8, net.net_name, net.ap_name - reserved = net.reserved - else: - utf8 = cast('bool', kwargs.get('utf8', False)) - reserved = cast('int', kwargs.get('reserved', 0)) - net_name = cast('bytes', kwargs.get('net_name', b'')) - ap_name = cast('bytes', kwargs.get('ap_name', b'')) - return Schema_ANINetworkIdentifierSuboption( - type=code, length=3 + len(net_name) + len(ap_name), - flags={'E': int(utf8), 'reserved': self._make_uint(reserved, bits=7, - name='reserved bits')}, - net_name_len=len(net_name), net_name=net_name, - ap_name_len=len(ap_name), ap_name=ap_name) - - if code == Enum_ANISuboption.Geo_Location: - if option is not None: - geo = cast('Data_ANIGeoLocationSuboption', option) - raw_lat, raw_lon = geo.raw_latitude, geo.raw_longitude - else: - raw_lat = cast('int', kwargs.get('raw_latitude', 0)) - raw_lon = cast('int', kwargs.get('raw_longitude', 0)) - return Schema_ANIGeoLocationSuboption( - type=code, length=6, location={ - 'latitude': self._make_sint(raw_lat, bits=24, name='ANI geo-location latitude'), - 'longitude': self._make_sint(raw_lon, bits=24, name='ANI geo-location longitude'), - }) - - if code == Enum_ANISuboption.Operator_Identifier: - if option is not None: - operator = cast('Data_ANIOperatorIdentifierSuboption', option) - op_id_type = operator.op_id_type # type: Enum_OperatorID | int - identifier = operator.identifier - else: - op_id_type = self._make_index( - kwargs.get('op_id_type', Enum_OperatorID.Realm_of_the_Operator), - Enum_OperatorID, bits=8, name='Op-ID type') - identifier = cast('bytes', kwargs.get('identifier', b'')) - return Schema_ANIOperatorIdentifierSuboption( - type=code, length=1 + len(identifier), - op_id_type=cast('Enum_OperatorID', op_id_type), identifier=identifier) - - if code == Enum_ANISuboption.Civic_Location: - if option is not None: - civic = cast('Data_ANICivicLocationSuboption', option) - fmt, location = civic.format, civic.location - octets = getattr(civic, 'reserved', b'\x00') - else: - fmt = cast('int', kwargs.get('format', 0)) - location = cast('bytes', kwargs.get('location', b'')) - octets = cast('bytes', kwargs.get('reserved', b'\x00')) - return Schema_ANICivicLocationSuboption( - type=code, length=2 + len(location), - format=self._make_uint(fmt, bits=8, name='civic location format'), - reserved=self._make_octets(octets, size=1, name='reserved octets'), - location=location) - - if code == Enum_ANISuboption.MAG_Group_Identifier: - if option is not None: - group_id = cast('Data_ANIMAGGroupIdentifierSuboption', option).group_id - else: - group_id = cast('int', kwargs.get('group_id', 0)) - return Schema_ANIMAGGroupIdentifierSuboption( - type=code, length=2, group_id=self._make_uint(group_id, bits=16, - name='MAG group identifier')) - - if code == Enum_ANISuboption.ANI_Update_Timer: - if option is not None: - timer = math.ceil(cast('Data_ANIUpdateTimerSuboption', - option).timer.total_seconds() / 4) - else: - raw_timer = kwargs.get('timer', 0) - timer = raw_timer if isinstance(raw_timer, int) else math.ceil( - raw_timer.total_seconds() / 4) - return Schema_ANIUpdateTimerSuboption( - type=code, length=2, timer=self._make_uint(timer, bits=16, name='ANI update timer')) - - if option is not None: - payload = cast('Data_UnassignedANISuboption', option).data - else: - payload = cast('bytes', kwargs.get('data', b'')) - return Schema_UnassignedANISuboption(type=code, length=len(payload), data=payload) - - def _make_opt_ani(self, type: 'Enum_Option', option: 'Optional[Data_AccessNetworkIdentifierOption]' = None, *, - suboptions: 'Optional[ANISuboption | list[Schema_ANISuboption | tuple[Enum_ANISuboption | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long - **kwargs: 'Any') -> 'Schema_AccessNetworkIdentifierOption': - """Make MH access network identifier option. + def _make_opt_ani(self, type: 'Enum_Option', option: 'Optional[Data_AccessNetworkIdentifierOption]' = None, *, + suboptions: 'Optional[ANISuboption | list[Schema_ANISuboption | tuple[Enum_ANISuboption | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long + **kwargs: 'Any') -> 'Schema_AccessNetworkIdentifierOption': + """Make MH access network identifier option. Args: type: Option type. @@ -10881,251 +9828,89 @@ def _make_opt_ams6(self, type: 'Enum_Option', context=context, ) - def _make_qos_attributes( - self, - attributes: 'QoSAttribute | list[Schema_QoSAttribute | tuple[Enum_QoSAttribute | int, dict[str, Any]] | bytes]', - ) -> 'list[Schema_QoSAttribute | bytes]': - """Make MH quality-of-service attributes. + def _make_opt_qos(self, type: 'Enum_Option', option: 'Optional[Data_QualityOfServiceOption]' = None, *, + sr_id: 'int' = 0, + dscp: 'int' = 0, + oc: 'int' = 0, + attributes: 'Optional[QoSAttribute | list[Schema_QoSAttribute | tuple[Enum_QoSAttribute | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long + reserved: 'bytes' = b'\x00\x00\x00', + tc_reserved: 'int' = 0, + **kwargs: 'Any') -> 'Schema_QualityOfServiceOption': + """Make MH quality-of-service option. Args: - attributes: Attributes, as parsed data, schemas, ``(type, kwargs)`` - pairs, or raw octets. + type: Option type. + option: Option data model. + sr_id: Service request identifier. + dscp: Differentiated services code point, 6 bits. + oc: Operational code. + attributes: Quality-of-service attributes. + reserved: The three reserved octets as captured. + tc_reserved: The 2 reserved bits of the traffic class, after ``dscp``. + **kwargs: Arbitrary keyword arguments. Returns: - Attribute schema list. + Constructed option schema. Raises: - ProtocolError: If the type of an attribute, given in a ``(type, - kwargs)`` pair or as the key of parsed data, is a name, a - :obj:`bool` or a code its field cannot carry, or if an - attribute given as raw octets is empty (:issue:`2723`). + ProtocolError: If ``sr_id``, ``dscp``, ``tc_reserved`` or ``oc`` does + not fit its field. + Also if the option is too long for its one-octet ``Option Length``. + Also if ``reserved`` is not three octets. + + .. versionchanged:: 1.6.0 + + Takes ``tc_reserved``, which a data model passed as ``option`` carries + as well (:issue:`2684`). """ - if isinstance(attributes, list): - entries = [] # type: list[Schema_QoSAttribute | bytes] - for item in attributes: - if isinstance(item, bytes) and not item: - raise ProtocolError(f'{self.alias}: invalid attribute: {item!r} holds no type octet') - if isinstance(item, (bytes, Schema)): - entries.append(cast('Schema_QoSAttribute | bytes', item)) - else: - kind, args = cast('tuple[Enum_QoSAttribute | int, dict[str, Any]]', item) - code = self._make_index(kind, Enum_QoSAttribute, bits=8, name='attribute type') - entries.append(self._make_qos_attribute(code, **args)) - return entries + if option is not None: + reserved = getattr(option, 'reserved', b'\x00\x00\x00') + sr_id = option.sr_id + dscp = option.dscp + tc_reserved = getattr(option, 'tc_reserved', 0) + oc = option.oc + attributes = option.attributes + else: + attributes = attributes or [] - return [self._make_qos_attribute( - self._make_index(key, Enum_QoSAttribute, bits=8, name='attribute type'), option) - for key, option in attributes.items(multi=True)] + dscp = self._make_uint(dscp, bits=6, name='DSCP') + tc_reserved = self._make_uint(tc_reserved, bits=2, name='traffic class reserved bits') - def _make_qos_attribute(self, code: 'Enum_QoSAttribute', - option: 'Optional[Data_QoSAttribute]' = None, - **kwargs: 'Any') -> 'Schema_QoSAttribute': - """Make one MH quality-of-service attribute. + entries = self._make_qos_attributes(attributes) + length = 6 + sum(len(entry) if isinstance(entry, bytes) else len(entry.pack()) + for entry in entries) + + return Schema_QualityOfServiceOption( + type=type, + length=self._make_uint(length, bits=8, name='Option Length'), + sr_id=self._make_uint(sr_id, bits=8, name='service request identifier'), + tc=dscp << 2 | tc_reserved, + oc=self._make_uint(oc, bits=8, name='operational code'), + reserved=self._make_octets(reserved, size=3, name='reserved octets'), + attributes=entries, + ) + + def _make_opt_lma_up(self, type: 'Enum_Option', option: 'Optional[Data_LMAUserPlaneAddressOption]' = None, *, + address: 'Optional[bytes | str | int | IPv4Address | IPv6Address]' = None, + reserved: 'bytes' = b'\x00\x00', + **kwargs: 'Any') -> 'Schema_LMAUserPlaneAddressOption': + """Make MH LMA user-plane address option. Args: - code: Attribute type. - option: Attribute data model. - **kwargs: Attribute fields, when no data model is given. + type: Option type. + option: Option data model. + address: LMA user-plane address, or :obj:`None` to omit it -- which is + how a mobile access gateway asks for a transport without naming an + address. + reserved: The two reserved octets as captured. + **kwargs: Arbitrary keyword arguments. Returns: - Constructed attribute schema. + Constructed option schema. Raises: - ProtocolError: If a rate, priority, vendor or reserved value does not fit - its field, or ``ts_format`` is a name, a :obj:`bool` or a code its - field cannot carry. - - Note: - The data model parameter is named ``option`` rather than ``data`` - deliberately. The vendor-specific attribute of - :rfc:`7222#section-4.2.11` has a field of its own called ``data``, so - a parameter of that name would capture a caller's ``data=`` and leave - the ``kwargs.get('data')`` fallback empty, silently dropping the vendor - payload and writing the length as though it were empty. ``vendor`` and - ``subtype`` do not collide, so the loss would look like a partial - success rather than a bug. - - Dispatch is on ``code`` rather than on ``isinstance``, for the reason - given in :meth:`_read_fid_suboptions`. - - """ - if code in (Enum_QoSAttribute.Per_Session_Agg_Max_DL_Bit_Rate, - Enum_QoSAttribute.Per_Session_Agg_Max_UL_Bit_Rate): - if option is not None: - session = cast('Data_PerSessionBitRateAttribute', option) - service, exclude, rate = session.service, session.exclude, session.rate - reserved = session.reserved - else: - service = cast('bool', kwargs.get('service', False)) - exclude = cast('bool', kwargs.get('exclude', False)) - reserved = cast('int', kwargs.get('reserved', 0)) - rate = cast('int', kwargs.get('rate', 0)) - return Schema_PerSessionBitRateAttribute( - type=code, length=6, - flags={'S': int(service), 'E': int(exclude), - 'reserved': self._make_uint(reserved, bits=14, name='reserved bits')}, - rate=self._make_uint(rate, bits=32, name='bit rate')) - - if code in (Enum_QoSAttribute.Per_MN_Agg_Max_DL_Bit_Rate, - Enum_QoSAttribute.Per_MN_Agg_Max_UL_Bit_Rate, - Enum_QoSAttribute.Aggregate_Max_DL_Bit_Rate, - Enum_QoSAttribute.Aggregate_Max_UL_Bit_Rate, - Enum_QoSAttribute.Guaranteed_DL_Bit_Rate, - Enum_QoSAttribute.Guaranteed_UL_Bit_Rate): - if option is not None: - rate = cast('Data_BitRateAttribute', option).rate - octets = getattr(option, 'reserved', b'\x00\x00') - else: - rate = cast('int', kwargs.get('rate', 0)) - octets = cast('bytes', kwargs.get('reserved', b'\x00\x00')) - return Schema_BitRateAttribute(type=code, length=6, - reserved=self._make_octets(octets, size=2, name='reserved octets'), - rate=self._make_uint(rate, bits=32, name='bit rate')) - - if code == Enum_QoSAttribute.Allocation_Retention_Priority: - if option is not None: - arp = cast('Data_AllocationRetentionPriorityAttribute', option) - level = arp.priority_level - capability = arp.preemption_capability - vulnerability = arp.preemption_vulnerability - octets = getattr(arp, 'reserved', b'\x00') - else: - level = cast('int', kwargs.get('priority_level', 1)) - capability = cast('int', kwargs.get('preemption_capability', 0)) - vulnerability = cast('int', kwargs.get('preemption_vulnerability', 0)) - octets = cast('bytes', kwargs.get('reserved', b'\x00')) - return Schema_AllocationRetentionPriorityAttribute( - type=code, length=2, reserved=self._make_octets(octets, size=1, name='reserved octets'), - priority={ - 'PL': self._make_uint(level, bits=4, name='priority level'), - 'PC': self._make_uint(capability, bits=2, name='preemption capability'), - 'PV': self._make_uint(vulnerability, bits=2, name='preemption vulnerability'), - }) - - if code == Enum_QoSAttribute.QoS_Traffic_Selector: - if option is not None: - ts_attr = cast('Data_QoSTrafficSelectorAttribute', option) - ts_format = ts_attr.ts_format # type: Enum_TrafficSelector | int - selector = ts_attr.selector - octets = getattr(ts_attr, 'reserved', b'\x00') - else: - ts_format = self._make_index( - kwargs.get('ts_format', Enum_TrafficSelector.IPv6_Binary_Traffic_Selector), - Enum_TrafficSelector, bits=8, name='TS format') - selector = cast('bytes', kwargs.get('selector', b'')) - octets = cast('bytes', kwargs.get('reserved', b'\x00')) - return Schema_QoSTrafficSelectorAttribute( - type=code, length=2 + len(selector), - reserved=self._make_octets(octets, size=1, name='reserved octets'), - ts_format=cast('Enum_TrafficSelector', ts_format), selector=selector) - - if code == Enum_QoSAttribute.QoS_Vendor_Specific_Attribute: - if option is not None: - vendor_attr = cast('Data_QoSVendorSpecificAttribute', option) - vendor, subtype, payload = (vendor_attr.vendor, vendor_attr.subtype, - vendor_attr.data) - octets = getattr(vendor_attr, 'reserved', b'\x00\x00') - else: - vendor = cast('int', kwargs.get('vendor', 0)) - subtype = cast('int', kwargs.get('subtype', 0)) - payload = cast('bytes', kwargs.get('data', b'')) - octets = cast('bytes', kwargs.get('reserved', b'\x00\x00')) - return Schema_QoSVendorSpecificAttribute( - type=code, length=7 + len(payload), - reserved=self._make_octets(octets, size=2, name='reserved octets'), - vendor=self._make_uint(vendor, bits=32, name='vendor ID'), - subtype=self._make_uint(subtype, bits=8, name='vendor subtype'), data=payload) - - if option is not None: - payload = cast('Data_UnassignedQoSAttribute', option).data - else: - payload = cast('bytes', kwargs.get('data', b'')) - return Schema_UnassignedQoSAttribute(type=code, length=len(payload), data=payload) - - def _make_opt_qos(self, type: 'Enum_Option', option: 'Optional[Data_QualityOfServiceOption]' = None, *, - sr_id: 'int' = 0, - dscp: 'int' = 0, - oc: 'int' = 0, - attributes: 'Optional[QoSAttribute | list[Schema_QoSAttribute | tuple[Enum_QoSAttribute | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long - reserved: 'bytes' = b'\x00\x00\x00', - tc_reserved: 'int' = 0, - **kwargs: 'Any') -> 'Schema_QualityOfServiceOption': - """Make MH quality-of-service option. - - Args: - type: Option type. - option: Option data model. - sr_id: Service request identifier. - dscp: Differentiated services code point, 6 bits. - oc: Operational code. - attributes: Quality-of-service attributes. - reserved: The three reserved octets as captured. - tc_reserved: The 2 reserved bits of the traffic class, after ``dscp``. - **kwargs: Arbitrary keyword arguments. - - Returns: - Constructed option schema. - - Raises: - ProtocolError: If ``sr_id``, ``dscp``, ``tc_reserved`` or ``oc`` does - not fit its field. - Also if the option is too long for its one-octet ``Option Length``. - Also if ``reserved`` is not three octets. - - .. versionchanged:: 1.6.0 - - Takes ``tc_reserved``, which a data model passed as ``option`` carries - as well (:issue:`2684`). - - """ - if option is not None: - reserved = getattr(option, 'reserved', b'\x00\x00\x00') - sr_id = option.sr_id - dscp = option.dscp - tc_reserved = getattr(option, 'tc_reserved', 0) - oc = option.oc - attributes = option.attributes - else: - attributes = attributes or [] - - dscp = self._make_uint(dscp, bits=6, name='DSCP') - tc_reserved = self._make_uint(tc_reserved, bits=2, name='traffic class reserved bits') - - entries = self._make_qos_attributes(attributes) - length = 6 + sum(len(entry) if isinstance(entry, bytes) else len(entry.pack()) - for entry in entries) - - return Schema_QualityOfServiceOption( - type=type, - length=self._make_uint(length, bits=8, name='Option Length'), - sr_id=self._make_uint(sr_id, bits=8, name='service request identifier'), - tc=dscp << 2 | tc_reserved, - oc=self._make_uint(oc, bits=8, name='operational code'), - reserved=self._make_octets(reserved, size=3, name='reserved octets'), - attributes=entries, - ) - - def _make_opt_lma_up(self, type: 'Enum_Option', option: 'Optional[Data_LMAUserPlaneAddressOption]' = None, *, - address: 'Optional[bytes | str | int | IPv4Address | IPv6Address]' = None, - reserved: 'bytes' = b'\x00\x00', - **kwargs: 'Any') -> 'Schema_LMAUserPlaneAddressOption': - """Make MH LMA user-plane address option. - - Args: - type: Option type. - option: Option data model. - address: LMA user-plane address, or :obj:`None` to omit it -- which is - how a mobile access gateway asks for a transport without naming an - address. - reserved: The two reserved octets as captured. - **kwargs: Arbitrary keyword arguments. - - Returns: - Constructed option schema. - - Raises: - ProtocolError: If ``reserved`` is not two octets. + ProtocolError: If ``reserved`` is not two octets. """ if option is not None: @@ -11238,164 +10023,42 @@ def _make_opt_mcast_ack(self, type: 'Enum_Option', data=data, ) - def _make_lcmp_suboptions( - self, - suboptions: 'LMAControlledMAGSuboption | list[Schema_LMAControlledMAGSuboption | tuple[Enum_LMAControlledMAGSuboption | int, dict[str, Any]] | bytes]', # pylint: disable=line-too-long - ) -> 'list[Schema_LMAControlledMAGSuboption | bytes]': - """Make MH LMA-controlled MAG parameters sub-options. + def _make_opt_lcmp(self, type: 'Enum_Option', option: 'Optional[Data_LMAControlledMAGParametersOption]' = None, *, + suboptions: 'Optional[LMAControlledMAGSuboption | list[Schema_LMAControlledMAGSuboption | tuple[Enum_LMAControlledMAGSuboption | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long + **kwargs: 'Any') -> 'Schema_LMAControlledMAGParametersOption': + """Make MH LMA-controlled MAG parameters option. Args: - suboptions: Sub-options, as parsed data, schemas, ``(type, kwargs)`` - pairs, or raw octets. + type: Option type. + option: Option data model. + suboptions: Sub-options; at least one is required. + **kwargs: Arbitrary keyword arguments. Returns: - Sub-option schema list. + Constructed option schema. Raises: - ProtocolError: If the type of a sub-option, given in a ``(type, - kwargs)`` pair or as the key of parsed data, is a name, a - :obj:`bool` or a code its field cannot carry, or if a - sub-option given as raw octets is empty (:issue:`2723`). + ProtocolError: If the option is too long for its one-octet + ``Option Length``. """ - if isinstance(suboptions, list): - entries = [] # type: list[Schema_LMAControlledMAGSuboption | bytes] - for item in suboptions: - if isinstance(item, bytes) and not item: - raise ProtocolError(f'{self.alias}: invalid sub-option: {item!r} holds no type octet') - if isinstance(item, (bytes, Schema)): - entries.append(cast('Schema_LMAControlledMAGSuboption | bytes', item)) - else: - kind, args = cast( - 'tuple[Enum_LMAControlledMAGSuboption | int, dict[str, Any]]', item) - code = self._make_index(kind, Enum_LMAControlledMAGSuboption, bits=8, - name='sub-option type') - entries.append(self._make_lcmp_suboption(code, **args)) - return entries - - return [self._make_lcmp_suboption( - self._make_index(key, Enum_LMAControlledMAGSuboption, bits=8, - name='sub-option type'), option) - for key, option in suboptions.items(multi=True)] + if option is not None: + suboptions = option.suboptions + else: + suboptions = suboptions or [] - @staticmethod - def _seconds(value: 'int | timedelta', unit: 'int' = 1) -> 'int': - """Return an interval in wire units. + entries = self._make_lcmp_suboptions(suboptions) + if not entries: + raise ProtocolError(f'{self.alias}: [OptNo {type}] at least one sub-option is required') - Args: - value: Interval, already in wire units when an :obj:`int`, or as a - :class:`~datetime.timedelta`. - unit: Number of seconds one wire unit stands for. + length = sum(len(entry) if isinstance(entry, bytes) else len(entry.pack()) + for entry in entries) - Returns: - The interval in wire units. - - """ - if isinstance(value, int): - return value - return math.ceil(value.total_seconds() / unit) - - def _make_lcmp_suboption(self, code: 'Enum_LMAControlledMAGSuboption', - option: 'Optional[Data_LMAControlledMAGSuboption]' = None, - **kwargs: 'Any') -> 'Schema_LMAControlledMAGSuboption': - """Make one MH LMA-controlled MAG parameters sub-option. - - Args: - code: Sub-option type. - option: Sub-option data model. - **kwargs: Sub-option fields, when no data model is given. - - Returns: - Constructed sub-option schema. - - Raises: - ProtocolError: If a timer or count does not fit its field. - - Note: - The data model parameter is named ``option`` rather than ``data`` so - that it cannot shadow a sub-option field of that name -- see - :meth:`_make_fid_suboption`. Dispatch is on ``code`` - rather than on ``isinstance``, for the reason given in - :meth:`_read_fid_suboptions`. - - """ - if code == Enum_LMAControlledMAGSuboption.Binding_Re_registration_Control: - if option is not None: - rereg = cast('Data_BindingReregistrationControlSuboption', option) - start = math.ceil(rereg.start_time.total_seconds() / 4) - initial = math.ceil(rereg.initial_retransmission.total_seconds()) - maximum = math.ceil(rereg.max_retransmission.total_seconds()) - else: - start = self._seconds(kwargs.get('start_time', 0), 4) - initial = self._seconds(kwargs.get('initial_retransmission', 0)) - maximum = self._seconds(kwargs.get('max_retransmission', 0)) - return Schema_BindingReregistrationControlSuboption( - type=code, length=6, - start_time=self._make_uint(start, bits=16, name='re-registration start time'), - initial_retransmission=self._make_uint(initial, bits=16, - name='initial retransmission timer'), - max_retransmission=self._make_uint(maximum, bits=16, - name='maximum retransmission timer')) - - if code == Enum_LMAControlledMAGSuboption.Heartbeat_Control: - if option is not None: - heartbeat = cast('Data_HeartbeatControlSuboption', option) - interval = math.ceil(heartbeat.interval.total_seconds()) - delay = math.ceil(heartbeat.retransmission_delay.total_seconds()) - count = heartbeat.max_retransmissions - else: - interval = self._seconds(kwargs.get('interval', 0)) - delay = self._seconds(kwargs.get('retransmission_delay', 0)) - count = cast('int', kwargs.get('max_retransmissions', 0)) - return Schema_HeartbeatControlSuboption( - type=code, length=6, - interval=self._make_uint(interval, bits=16, name='heartbeat interval'), - retransmission_delay=self._make_uint(delay, bits=16, name='retransmission delay'), - max_retransmissions=self._make_uint(count, bits=16, name='maximum retransmissions')) - - if option is not None: - payload = cast('Data_UnassignedLMAControlledMAGSuboption', option).data - else: - payload = cast('bytes', kwargs.get('data', b'')) - return Schema_UnassignedLMAControlledMAGSuboption( - type=code, length=len(payload), data=payload) - - def _make_opt_lcmp(self, type: 'Enum_Option', option: 'Optional[Data_LMAControlledMAGParametersOption]' = None, *, - suboptions: 'Optional[LMAControlledMAGSuboption | list[Schema_LMAControlledMAGSuboption | tuple[Enum_LMAControlledMAGSuboption | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long - **kwargs: 'Any') -> 'Schema_LMAControlledMAGParametersOption': - """Make MH LMA-controlled MAG parameters option. - - Args: - type: Option type. - option: Option data model. - suboptions: Sub-options; at least one is required. - **kwargs: Arbitrary keyword arguments. - - Returns: - Constructed option schema. - - Raises: - ProtocolError: If the option is too long for its one-octet - ``Option Length``. - - """ - if option is not None: - suboptions = option.suboptions - else: - suboptions = suboptions or [] - - entries = self._make_lcmp_suboptions(suboptions) - if not entries: - raise ProtocolError(f'{self.alias}: [OptNo {type}] at least one sub-option is required') - - length = sum(len(entry) if isinstance(entry, bytes) else len(entry.pack()) - for entry in entries) - - return Schema_LMAControlledMAGParametersOption( - type=type, - length=self._make_uint(length, bits=8, name='Option Length'), - suboptions=entries, - ) + return Schema_LMAControlledMAGParametersOption( + type=type, + length=self._make_uint(length, bits=8, name='Option Length'), + suboptions=entries, + ) def _make_opt_mag_mp(self, type: 'Enum_Option', option: 'Optional[Data_MAGMultipathBindingOption]' = None, *, att: 'Enum_AccessType | int' = Enum_AccessType.Virtual, @@ -11703,73 +10366,6 @@ def _make_opt_dlif_lladdr(self, type: 'Enum_Option', option: 'Optional[Data_DLIF lla=lla, ) - def _make_cga_extensions(self, extensions: 'Extension | list[Schema_CGAExtension | tuple[Enum_CGAExtension | int, dict[str, Any]] | bytes]') -> 'tuple[list[Schema_CGAExtension | bytes], int]': # pylint: disable=line-too-long - """Make CGA extensions for MH. - - Args: - extensions: CGA extensions. - - Returns: - Tuple of extensions and total length of extensions. - - Raises: - ProtocolError: If the type of an extension, given in a ``(type, - kwargs)`` pair or as the key of parsed data, is a name, a - :obj:`bool` or a code its field cannot carry, or if an - extension given as raw octets is empty (:issue:`2723`). - - """ - total_length = 0 - if isinstance(extensions, list): - extensions_list = [] # type: list[Schema_CGAExtension | bytes] - for schema in extensions: - if isinstance(schema, bytes): - if not schema: - raise ProtocolError(f'{self.alias}: invalid CGA extension: {schema!r} holds no type field') - code = Enum_CGAExtension.get(int.from_bytes(schema[0:2], 'big', signed=False)) - - data = schema # type: Schema_CGAExtension | bytes - data_len = len(data) - elif isinstance(schema, Schema): - data = schema - data_len = len(schema.pack()) - else: - kind, args = cast('tuple[Enum_CGAExtension | int, dict[str, Any]]', schema) - code = self._make_index(kind, Enum_CGAExtension, bits=16, - name='extension type') - name = self._lookup_registry(self.__extension__, code) - if isinstance(name, str): - meth_name = f'_make_ext_{name}' - meth = cast('ExtensionConstructor', - getattr(self, meth_name, self._make_ext_none)) - else: - meth = name[1] - - data = meth(code, **args) - data_len = len(data.pack()) - - extensions_list.append(data) - total_length += data_len - return extensions_list, total_length - - extensions_list = [] - for key, extension in extensions.items(multi=True): - code = self._make_index(key, Enum_CGAExtension, bits=16, name='extension type') - name = self._lookup_registry(self.__extension__, code) - if isinstance(name, str): - meth_name = f'_make_ext_{name}' - meth = cast('ExtensionConstructor', - getattr(self, meth_name, self._make_ext_none)) - else: - meth = name[1] - - data = meth(code, extension) - data_len = len(data.pack()) - - extensions_list.append(data) - total_length += data_len - return extensions_list, total_length - def _make_ext_none(self, type: 'Enum_CGAExtension', option: 'Optional[Data_UnknownExtension]' = None, *, data: 'bytes' = b'', **kwargs: 'Any') -> 'Schema_UnknownExtension': @@ -11872,3 +10468,1407 @@ def _make_ext_exp(self, type: 'Enum_CGAExtension', option: 'Optional[Data_Experi length=len(data), data=data, ) + + @staticmethod + def _mh_message_length(header_len: 'int') -> 'int': + """Compute the total on-the-wire MH header length for a given ``Header Len``. + + Per :rfc:`6275#section-6.1.1`, ``Header Len`` is *"the length of the + Mobility Header, in units of 8 octets, excluding the first 8 + octets"* -- i.e. the total header is ``8 + 8 * header_len`` octets, + or equivalently ``(header_len + 1) * 8``. Every ``_read_msg_*`` + below reports this value as the parsed message's own ``.length``, + which :meth:`~pcapkit.protocols.internet.mh.protocol.MH.read` subtracts from the + outer packet length to find the next layer's length. It is the inverse + of :meth:`make`'s ``(len(data_val) + 6) // 8 - 1``. Do NOT drop the + ``+ 1``: the units either side of it differ (octets vs. 8-octet + units), and dropping the offset silently reinterprets the field. + + Args: + header_len: raw ``Header Len`` field value, as read off the wire. + + Returns: + Total length, in octets, of the on-the-wire MH header. + + """ + return (header_len + 1) * 8 + + @staticmethod + def _mh_option_length(schema_length: 'int') -> 'int': + """Compute an MH option's whole-option length from its on-the-wire ``Option Length``. + + Per :rfc:`6275#section-6.2`, an MH option's ``Option Length`` field + (what each ``Schema_*Option.length`` here holds) counts *"the length + of the option, in octets, excluding the Option Type and Option + Length fields"* -- so the whole option, which is what every + ``_read_opt_*`` below reports back as the parsed option's own + ``.length``, is two octets more. The read-side call sites share this + helper so the arithmetic lives in one place. The exceptions are the + options whose ``Option Length`` is not that octet count: a ``Pad1`` + option, which has no ``Option Length`` field at all (see the ``Note:`` + on :meth:`_read_opt_pad` below), the BADF option, whose length leaves + out its SPI (see :meth:`_read_opt_badf`), and the multicast mobility + and acknowledgement options, whose length counts 32-bit words (see + :meth:`_read_opt_mcast`). Do NOT drop the ``+ 2``. + + The ``+ 2`` is specific to an :rfc:`6275#section-6.2` mobility + option, whose Option Type and Option Length are one octet each. It + is **not** a universal contract for everything this class parses: + a CGA extension's Extension Type and Extension Data Length are two + octets each [:rfc:`4581#section-2`], so those readers use + :meth:`_mh_extension_length` instead. Reusing this helper for them + would report every parsed CGA extension two octets short. + + Note that only the *stored-length* read-side call sites are + collected here -- most ``_make_opt_*`` methods recompute the wire + ``Option Length`` from ``len(value)`` rather than reading a parsed + ``.length`` back, so they have nothing to unify against this helper. + + Args: + schema_length: raw ``Option Length`` field value, as read off the wire. + + Returns: + Whole-option length, in octets, including the Option Type and + Option Length fields. + + """ + return schema_length + 2 + + def _read_mh_options(self, options_schema: 'list[Schema_Option]') -> 'Option': + """Read MH options. + + Structure of MH option [:rfc:`6275`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Type | Option Length | Option Data... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + options_schema: Parsed MH options. + + Returns: + Parsed MH options data. + + Raises: + ProtocolError: If an option read by a ``_read_opt_*`` method has fields + that do not take exactly the octets its ``Option Length`` declares + [:rfc:`6275#section-6.2.1`]. A parser registered as a callable + reports its own length, and is not checked. Or if the capture cut + an option short and its parser refuses the zeros it lacks, or its + maker does not write the octets captured back. + + .. versionchanged:: 1.6.0 + + * Such an option is refused, rather than read from octets outside it + and rebuilt with the length its fields give (:issue:`2684`). + * An option the capture cut short is read, its missing octets as + zeros, if its maker writes the octets captured back + (:issue:`2694`). + + """ + options = OrderedMultiDict() # type: Option + + for schema in options_schema: + type = schema.type + name = self._lookup_registry(self.__option__, type) + + if isinstance(name, str): + meth_name = f'_read_opt_{name}' + meth = cast('OptionParser', + getattr(self, meth_name, self._read_opt_none)) + maker = cast('OptionConstructor', + getattr(self, f'_make_opt_{name}', self._make_opt_none)) + else: + meth, maker = name + + # NOTE: The capture cut this option short, as its short read records, so + # the octets it lacks read as zeros and it is short of its length. It + # stands if its parser takes those zeros and its maker writes the octets + # captured back (#2694); otherwise it is refused, as an option running + # past the data is (#1325). + if '__short_read__' in schema.__dict__: + try: + with CaughtByCaller(BaseError): + data = meth(schema, options=options) + made = maker(type, data).pack()[:len(schema)] + except BaseError as error: + raise ProtocolError(f'{self.alias}: [OptNo {type}] option cut short: {error}') from error + if made != bytes(schema): + raise ProtocolError(f'{self.alias}: [OptNo {type}] option cut short does not ' + 'rebuild as captured') + else: + data = meth(schema, options=options) + + # NOTE: Fields are read at their own widths, so a length they do not + # fill exactly reads octets not the option's, and rebuilds as another + # (#2684). + if isinstance(name, str) and len(schema) != data.length: + raise ProtocolError(f'{self.alias}: [OptNo {type}] invalid option length: {schema.length}') + + # record option data + options.add(type, data) + + return options + + def _read_fid_suboptions( + self, suboptions_schema: 'list[Schema_FlowIdentificationSuboption]', + ) -> 'FlowIDSuboption': + """Read MH flow identification sub-options. + + Structure of MH flow identification sub-option [:rfc:`6089#section-4.2.1`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Sub-Opt Type |Sub-Opt Length | Sub-Option Data... + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + These are sub-options of one mobility option rather than mobility options + in their own right, so they are dispatched here rather than through + :attr:`self.__option__ `. + + Note: + Dispatch is on the sub-option **type code** rather than on + ``isinstance`` of the schema class: the code is on the wire and the + registry keys on it, so it is the cheaper discriminator. A class test + would also be sound, since every + :class:`~pcapkit.protocols.schema.Schema` subclass carries its + own :class:`abc.ABCMeta` cache (:issue:`439`). The same reasoning + applies to :meth:`_read_ani_suboptions`, + :meth:`_read_qos_attributes`, :meth:`_read_lcmp_suboptions` and their + constructor counterparts. + + Args: + suboptions_schema: Parsed sub-option schemas. + + Returns: + Parsed sub-option data. + + Raises: + ProtocolError: If a sub-option's fields do not take exactly the octets + its ``Sub-Opt Length`` declares [:rfc:`6089#section-4.2.1`], or a + target care-of address is neither IPv4 nor IPv6 + [:rfc:`7109#section-6.2.2`], as the binding identifier option's + is refused. + + .. versionchanged:: 1.6.0 + + Such a sub-option is refused, as a mobility option is by + :meth:`_read_mh_options`, rather than read from octets outside it or + left unable to rebuild (:issue:`2684`). + + """ + suboptions = OrderedMultiDict() # type: FlowIDSuboption + + for schema in suboptions_schema: + code = schema.type + + if code in (Enum_FlowIDSuboption.Pad, Enum_FlowIDSuboption.PadN): + pad = cast('Schema_PadFlowIdentificationSuboption', schema) + size = 1 if code == Enum_FlowIDSuboption.Pad else self._mh_option_length(pad.length) + data = Data_PadFlowIdentificationSuboption( + type=code, + length=size, + data=pad.data, + ) # type: Data_FlowIdentificationSuboption + elif code == Enum_FlowIDSuboption.BID_Reference: + bid_ref = cast('Schema_BIDReferenceSuboption', schema) + data = Data_BIDReferenceSuboption( + type=code, + length=self._mh_option_length(bid_ref.length), + bid=tuple(bid_ref.bid), + ) + elif code == Enum_FlowIDSuboption.Traffic_Selector: + selector = cast('Schema_TrafficSelectorSuboption', schema) + data = Data_TrafficSelectorSuboption( + type=code, + length=self._mh_option_length(selector.length), + ts_format=selector.ts_format, + reserved=selector.reserved, + selector=selector.selector, + ) + elif code == Enum_FlowIDSuboption.Flow_Binding_Action: + action = cast('Schema_FlowBindingActionSuboption', schema) + data = Data_FlowBindingActionSuboption( + type=code, + length=self._mh_option_length(action.length), + reserved=action.reserved, + action=action.action, + ) + elif code == Enum_FlowIDSuboption.Target_Care_of_Address: + target = cast('Schema_TargetCareofAddressSuboption', schema) + if target.length not in (6, 18): + raise ProtocolError(f'{self.alias}: [FID SubOpt {code}] invalid sub-option length: {target.length}') + data = Data_TargetCareofAddressSuboption( + type=code, + length=self._mh_option_length(target.length), + reserved=target.reserved, + address=target.address, + ) + else: + unknown = cast('Schema_UnassignedFlowIdentificationSuboption', schema) + data = Data_UnassignedFlowIdentificationSuboption( + type=code, + length=self._mh_option_length(unknown.length), + data=unknown.data, + ) + + if len(schema) != data.length: + raise ProtocolError(f'{self.alias}: [FID SubOpt {code}] invalid sub-option length: {schema.length}') + suboptions.add(code, data) + + return suboptions + + def _read_ani_suboptions( + self, suboptions_schema: 'list[Schema_ANISuboption]', + ) -> 'ANISuboption': + """Read MH access network identifier sub-options. + + Structure of MH access network identifier sub-option + [:rfc:`6757#section-3.1`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | ANI Type | ANI Length | Option Data ~ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + The geo-location degrees of :rfc:`6757#section-3.1.2` are 24-bit + **two's-complement** fixed-point values with nine integer bits. A + :class:`~pcapkit.corekit.fields.strings.BitField` reads them unsigned, so + the sign is applied here by :meth:`_read_sint`, and both the decoded + degrees and the raw signed integers are recorded -- the latter so that + the exact wire value survives a round trip through the data model. + + Note: + Dispatch is on the sub-option type code rather than on ``isinstance`` + of the schema class, for the reason given in + :meth:`_read_fid_suboptions`. + + Args: + suboptions_schema: Parsed sub-option schemas. + + Returns: + Parsed sub-option data. + + Raises: + ProtocolError: If a sub-option's fields do not take exactly the octets + its ``ANI Length`` declares [:rfc:`6757#section-3.1`]. + + .. versionchanged:: 1.6.0 + + Such a sub-option is refused, as a mobility option is by + :meth:`_read_mh_options` (:issue:`2684`). + + """ + suboptions = OrderedMultiDict() # type: ANISuboption + + for schema in suboptions_schema: + code = schema.type + + if code == Enum_ANISuboption.Network_Identifier: + net = cast('Schema_ANINetworkIdentifierSuboption', schema) + data = Data_ANINetworkIdentifierSuboption( + type=code, + length=self._mh_option_length(net.length), + utf8=bool(net.flags['E']), + reserved=net.flags['reserved'], + net_name=net.net_name, + ap_name=net.ap_name, + ) # type: Data_ANISuboption + elif code == Enum_ANISuboption.Geo_Location: + geo = cast('Schema_ANIGeoLocationSuboption', schema) + raw_lat = self._read_sint(geo.location['latitude'], 24) + raw_lon = self._read_sint(geo.location['longitude'], 24) + data = Data_ANIGeoLocationSuboption( + type=code, + length=self._mh_option_length(geo.length), + latitude=raw_lat / 2 ** 15, + longitude=raw_lon / 2 ** 15, + raw_latitude=raw_lat, + raw_longitude=raw_lon, + ) + elif code == Enum_ANISuboption.Operator_Identifier: + operator = cast('Schema_ANIOperatorIdentifierSuboption', schema) + data = Data_ANIOperatorIdentifierSuboption( + type=code, + length=self._mh_option_length(operator.length), + op_id_type=operator.op_id_type, + identifier=operator.identifier, + ) + elif code == Enum_ANISuboption.Civic_Location: + civic = cast('Schema_ANICivicLocationSuboption', schema) + data = Data_ANICivicLocationSuboption( + type=code, + length=self._mh_option_length(civic.length), + format=civic.format, + reserved=civic.reserved, + location=civic.location, + ) + elif code == Enum_ANISuboption.MAG_Group_Identifier: + group = cast('Schema_ANIMAGGroupIdentifierSuboption', schema) + data = Data_ANIMAGGroupIdentifierSuboption( + type=code, + length=self._mh_option_length(group.length), + group_id=group.group_id, + ) + elif code == Enum_ANISuboption.ANI_Update_Timer: + timer = cast('Schema_ANIUpdateTimerSuboption', schema) + data = Data_ANIUpdateTimerSuboption( + type=code, + length=self._mh_option_length(timer.length), + timer=datetime.timedelta(seconds=timer.timer * 4), + ) + else: + unknown = cast('Schema_UnassignedANISuboption', schema) + data = Data_UnassignedANISuboption( + type=code, + length=self._mh_option_length(unknown.length), + data=unknown.data, + ) + + if len(schema) != data.length: + raise ProtocolError(f'{self.alias}: [ANI SubOpt {code}] invalid sub-option length: {schema.length}') + suboptions.add(code, data) + + return suboptions + + def _read_qos_attributes( + self, attributes_schema: 'list[Schema_QoSAttribute]', + ) -> 'QoSAttribute': + """Read MH quality-of-service attributes. + + Structure of MH quality-of-service attribute [:rfc:`7222#section-4.2`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type | Length | Value ~ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + Every bit rate here is in **bits** per second [:rfc:`7222#section-4.2.1`], + not the kilobytes per second of the load information option's capacity + fields. + + Dispatch is on the attribute type code rather than on ``isinstance`` of + the schema class, for the reason given in + :meth:`_read_fid_suboptions`. + + Args: + attributes_schema: Parsed attribute schemas. + + Returns: + Parsed attribute data. + + Raises: + ProtocolError: If an attribute's fields do not take exactly the octets + its ``Length`` declares [:rfc:`7222#section-4.2`]. + + .. versionchanged:: 1.6.0 + + Such an attribute is refused, as a mobility option is by + :meth:`_read_mh_options` (:issue:`2684`). + + """ + attributes = OrderedMultiDict() # type: QoSAttribute + + for schema in attributes_schema: + code = schema.type + + if code in (Enum_QoSAttribute.Per_Session_Agg_Max_DL_Bit_Rate, + Enum_QoSAttribute.Per_Session_Agg_Max_UL_Bit_Rate): + session = cast('Schema_PerSessionBitRateAttribute', schema) + data = Data_PerSessionBitRateAttribute( + type=code, + length=self._mh_option_length(session.length), + service=bool(session.flags['S']), + exclude=bool(session.flags['E']), + reserved=session.flags['reserved'], + rate=session.rate, + ) # type: Data_QoSAttribute + elif code in (Enum_QoSAttribute.Per_MN_Agg_Max_DL_Bit_Rate, + Enum_QoSAttribute.Per_MN_Agg_Max_UL_Bit_Rate, + Enum_QoSAttribute.Aggregate_Max_DL_Bit_Rate, + Enum_QoSAttribute.Aggregate_Max_UL_Bit_Rate, + Enum_QoSAttribute.Guaranteed_DL_Bit_Rate, + Enum_QoSAttribute.Guaranteed_UL_Bit_Rate): + rate = cast('Schema_BitRateAttribute', schema) + data = Data_BitRateAttribute( + type=code, + length=self._mh_option_length(rate.length), + reserved=rate.reserved, + rate=rate.rate, + ) + elif code == Enum_QoSAttribute.Allocation_Retention_Priority: + arp = cast('Schema_AllocationRetentionPriorityAttribute', schema) + data = Data_AllocationRetentionPriorityAttribute( + type=code, + length=self._mh_option_length(arp.length), + reserved=arp.reserved, + priority_level=arp.priority['PL'], + preemption_capability=arp.priority['PC'], + preemption_vulnerability=arp.priority['PV'], + ) + elif code == Enum_QoSAttribute.QoS_Traffic_Selector: + selector = cast('Schema_QoSTrafficSelectorAttribute', schema) + data = Data_QoSTrafficSelectorAttribute( + type=code, + length=self._mh_option_length(selector.length), + reserved=selector.reserved, + ts_format=selector.ts_format, + selector=selector.selector, + ) + elif code == Enum_QoSAttribute.QoS_Vendor_Specific_Attribute: + vendor = cast('Schema_QoSVendorSpecificAttribute', schema) + data = Data_QoSVendorSpecificAttribute( + type=code, + length=self._mh_option_length(vendor.length), + reserved=vendor.reserved, + vendor=vendor.vendor, + subtype=vendor.subtype, + data=vendor.data, + ) + else: + unknown = cast('Schema_UnassignedQoSAttribute', schema) + data = Data_UnassignedQoSAttribute( + type=code, + length=self._mh_option_length(unknown.length), + data=unknown.data, + ) + + if len(schema) != data.length: + raise ProtocolError(f'{self.alias}: [QoS Attr {code}] invalid attribute length: {schema.length}') + attributes.add(code, data) + + return attributes + + def _read_lcmp_suboptions( + self, suboptions_schema: 'list[Schema_LMAControlledMAGSuboption]', + ) -> 'LMAControlledMAGSuboption': + """Read MH LMA-controlled MAG parameters sub-options. + + Structure of MH LMA-controlled MAG parameters sub-option + [:rfc:`8127#section-3.1`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | LCMP Type | LCMP Length | Sub-Option Data ~ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Note: + Only the re-registration start time is in units of 4 seconds; every + other interval in these two sub-options is in whole seconds + [:rfc:`8127#section-3.1.1`, :rfc:`8127#section-3.1.2`]. + + Dispatch is on the sub-option type code rather than on ``isinstance`` + of the schema class, for the reason given in + :meth:`_read_fid_suboptions`. + + Args: + suboptions_schema: Parsed sub-option schemas. + + Returns: + Parsed sub-option data. + + Raises: + ProtocolError: If a sub-option's fields do not take exactly the octets + its ``LCMP Length`` declares [:rfc:`8127#section-3.1`]. + + .. versionchanged:: 1.6.0 + + Such a sub-option is refused, as a mobility option is by + :meth:`_read_mh_options` (:issue:`2684`). + + """ + suboptions = OrderedMultiDict() # type: LMAControlledMAGSuboption + + for schema in suboptions_schema: + code = schema.type + + if code == Enum_LMAControlledMAGSuboption.Binding_Re_registration_Control: + rereg = cast('Schema_BindingReregistrationControlSuboption', schema) + data = Data_BindingReregistrationControlSuboption( + type=code, + length=self._mh_option_length(rereg.length), + start_time=datetime.timedelta(seconds=rereg.start_time * 4), + initial_retransmission=datetime.timedelta( + seconds=rereg.initial_retransmission), + max_retransmission=datetime.timedelta(seconds=rereg.max_retransmission), + ) # type: Data_LMAControlledMAGSuboption + elif code == Enum_LMAControlledMAGSuboption.Heartbeat_Control: + heartbeat = cast('Schema_HeartbeatControlSuboption', schema) + data = Data_HeartbeatControlSuboption( + type=code, + length=self._mh_option_length(heartbeat.length), + interval=datetime.timedelta(seconds=heartbeat.interval), + retransmission_delay=datetime.timedelta( + seconds=heartbeat.retransmission_delay), + max_retransmissions=heartbeat.max_retransmissions, + ) + else: + unknown = cast('Schema_UnassignedLMAControlledMAGSuboption', schema) + data = Data_UnassignedLMAControlledMAGSuboption( + type=code, + length=self._mh_option_length(unknown.length), + data=unknown.data, + ) + + if len(schema) != data.length: + raise ProtocolError(f'{self.alias}: [LCMP SubOpt {code}] invalid sub-option length: {schema.length}') + suboptions.add(code, data) + + return suboptions + + @staticmethod + def _mh_extension_length(schema_length: 'int') -> 'int': + """Compute a CGA extension's whole-structure length from its on-the-wire ``Extension Data Length``. + + This is the CGA-extension counterpart of :meth:`_mh_option_length`, and + it deliberately adds ``4`` rather than ``2``. A CGA extension is **not** + an :rfc:`6275#section-6.2` mobility option: per :rfc:`4581#section-2`, + which defines the TLV format and formally updates :rfc:`3972`, its + ``Extension Type`` is a *"16-bit identifier of the type of the Extension + Field"* and its ``Extension Data Length`` a *"16-bit unsigned integer. + Length of the Extension Data field of this option, in octets"*. So the + fixed header is two 2-octet fields, not two 1-octet ones, and the whole + structure is ``4`` octets more than the stored length -- a fact + :rfc:`5535#section-5` states from the other direction for the one + non-experimental assigned type, whose ``Ext Len`` is the *"[l]ength of + the Extension in octets, not including the first 4 octets"*. + + Passing these lengths through :meth:`_mh_option_length` would report + every parsed CGA extension two octets short: an 8-octet extension with + an ``Extension Data Length`` of ``4`` would come back as ``6``. The write + side is unaffected, since :meth:`_make_cga_extensions` measures + ``len(schema.pack())`` instead. + + Args: + schema_length: raw ``Extension Data Length`` field value, as read off the wire. + + Returns: + Whole-extension length, in octets, including the Extension Type and + Extension Data Length fields. + + """ + return schema_length + 4 + + def _read_cga_extensions(self, extensions_schema: 'list[Schema_CGAExtension]') -> 'Extension': + """Read CGA extensions. + + Structure of CGA extensions [:rfc:`4581`]: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Extension Type | Extension Data Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + ~ Extension Data ~ + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Args: + extensions_schema: Parsed CGA extensions. + + Returns: + Parsed CGA extensions data. + + Raises: + ProtocolError: If an extension read by a ``_read_ext_*`` method has + fields that do not take exactly the octets its ``Extension Data + Length`` declares [:rfc:`4581#section-2`]; a parser registered as + a callable is not checked. + + .. versionchanged:: 1.6.0 + + Such an extension is refused, as a mobility option is by + :meth:`_read_mh_options` (:issue:`2684`). + + """ + extensions = OrderedMultiDict() # type: Extension + + for schema in extensions_schema: + type = schema.type + name = self._lookup_registry(self.__extension__, type) + + if isinstance(name, str): + meth_name = f'_read_ext_{name}' + meth = cast('ExtensionParser', + getattr(self, meth_name, self._read_ext_none)) + else: + meth = name[0] + data = meth(schema, extensions=extensions) + if isinstance(name, str) and len(schema) != data.length: + raise ProtocolError(f'{self.alias}: [CGA Ext {type}] invalid extension length: {schema.length}') + + # record extension data + extensions.add(type, data) + + return extensions + + def _make_pad_options(self, offset: 'int') -> 'tuple[list[Schema_PadOption], int]': + """Make the padding options needed to align the header to 8 octets. + + Args: + offset: Number of octets emitted so far, **counting the six octets + of the fixed header** (``Payload Proto``, ``Header Len``, + ``MH Type``, ``Reserved`` and ``Checksum``) as well as the whole + message data. + + Returns: + Tuple of the padding option schemas and the number of octets they + occupy. + + Note: + The Mobility Header is a multiple of 8 octets and + :attr:`~pcapkit.protocols.internet.mh.schema.MH.length` counts those + units less one [:rfc:`6275#section-6.1.1`]. Its fixed part is + **six** octets, not the two of an IPv6 options header, so it is + ``len(data) + 6`` that has to be aligned -- which means the message + data is well formed only when it is 2 octets past a multiple of 8, + a different modulus from the one the extension headers use. + + A ``PadN`` option spends two octets on its own type and + ``Option Length`` fields before any padding data, so occupying + ``pad_len`` octets means an ``Option Length`` of ``pad_len - 2``. + One octet is padded with a ``Pad1`` option instead, that being the + only form which can occupy a single octet. Two octets are one + ``PadN`` with an ``Option Length`` of ``0``, not two ``Pad1``: + :rfc:`6275#section-6.2.2` says a ``PadN`` should be used rather than + multiple ``Pad1`` options. + + """ + pad_len = pad_length(offset, 8) + if pad_len == 0: + return [], 0 + if pad_len == 1: + return [self._make_opt_pad(Enum_Option.Pad1, length=0)], pad_len # type: ignore[arg-type] + return [self._make_opt_pad(Enum_Option.PadN, length=pad_len - 2)], pad_len # type: ignore[arg-type] + + def _make_mh_options(self, options: 'Option | list[Schema_Option | tuple[Enum_Option | int, dict[str, Any]] | bytes]') -> 'list[Schema_Option | bytes]': # pylint: disable=line-too-long + """Make options for MH. + + Args: + options: MH options. + + Returns: + Mobility options list. + + Raises: + ProtocolError: If the type of an option, given in a ``(type, + kwargs)`` pair or as the key of parsed data, is a name, a + :obj:`bool` or a code its field cannot carry, or if an option + given as raw octets is empty (:issue:`2723`). + + """ + if isinstance(options, list): + options_list = [] # type: list[Schema_Option | bytes] + for schema in options: + if isinstance(schema, bytes): + if not schema: + raise ProtocolError(f'{self.alias}: invalid option: {schema!r} holds no type octet') + code = Enum_Option.get(int.from_bytes(schema[0:1], 'big', signed=False)) + + data = schema # type: Schema_Option | bytes + elif isinstance(schema, Schema): + data = schema + else: + kind, args = cast('tuple[Enum_Option | int, dict[str, Any]]', schema) + code = self._make_index(kind, Enum_Option, bits=8, name='option type') + name = self._lookup_registry(self.__option__, code) + if isinstance(name, str): + meth_name = f'_make_opt_{name}' + meth = cast('OptionConstructor', + getattr(self, meth_name, self._make_opt_none)) + else: + meth = name[1] + data = meth(code, **args) + + options_list.append(data) + return options_list + + options_list = [] + for key, option in options.items(multi=True): + code = self._make_index(key, Enum_Option, bits=8, name='option type') + name = self._lookup_registry(self.__option__, code) + if isinstance(name, str): + meth_name = f'_make_opt_{name}' + meth = cast('OptionConstructor', + getattr(self, meth_name, self._make_opt_none)) + else: + meth = name[1] + + data = meth(code, option) + options_list.append(data) + return options_list + + def _make_fid_suboptions( + self, + suboptions: 'FlowIDSuboption | list[Schema_FlowIdentificationSuboption | tuple[Enum_FlowIDSuboption | int, dict[str, Any]] | bytes]', # pylint: disable=line-too-long + ) -> 'list[Schema_FlowIdentificationSuboption | bytes]': + """Make MH flow identification sub-options. + + Args: + suboptions: Sub-options, as parsed data, schemas, ``(type, kwargs)`` + pairs, or raw octets. + + Returns: + Sub-option schema list. + + Raises: + ProtocolError: If the type of a sub-option, given in a ``(type, + kwargs)`` pair or as the key of parsed data, is a name, a + :obj:`bool` or a code its field cannot carry, or if a + sub-option given as raw octets is empty (:issue:`2723`). + + """ + if isinstance(suboptions, list): + entries = [] # type: list[Schema_FlowIdentificationSuboption | bytes] + for item in suboptions: + if isinstance(item, bytes) and not item: + raise ProtocolError(f'{self.alias}: invalid sub-option: {item!r} holds no type octet') + if isinstance(item, (bytes, Schema)): + entries.append(cast('Schema_FlowIdentificationSuboption | bytes', item)) + else: + kind, args = cast('tuple[Enum_FlowIDSuboption | int, dict[str, Any]]', item) + code = self._make_index(kind, Enum_FlowIDSuboption, bits=8, name='sub-option type') + entries.append(self._make_fid_suboption(code, **args)) + return entries + + return [self._make_fid_suboption( + self._make_index(key, Enum_FlowIDSuboption, bits=8, name='sub-option type'), option) + for key, option in suboptions.items(multi=True)] + + def _make_fid_suboption(self, code: 'Enum_FlowIDSuboption', + option: 'Optional[Data_FlowIdentificationSuboption]' = None, + **kwargs: 'Any') -> 'Schema_FlowIdentificationSuboption': + """Make one MH flow identification sub-option. + + Args: + code: Sub-option type. + option: Sub-option data model. + **kwargs: Sub-option fields, when no data model is given. + + Returns: + Constructed sub-option schema. + + Raises: + ProtocolError: If an entry of a ``BID_Reference`` sub-option's ``bid`` does + not fit its field, a ``ts_format`` or ``action`` is a name, a + :obj:`bool` or a code its field cannot carry, or a sub-option's + ``reserved`` is not as many octets as its field. + + Note: + The data model parameter is named ``option`` rather than ``data`` + to match :meth:`_make_opt_fid` and the rest of this module -- and + because ``data`` would **shadow a field**. The traffic selector and + unassigned sub-options both carry a field of their own called + ``data``, so a caller's ``data=`` would bind to the model parameter + instead of landing in ``**kwargs``, and the fallback that reads + ``data`` from ``**kwargs`` would never see it: the payload would be + silently dropped and the length written as though it were empty, + with no exception. + + Dispatch is on ``code`` rather than on ``isinstance`` of the schema or + data class, for the reason given in :meth:`_read_fid_suboptions`. + + """ + if code in (Enum_FlowIDSuboption.Pad, Enum_FlowIDSuboption.PadN): + if code == Enum_FlowIDSuboption.Pad: + pad_len = 0 + elif option is not None: + pad_len = option.length - 2 + else: + pad_len = cast('int', kwargs.get('length', 0)) + if option is not None: + pad_data = getattr(option, 'data', b'') + else: + pad_data = cast('bytes', kwargs.get('data', b'')) + return Schema_PadFlowIdentificationSuboption(type=code, length=pad_len, data=pad_data) + + if code == Enum_FlowIDSuboption.BID_Reference: + if option is not None: + bid = list(cast('Data_BIDReferenceSuboption', option).bid) + else: + bid = cast('list[int]', kwargs.get('bid') or []) + for item in bid: + self._make_uint(item, bits=16, name='binding identifier') + return Schema_BIDReferenceSuboption(type=code, length=len(bid) * 2, bid=bid) + + if code == Enum_FlowIDSuboption.Traffic_Selector: + if option is not None: + selector_opt = cast('Data_TrafficSelectorSuboption', option) + ts_format = selector_opt.ts_format # type: Enum_TrafficSelector | int + selector = selector_opt.selector + reserved = getattr(selector_opt, 'reserved', b'\x00') + else: + ts_format = self._make_index( + kwargs.get('ts_format', Enum_TrafficSelector.IPv6_Binary_Traffic_Selector), + Enum_TrafficSelector, bits=8, name='TS format') + selector = cast('bytes', kwargs.get('selector', b'')) + reserved = cast('bytes', kwargs.get('reserved', b'\x00')) + return Schema_TrafficSelectorSuboption( + type=code, length=2 + len(selector), + ts_format=cast('Enum_TrafficSelector', ts_format), + reserved=self._make_octets(reserved, size=1, name='reserved octets'), + selector=selector) + + if code == Enum_FlowIDSuboption.Flow_Binding_Action: + if option is not None: + action = cast('Data_FlowBindingActionSuboption', + option).action # type: Enum_FlowBindingAction | int + reserved = getattr(option, 'reserved', b'\x00') + else: + action = self._make_index(kwargs.get('action', Enum_FlowBindingAction.Add), + Enum_FlowBindingAction, bits=8, name='action') + reserved = cast('bytes', kwargs.get('reserved', b'\x00')) + return Schema_FlowBindingActionSuboption( + type=code, length=2, reserved=self._make_octets(reserved, size=1, name='reserved octets'), + action=cast('Enum_FlowBindingAction', action)) + + if code == Enum_FlowIDSuboption.Target_Care_of_Address: + if option is not None: + address = cast('Data_TargetCareofAddressSuboption', option).address # type: Any + reserved = getattr(option, 'reserved', b'\x00\x00') + else: + address = kwargs.get('address', '::') + reserved = cast('bytes', kwargs.get('reserved', b'\x00\x00')) + # NOTE: Through ``parse_ip_address`` because the sub-option length + # below is derived from the family here, ahead of the schema, so a + # bare ``ipaddress.ip_address`` would launder a ``bool`` past the + # schema's guard, packing ``address=True`` as ``0506000000000001``. + addr = parse_ip_address( + address, f'{self.alias}: [OptNo {code}] invalid target care-of address') + return Schema_TargetCareofAddressSuboption( + type=code, length=6 if addr.version == 4 else 18, + reserved=self._make_octets(reserved, size=2, name='reserved octets'), + address=addr) + + if option is not None: + payload = cast('Data_UnassignedFlowIdentificationSuboption', option).data + else: + payload = cast('bytes', kwargs.get('data', b'')) + return Schema_UnassignedFlowIdentificationSuboption( + type=code, length=len(payload), data=payload) + + def _make_ani_suboptions( + self, + suboptions: 'ANISuboption | list[Schema_ANISuboption | tuple[Enum_ANISuboption | int, dict[str, Any]] | bytes]', + ) -> 'list[Schema_ANISuboption | bytes]': + """Make MH access network identifier sub-options. + + Args: + suboptions: Sub-options, as parsed data, schemas, ``(type, kwargs)`` + pairs, or raw octets. + + Returns: + Sub-option schema list. + + Raises: + ProtocolError: If the type of a sub-option, given in a ``(type, + kwargs)`` pair or as the key of parsed data, is a name, a + :obj:`bool` or a code its field cannot carry, or if a + sub-option given as raw octets is empty (:issue:`2723`). + + """ + if isinstance(suboptions, list): + entries = [] # type: list[Schema_ANISuboption | bytes] + for item in suboptions: + if isinstance(item, bytes) and not item: + raise ProtocolError(f'{self.alias}: invalid sub-option: {item!r} holds no type octet') + if isinstance(item, (bytes, Schema)): + entries.append(cast('Schema_ANISuboption | bytes', item)) + else: + kind, args = cast('tuple[Enum_ANISuboption | int, dict[str, Any]]', item) + code = self._make_index(kind, Enum_ANISuboption, bits=8, name='sub-option type') + entries.append(self._make_ani_suboption(code, **args)) + return entries + + return [self._make_ani_suboption( + self._make_index(key, Enum_ANISuboption, bits=8, name='sub-option type'), option) + for key, option in suboptions.items(multi=True)] + + def _make_ani_suboption(self, code: 'Enum_ANISuboption', + option: 'Optional[Data_ANISuboption]' = None, + **kwargs: 'Any') -> 'Schema_ANISuboption': + """Make one MH access network identifier sub-option. + + Args: + code: Sub-option type. + option: Sub-option data model. + **kwargs: Sub-option fields, when no data model is given. + + Returns: + Constructed sub-option schema. + + Raises: + ProtocolError: If a raw geo-location degree does not fit in 24 + bits as two's complement, or a sub-option's ``reserved``, + ``format``, ``group_id`` or ``timer`` does not fit its field, or + ``op_id_type`` is a name, a :obj:`bool` or a code its field + cannot carry. + + Note: + The geo-location degrees are re-encoded from the **raw** signed + integers rather than from the decoded floats, since a float cannot + always be converted back to the same 24-bit fixed-point value, and + :meth:`_make_sint` gives back the bits :meth:`_read_sint` read. + + The data model parameter is named ``option`` rather than ``data`` so + that it cannot shadow a sub-option field of that name -- see + :meth:`_make_fid_suboption`. Dispatch is on ``code`` + rather than on ``isinstance``, for the reason given in + :meth:`_read_fid_suboptions`. + + """ + if code == Enum_ANISuboption.Network_Identifier: + if option is not None: + net = cast('Data_ANINetworkIdentifierSuboption', option) + utf8, net_name, ap_name = net.utf8, net.net_name, net.ap_name + reserved = net.reserved + else: + utf8 = cast('bool', kwargs.get('utf8', False)) + reserved = cast('int', kwargs.get('reserved', 0)) + net_name = cast('bytes', kwargs.get('net_name', b'')) + ap_name = cast('bytes', kwargs.get('ap_name', b'')) + return Schema_ANINetworkIdentifierSuboption( + type=code, length=3 + len(net_name) + len(ap_name), + flags={'E': int(utf8), 'reserved': self._make_uint(reserved, bits=7, + name='reserved bits')}, + net_name_len=len(net_name), net_name=net_name, + ap_name_len=len(ap_name), ap_name=ap_name) + + if code == Enum_ANISuboption.Geo_Location: + if option is not None: + geo = cast('Data_ANIGeoLocationSuboption', option) + raw_lat, raw_lon = geo.raw_latitude, geo.raw_longitude + else: + raw_lat = cast('int', kwargs.get('raw_latitude', 0)) + raw_lon = cast('int', kwargs.get('raw_longitude', 0)) + return Schema_ANIGeoLocationSuboption( + type=code, length=6, location={ + 'latitude': self._make_sint(raw_lat, bits=24, name='ANI geo-location latitude'), + 'longitude': self._make_sint(raw_lon, bits=24, name='ANI geo-location longitude'), + }) + + if code == Enum_ANISuboption.Operator_Identifier: + if option is not None: + operator = cast('Data_ANIOperatorIdentifierSuboption', option) + op_id_type = operator.op_id_type # type: Enum_OperatorID | int + identifier = operator.identifier + else: + op_id_type = self._make_index( + kwargs.get('op_id_type', Enum_OperatorID.Realm_of_the_Operator), + Enum_OperatorID, bits=8, name='Op-ID type') + identifier = cast('bytes', kwargs.get('identifier', b'')) + return Schema_ANIOperatorIdentifierSuboption( + type=code, length=1 + len(identifier), + op_id_type=cast('Enum_OperatorID', op_id_type), identifier=identifier) + + if code == Enum_ANISuboption.Civic_Location: + if option is not None: + civic = cast('Data_ANICivicLocationSuboption', option) + fmt, location = civic.format, civic.location + octets = getattr(civic, 'reserved', b'\x00') + else: + fmt = cast('int', kwargs.get('format', 0)) + location = cast('bytes', kwargs.get('location', b'')) + octets = cast('bytes', kwargs.get('reserved', b'\x00')) + return Schema_ANICivicLocationSuboption( + type=code, length=2 + len(location), + format=self._make_uint(fmt, bits=8, name='civic location format'), + reserved=self._make_octets(octets, size=1, name='reserved octets'), + location=location) + + if code == Enum_ANISuboption.MAG_Group_Identifier: + if option is not None: + group_id = cast('Data_ANIMAGGroupIdentifierSuboption', option).group_id + else: + group_id = cast('int', kwargs.get('group_id', 0)) + return Schema_ANIMAGGroupIdentifierSuboption( + type=code, length=2, group_id=self._make_uint(group_id, bits=16, + name='MAG group identifier')) + + if code == Enum_ANISuboption.ANI_Update_Timer: + if option is not None: + timer = math.ceil(cast('Data_ANIUpdateTimerSuboption', + option).timer.total_seconds() / 4) + else: + raw_timer = kwargs.get('timer', 0) + timer = raw_timer if isinstance(raw_timer, int) else math.ceil( + raw_timer.total_seconds() / 4) + return Schema_ANIUpdateTimerSuboption( + type=code, length=2, timer=self._make_uint(timer, bits=16, name='ANI update timer')) + + if option is not None: + payload = cast('Data_UnassignedANISuboption', option).data + else: + payload = cast('bytes', kwargs.get('data', b'')) + return Schema_UnassignedANISuboption(type=code, length=len(payload), data=payload) + + def _make_qos_attributes( + self, + attributes: 'QoSAttribute | list[Schema_QoSAttribute | tuple[Enum_QoSAttribute | int, dict[str, Any]] | bytes]', + ) -> 'list[Schema_QoSAttribute | bytes]': + """Make MH quality-of-service attributes. + + Args: + attributes: Attributes, as parsed data, schemas, ``(type, kwargs)`` + pairs, or raw octets. + + Returns: + Attribute schema list. + + Raises: + ProtocolError: If the type of an attribute, given in a ``(type, + kwargs)`` pair or as the key of parsed data, is a name, a + :obj:`bool` or a code its field cannot carry, or if an + attribute given as raw octets is empty (:issue:`2723`). + + """ + if isinstance(attributes, list): + entries = [] # type: list[Schema_QoSAttribute | bytes] + for item in attributes: + if isinstance(item, bytes) and not item: + raise ProtocolError(f'{self.alias}: invalid attribute: {item!r} holds no type octet') + if isinstance(item, (bytes, Schema)): + entries.append(cast('Schema_QoSAttribute | bytes', item)) + else: + kind, args = cast('tuple[Enum_QoSAttribute | int, dict[str, Any]]', item) + code = self._make_index(kind, Enum_QoSAttribute, bits=8, name='attribute type') + entries.append(self._make_qos_attribute(code, **args)) + return entries + + return [self._make_qos_attribute( + self._make_index(key, Enum_QoSAttribute, bits=8, name='attribute type'), option) + for key, option in attributes.items(multi=True)] + + def _make_qos_attribute(self, code: 'Enum_QoSAttribute', + option: 'Optional[Data_QoSAttribute]' = None, + **kwargs: 'Any') -> 'Schema_QoSAttribute': + """Make one MH quality-of-service attribute. + + Args: + code: Attribute type. + option: Attribute data model. + **kwargs: Attribute fields, when no data model is given. + + Returns: + Constructed attribute schema. + + Raises: + ProtocolError: If a rate, priority, vendor or reserved value does not fit + its field, or ``ts_format`` is a name, a :obj:`bool` or a code its + field cannot carry. + + Note: + The data model parameter is named ``option`` rather than ``data`` + deliberately. The vendor-specific attribute of + :rfc:`7222#section-4.2.11` has a field of its own called ``data``, so + a parameter of that name would capture a caller's ``data=`` and leave + the ``kwargs.get('data')`` fallback empty, silently dropping the vendor + payload and writing the length as though it were empty. ``vendor`` and + ``subtype`` do not collide, so the loss would look like a partial + success rather than a bug. + + Dispatch is on ``code`` rather than on ``isinstance``, for the reason + given in :meth:`_read_fid_suboptions`. + + """ + if code in (Enum_QoSAttribute.Per_Session_Agg_Max_DL_Bit_Rate, + Enum_QoSAttribute.Per_Session_Agg_Max_UL_Bit_Rate): + if option is not None: + session = cast('Data_PerSessionBitRateAttribute', option) + service, exclude, rate = session.service, session.exclude, session.rate + reserved = session.reserved + else: + service = cast('bool', kwargs.get('service', False)) + exclude = cast('bool', kwargs.get('exclude', False)) + reserved = cast('int', kwargs.get('reserved', 0)) + rate = cast('int', kwargs.get('rate', 0)) + return Schema_PerSessionBitRateAttribute( + type=code, length=6, + flags={'S': int(service), 'E': int(exclude), + 'reserved': self._make_uint(reserved, bits=14, name='reserved bits')}, + rate=self._make_uint(rate, bits=32, name='bit rate')) + + if code in (Enum_QoSAttribute.Per_MN_Agg_Max_DL_Bit_Rate, + Enum_QoSAttribute.Per_MN_Agg_Max_UL_Bit_Rate, + Enum_QoSAttribute.Aggregate_Max_DL_Bit_Rate, + Enum_QoSAttribute.Aggregate_Max_UL_Bit_Rate, + Enum_QoSAttribute.Guaranteed_DL_Bit_Rate, + Enum_QoSAttribute.Guaranteed_UL_Bit_Rate): + if option is not None: + rate = cast('Data_BitRateAttribute', option).rate + octets = getattr(option, 'reserved', b'\x00\x00') + else: + rate = cast('int', kwargs.get('rate', 0)) + octets = cast('bytes', kwargs.get('reserved', b'\x00\x00')) + return Schema_BitRateAttribute(type=code, length=6, + reserved=self._make_octets(octets, size=2, name='reserved octets'), + rate=self._make_uint(rate, bits=32, name='bit rate')) + + if code == Enum_QoSAttribute.Allocation_Retention_Priority: + if option is not None: + arp = cast('Data_AllocationRetentionPriorityAttribute', option) + level = arp.priority_level + capability = arp.preemption_capability + vulnerability = arp.preemption_vulnerability + octets = getattr(arp, 'reserved', b'\x00') + else: + level = cast('int', kwargs.get('priority_level', 1)) + capability = cast('int', kwargs.get('preemption_capability', 0)) + vulnerability = cast('int', kwargs.get('preemption_vulnerability', 0)) + octets = cast('bytes', kwargs.get('reserved', b'\x00')) + return Schema_AllocationRetentionPriorityAttribute( + type=code, length=2, reserved=self._make_octets(octets, size=1, name='reserved octets'), + priority={ + 'PL': self._make_uint(level, bits=4, name='priority level'), + 'PC': self._make_uint(capability, bits=2, name='preemption capability'), + 'PV': self._make_uint(vulnerability, bits=2, name='preemption vulnerability'), + }) + + if code == Enum_QoSAttribute.QoS_Traffic_Selector: + if option is not None: + ts_attr = cast('Data_QoSTrafficSelectorAttribute', option) + ts_format = ts_attr.ts_format # type: Enum_TrafficSelector | int + selector = ts_attr.selector + octets = getattr(ts_attr, 'reserved', b'\x00') + else: + ts_format = self._make_index( + kwargs.get('ts_format', Enum_TrafficSelector.IPv6_Binary_Traffic_Selector), + Enum_TrafficSelector, bits=8, name='TS format') + selector = cast('bytes', kwargs.get('selector', b'')) + octets = cast('bytes', kwargs.get('reserved', b'\x00')) + return Schema_QoSTrafficSelectorAttribute( + type=code, length=2 + len(selector), + reserved=self._make_octets(octets, size=1, name='reserved octets'), + ts_format=cast('Enum_TrafficSelector', ts_format), selector=selector) + + if code == Enum_QoSAttribute.QoS_Vendor_Specific_Attribute: + if option is not None: + vendor_attr = cast('Data_QoSVendorSpecificAttribute', option) + vendor, subtype, payload = (vendor_attr.vendor, vendor_attr.subtype, + vendor_attr.data) + octets = getattr(vendor_attr, 'reserved', b'\x00\x00') + else: + vendor = cast('int', kwargs.get('vendor', 0)) + subtype = cast('int', kwargs.get('subtype', 0)) + payload = cast('bytes', kwargs.get('data', b'')) + octets = cast('bytes', kwargs.get('reserved', b'\x00\x00')) + return Schema_QoSVendorSpecificAttribute( + type=code, length=7 + len(payload), + reserved=self._make_octets(octets, size=2, name='reserved octets'), + vendor=self._make_uint(vendor, bits=32, name='vendor ID'), + subtype=self._make_uint(subtype, bits=8, name='vendor subtype'), data=payload) + + if option is not None: + payload = cast('Data_UnassignedQoSAttribute', option).data + else: + payload = cast('bytes', kwargs.get('data', b'')) + return Schema_UnassignedQoSAttribute(type=code, length=len(payload), data=payload) + + def _make_lcmp_suboptions( + self, + suboptions: 'LMAControlledMAGSuboption | list[Schema_LMAControlledMAGSuboption | tuple[Enum_LMAControlledMAGSuboption | int, dict[str, Any]] | bytes]', # pylint: disable=line-too-long + ) -> 'list[Schema_LMAControlledMAGSuboption | bytes]': + """Make MH LMA-controlled MAG parameters sub-options. + + Args: + suboptions: Sub-options, as parsed data, schemas, ``(type, kwargs)`` + pairs, or raw octets. + + Returns: + Sub-option schema list. + + Raises: + ProtocolError: If the type of a sub-option, given in a ``(type, + kwargs)`` pair or as the key of parsed data, is a name, a + :obj:`bool` or a code its field cannot carry, or if a + sub-option given as raw octets is empty (:issue:`2723`). + + """ + if isinstance(suboptions, list): + entries = [] # type: list[Schema_LMAControlledMAGSuboption | bytes] + for item in suboptions: + if isinstance(item, bytes) and not item: + raise ProtocolError(f'{self.alias}: invalid sub-option: {item!r} holds no type octet') + if isinstance(item, (bytes, Schema)): + entries.append(cast('Schema_LMAControlledMAGSuboption | bytes', item)) + else: + kind, args = cast( + 'tuple[Enum_LMAControlledMAGSuboption | int, dict[str, Any]]', item) + code = self._make_index(kind, Enum_LMAControlledMAGSuboption, bits=8, + name='sub-option type') + entries.append(self._make_lcmp_suboption(code, **args)) + return entries + + return [self._make_lcmp_suboption( + self._make_index(key, Enum_LMAControlledMAGSuboption, bits=8, + name='sub-option type'), option) + for key, option in suboptions.items(multi=True)] + + @staticmethod + def _seconds(value: 'int | timedelta', unit: 'int' = 1) -> 'int': + """Return an interval in wire units. + + Args: + value: Interval, already in wire units when an :obj:`int`, or as a + :class:`~datetime.timedelta`. + unit: Number of seconds one wire unit stands for. + + Returns: + The interval in wire units. + + """ + if isinstance(value, int): + return value + return math.ceil(value.total_seconds() / unit) + + def _make_lcmp_suboption(self, code: 'Enum_LMAControlledMAGSuboption', + option: 'Optional[Data_LMAControlledMAGSuboption]' = None, + **kwargs: 'Any') -> 'Schema_LMAControlledMAGSuboption': + """Make one MH LMA-controlled MAG parameters sub-option. + + Args: + code: Sub-option type. + option: Sub-option data model. + **kwargs: Sub-option fields, when no data model is given. + + Returns: + Constructed sub-option schema. + + Raises: + ProtocolError: If a timer or count does not fit its field. + + Note: + The data model parameter is named ``option`` rather than ``data`` so + that it cannot shadow a sub-option field of that name -- see + :meth:`_make_fid_suboption`. Dispatch is on ``code`` + rather than on ``isinstance``, for the reason given in + :meth:`_read_fid_suboptions`. + + """ + if code == Enum_LMAControlledMAGSuboption.Binding_Re_registration_Control: + if option is not None: + rereg = cast('Data_BindingReregistrationControlSuboption', option) + start = math.ceil(rereg.start_time.total_seconds() / 4) + initial = math.ceil(rereg.initial_retransmission.total_seconds()) + maximum = math.ceil(rereg.max_retransmission.total_seconds()) + else: + start = self._seconds(kwargs.get('start_time', 0), 4) + initial = self._seconds(kwargs.get('initial_retransmission', 0)) + maximum = self._seconds(kwargs.get('max_retransmission', 0)) + return Schema_BindingReregistrationControlSuboption( + type=code, length=6, + start_time=self._make_uint(start, bits=16, name='re-registration start time'), + initial_retransmission=self._make_uint(initial, bits=16, + name='initial retransmission timer'), + max_retransmission=self._make_uint(maximum, bits=16, + name='maximum retransmission timer')) + + if code == Enum_LMAControlledMAGSuboption.Heartbeat_Control: + if option is not None: + heartbeat = cast('Data_HeartbeatControlSuboption', option) + interval = math.ceil(heartbeat.interval.total_seconds()) + delay = math.ceil(heartbeat.retransmission_delay.total_seconds()) + count = heartbeat.max_retransmissions + else: + interval = self._seconds(kwargs.get('interval', 0)) + delay = self._seconds(kwargs.get('retransmission_delay', 0)) + count = cast('int', kwargs.get('max_retransmissions', 0)) + return Schema_HeartbeatControlSuboption( + type=code, length=6, + interval=self._make_uint(interval, bits=16, name='heartbeat interval'), + retransmission_delay=self._make_uint(delay, bits=16, name='retransmission delay'), + max_retransmissions=self._make_uint(count, bits=16, name='maximum retransmissions')) + + if option is not None: + payload = cast('Data_UnassignedLMAControlledMAGSuboption', option).data + else: + payload = cast('bytes', kwargs.get('data', b'')) + return Schema_UnassignedLMAControlledMAGSuboption( + type=code, length=len(payload), data=payload) + + def _make_cga_extensions(self, extensions: 'Extension | list[Schema_CGAExtension | tuple[Enum_CGAExtension | int, dict[str, Any]] | bytes]') -> 'tuple[list[Schema_CGAExtension | bytes], int]': # pylint: disable=line-too-long + """Make CGA extensions for MH. + + Args: + extensions: CGA extensions. + + Returns: + Tuple of extensions and total length of extensions. + + Raises: + ProtocolError: If the type of an extension, given in a ``(type, + kwargs)`` pair or as the key of parsed data, is a name, a + :obj:`bool` or a code its field cannot carry, or if an + extension given as raw octets is empty (:issue:`2723`). + + """ + total_length = 0 + if isinstance(extensions, list): + extensions_list = [] # type: list[Schema_CGAExtension | bytes] + for schema in extensions: + if isinstance(schema, bytes): + if not schema: + raise ProtocolError(f'{self.alias}: invalid CGA extension: {schema!r} holds no type field') + code = Enum_CGAExtension.get(int.from_bytes(schema[0:2], 'big', signed=False)) + + data = schema # type: Schema_CGAExtension | bytes + data_len = len(data) + elif isinstance(schema, Schema): + data = schema + data_len = len(schema.pack()) + else: + kind, args = cast('tuple[Enum_CGAExtension | int, dict[str, Any]]', schema) + code = self._make_index(kind, Enum_CGAExtension, bits=16, + name='extension type') + name = self._lookup_registry(self.__extension__, code) + if isinstance(name, str): + meth_name = f'_make_ext_{name}' + meth = cast('ExtensionConstructor', + getattr(self, meth_name, self._make_ext_none)) + else: + meth = name[1] + + data = meth(code, **args) + data_len = len(data.pack()) + + extensions_list.append(data) + total_length += data_len + return extensions_list, total_length + + extensions_list = [] + for key, extension in extensions.items(multi=True): + code = self._make_index(key, Enum_CGAExtension, bits=16, name='extension type') + name = self._lookup_registry(self.__extension__, code) + if isinstance(name, str): + meth_name = f'_make_ext_{name}' + meth = cast('ExtensionConstructor', + getattr(self, meth_name, self._make_ext_none)) + else: + meth = name[1] + + data = meth(code, extension) + data_len = len(data.pack()) + + extensions_list.append(data) + total_length += data_len + return extensions_list, total_length diff --git a/pcapkit/protocols/link/frad/protocol.py b/pcapkit/protocols/link/frad/protocol.py index 707fe5722f..cdd98bc131 100644 --- a/pcapkit/protocols/link/frad/protocol.py +++ b/pcapkit/protocols/link/frad/protocol.py @@ -90,8 +90,8 @@ class FRAD(Link[Data_FRAD, Schema_FRAD], __proto__ = collections.defaultdict( lambda: ModuleDescriptor('pcapkit.protocols.misc.raw.protocol', 'Raw'), { - 0xCC: ModuleDescriptor('pcapkit.protocols.internet.ipv4.protocol', 'IPv4'), 0x8E: ModuleDescriptor('pcapkit.protocols.internet.ipv6.protocol', 'IPv6'), + 0xCC: ModuleDescriptor('pcapkit.protocols.internet.ipv4.protocol', 'IPv4'), }, ) # type: DefaultDict[int, ModuleDescriptor[ProtocolBase] | Type[ProtocolBase]] diff --git a/pcapkit/protocols/link/link.py b/pcapkit/protocols/link/link.py index 42a61c1dab..81d7854e0a 100644 --- a/pcapkit/protocols/link/link.py +++ b/pcapkit/protocols/link/link.py @@ -81,9 +81,11 @@ class Link(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=abstra __proto__ = collections.defaultdict( lambda: ModuleDescriptor('pcapkit.protocols.misc.raw.protocol', 'Raw'), { + Enum_EtherType.Internet_Protocol_version_4: ModuleDescriptor('pcapkit.protocols.internet.ipv4.protocol', 'IPv4'), Enum_EtherType.Address_Resolution_Protocol: ModuleDescriptor('pcapkit.protocols.link.arp.protocol', 'ARP'), Enum_EtherType.Reverse_Address_Resolution_Protocol: ModuleDescriptor('pcapkit.protocols.application.rarp.protocol', 'RARP'), + # The 802.1Q customer tag and the 802.1ad service tag. Q-in-Q stacks # them -- the service tag's own next-EtherType is what selects the # customer tag -- so the two coexist in one frame rather than @@ -93,14 +95,13 @@ class Link(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=abstra # :class:`~pcapkit.protocols.link.vlan.protocol.VLAN`. Enum_EtherType.Customer_VLAN_Tag_Type: ModuleDescriptor('pcapkit.protocols.link.c_tag.protocol', 'C_Tag'), - Enum_EtherType.IEEE_Std_802_1Q_Service_VLAN_tag_identifier: - ModuleDescriptor('pcapkit.protocols.link.s_tag.protocol', 'S_Tag'), - - Enum_EtherType.Internet_Protocol_version_4: ModuleDescriptor('pcapkit.protocols.internet.ipv4.protocol', 'IPv4'), - Enum_EtherType.Internet_Protocol_version_6: ModuleDescriptor('pcapkit.protocols.internet.ipv6.protocol', 'IPv6'), # see https://en.wikipedia.org/wiki/EtherType#Values 0x8137: ModuleDescriptor('pcapkit.protocols.internet.ipx.protocol', 'IPX'), + + Enum_EtherType.Internet_Protocol_version_6: ModuleDescriptor('pcapkit.protocols.internet.ipv6.protocol', 'IPv6'), + Enum_EtherType.IEEE_Std_802_1Q_Service_VLAN_tag_identifier: + ModuleDescriptor('pcapkit.protocols.link.s_tag.protocol', 'S_Tag'), }, ) # type: DefaultDict[int | Enum_EtherType, ModuleDescriptor[ProtocolBase] | Type[ProtocolBase]] diff --git a/pcapkit/protocols/misc/checksum/ip_pseudo/protocol.py b/pcapkit/protocols/misc/checksum/ip_pseudo/protocol.py index 32cddb59ce..ad6bfadf2b 100644 --- a/pcapkit/protocols/misc/checksum/ip_pseudo/protocol.py +++ b/pcapkit/protocols/misc/checksum/ip_pseudo/protocol.py @@ -410,36 +410,6 @@ def _source(self, ip: 'IPv4 | IPv6') -> 'IPv4Address | IPv6Address': 'parsed; give src') return src - @staticmethod - def _walk(ip: 'IPv6') -> 'tuple[Enum_TransType, int]': - """Walk the extension headers of an IPv6 layer up to its upper layer. - - A Mobility Header or HIP (:rfc:`6275#section-6.1.1`, :rfc:`7401#section-5.1.1`) - is the upper layer itself, and an Encapsulating Security Payload - (:rfc:`4303`) ends the walk, its upper layer being encrypted. - - Args: - ip: IPv6 layer. - - Returns: - The code the walk stopped on, and the octets of the headers ahead of - it. The code is an extension header's if ``ip`` did not parse that - header: a malformed one (:issue:`2477`), one past the header limit, - or one not captured, or cut short by the capture (:issue:`2694`). - - """ - code, before = ip.info.next, 0 - for _, header in ip.extension_headers.items(multi=True): - if code in _TERMINAL_HEADERS or code == Enum_TransType.ESP: - break - # NOTE: A header the capture cut short is its schema's short read - # (#2694), and as unwalked as one not captured at all. - if header.info.get('__short_read__') is not None: - break - before += header.length - code = header.info.next - return code, before - def _destination(self, ip: 'IPv4 | IPv6') -> 'IPv4Address | IPv6Address': """Read the pseudo-header destination address from an IP layer. @@ -509,6 +479,78 @@ def _upper_protocol(self, ip: 'IPv4 | IPv6') -> 'Enum_TransType': """ return self._upper(ip)[0] + def _upper_length(self, ip: 'IPv4 | IPv6', *, from_layer: 'bool' = True) -> 'int': + """Read the upper-layer packet length from an IP layer. + + Args: + ip: IPv4 or IPv6 layer. + from_layer: Whether the upper layer is the one ``ip`` carries, + rather than one given by the caller. + + Returns: + The Length field of a UDP header that is present, else the length + past the IP header and extension headers. + + Raises: + ProtocolError: If ``ip`` is a fragment, ends at an unparsed header, + or has a zero Payload Length and no Jumbo Payload option. + + """ + if isinstance(ip, IPv4): + info4 = ip.info + if info4.offset or info4.flags.mf: + raise ProtocolError(f'{self.alias}: {ip.alias} fragment; give upper_length') + protocol, before = self._upper(ip) + return self._udp_length(ip, protocol, info4.hdr_len + before, + info4.len - info4.hdr_len - before, from_layer) + + protocol, before = self._upper(ip) + info = ip.info + for frag in ip.extension_headers.getlist(Enum_ExtensionHeader.IPv6_Frag): # type: ignore[arg-type] + if frag.info.offset or frag.info.mf: + raise ProtocolError(f'{self.alias}: {ip.alias} fragment; give upper_length') + + extent = info.payload + if not extent: + jumbo = jumbo_payload_length(info.next, ip.data[IPV6_HEADER_LEN:]) + if jumbo is None: + raise ProtocolError(f'{self.alias}: Payload Length 0 and no Jumbo Payload ' + 'option; give upper_length') + extent = jumbo + # NOTE: ``info.hdr_len`` ends at the first Fragment header, so headers + # after it are not counted; the fixed header plus ``before`` is the offset. + return self._udp_length(ip, protocol, IPV6_HEADER_LEN + before, extent - before, from_layer) + + @staticmethod + def _walk(ip: 'IPv6') -> 'tuple[Enum_TransType, int]': + """Walk the extension headers of an IPv6 layer up to its upper layer. + + A Mobility Header or HIP (:rfc:`6275#section-6.1.1`, :rfc:`7401#section-5.1.1`) + is the upper layer itself, and an Encapsulating Security Payload + (:rfc:`4303`) ends the walk, its upper layer being encrypted. + + Args: + ip: IPv6 layer. + + Returns: + The code the walk stopped on, and the octets of the headers ahead of + it. The code is an extension header's if ``ip`` did not parse that + header: a malformed one (:issue:`2477`), one past the header limit, + or one not captured, or cut short by the capture (:issue:`2694`). + + """ + code, before = ip.info.next, 0 + for _, header in ip.extension_headers.items(multi=True): + if code in _TERMINAL_HEADERS or code == Enum_TransType.ESP: + break + # NOTE: A header the capture cut short is its schema's short read + # (#2694), and as unwalked as one not captured at all. + if header.info.get('__short_read__') is not None: + break + before += header.length + code = header.info.next + return code, before + def _upper(self, ip: 'IPv4 | IPv6') -> 'tuple[Enum_TransType, int]': """Read the upper-layer protocol and the octets of the headers ahead of it. @@ -564,48 +606,6 @@ def _is_extension_header(code: 'int') -> 'bool': return False return True - def _upper_length(self, ip: 'IPv4 | IPv6', *, from_layer: 'bool' = True) -> 'int': - """Read the upper-layer packet length from an IP layer. - - Args: - ip: IPv4 or IPv6 layer. - from_layer: Whether the upper layer is the one ``ip`` carries, - rather than one given by the caller. - - Returns: - The Length field of a UDP header that is present, else the length - past the IP header and extension headers. - - Raises: - ProtocolError: If ``ip`` is a fragment, ends at an unparsed header, - or has a zero Payload Length and no Jumbo Payload option. - - """ - if isinstance(ip, IPv4): - info4 = ip.info - if info4.offset or info4.flags.mf: - raise ProtocolError(f'{self.alias}: {ip.alias} fragment; give upper_length') - protocol, before = self._upper(ip) - return self._udp_length(ip, protocol, info4.hdr_len + before, - info4.len - info4.hdr_len - before, from_layer) - - protocol, before = self._upper(ip) - info = ip.info - for frag in ip.extension_headers.getlist(Enum_ExtensionHeader.IPv6_Frag): # type: ignore[arg-type] - if frag.info.offset or frag.info.mf: - raise ProtocolError(f'{self.alias}: {ip.alias} fragment; give upper_length') - - extent = info.payload - if not extent: - jumbo = jumbo_payload_length(info.next, ip.data[IPV6_HEADER_LEN:]) - if jumbo is None: - raise ProtocolError(f'{self.alias}: Payload Length 0 and no Jumbo Payload ' - 'option; give upper_length') - extent = jumbo - # NOTE: ``info.hdr_len`` ends at the first Fragment header, so headers - # after it are not counted; the fixed header plus ``before`` is the offset. - return self._udp_length(ip, protocol, IPV6_HEADER_LEN + before, extent - before, from_layer) - def _udp_length(self, ip: 'IPv4 | IPv6', protocol: 'int', hdr_len: 'int', length: 'int', from_layer: 'bool') -> 'int': """Prefer the Length field of a UDP header to the length from the IP layer. diff --git a/pcapkit/protocols/misc/pcap/frame/protocol.py b/pcapkit/protocols/misc/pcap/frame/protocol.py index c7b27b9ecb..76d2943fa6 100644 --- a/pcapkit/protocols/misc/pcap/frame/protocol.py +++ b/pcapkit/protocols/misc/pcap/frame/protocol.py @@ -588,36 +588,6 @@ def _make_data(cls, data: 'Data_Frame') -> 'dict[str, Any]': # type: ignore[ove 'packet': cls._make_payload(data), } - def _check_record_floor(self, length: 'Optional[int]') -> 'None': - """Refuse a tail too short to hold a record header. - - Args: - length: Length of packet data, as the caller declared it, or - :obj:`None` to measure what is left in - :attr:`self._file `. - - Raises: - StreamEOFError: If fewer than sixteen octets are left to read. - - A PCAP record header is sixteen octets, which is what - :meth:`__length_hint__` reports. A shorter tail is not a record, so - parsing one would only invent fields from zero padding. Reporting the - end of the stream instead lets :meth:`Extractor.record_frames - ` stop with the - frames read so far, as the PCAP-NG reader does for a truncated block - (:issue:`678`). See :issue:`1265`. - - """ - if length is None: - current = self._file.tell() - length = self._file.seek(0, io.SEEK_END) - current - self._file.seek(current, io.SEEK_SET) - - if length < RECORD_HEADER_LEN: - raise StreamEOFError(f'PCAP: record truncated: {length} octet(s) left, ' - f'fewer than the {RECORD_HEADER_LEN} a record header needs', - quiet=True) - def _decode_next_layer(self, dict_: 'Data_Frame', proto: 'Optional[int]' = None, length: 'Optional[int]' = None, *, packet: 'Optional[dict[str, Any]]' = None) -> 'Data_Frame': # pylint: disable=arguments-differ r"""Decode next layer protocol. @@ -697,6 +667,36 @@ def _make_timestamp(self, timestamp: 'Optional[float | Decimal | dt_type | int]' return ts_sec, ts_usec + def _check_record_floor(self, length: 'Optional[int]') -> 'None': + """Refuse a tail too short to hold a record header. + + Args: + length: Length of packet data, as the caller declared it, or + :obj:`None` to measure what is left in + :attr:`self._file `. + + Raises: + StreamEOFError: If fewer than sixteen octets are left to read. + + A PCAP record header is sixteen octets, which is what + :meth:`__length_hint__` reports. A shorter tail is not a record, so + parsing one would only invent fields from zero padding. Reporting the + end of the stream instead lets :meth:`Extractor.record_frames + ` stop with the + frames read so far, as the PCAP-NG reader does for a truncated block + (:issue:`678`). See :issue:`1265`. + + """ + if length is None: + current = self._file.tell() + length = self._file.seek(0, io.SEEK_END) - current + self._file.seek(current, io.SEEK_SET) + + if length < RECORD_HEADER_LEN: + raise StreamEOFError(f'PCAP: record truncated: {length} octet(s) left, ' + f'fewer than the {RECORD_HEADER_LEN} a record header needs', + quiet=True) + def _dispatch_linktype(self, proto: 'Optional[int]', length: 'Optional[int]' = None) -> 'Optional[int]': """Link type a frame's data is dispatched under. diff --git a/pcapkit/protocols/misc/pcap/header/protocol.py b/pcapkit/protocols/misc/pcap/header/protocol.py index 07ce4b55ea..88a2c10021 100644 --- a/pcapkit/protocols/misc/pcap/header/protocol.py +++ b/pcapkit/protocols/misc/pcap/header/protocol.py @@ -346,24 +346,6 @@ def _make_data(cls, data: 'Data_Header') -> 'dict[str, Any]': # type: ignore[ov 'network': data.network, } - def _read_protos(self, size: int) -> 'Enum_LinkType': - """Read next layer protocol type. - - Reads ``size`` octets as an unsigned integer in the file's byte order - (:attr:`byteorder`), falling back to little-endian before :meth:`read` - has set it (:issue:`2312`). - - Arguments: - size: buffer size - - Returns: - Link layer protocol enumeration. - - """ - _byte = self._read_unpack(size, lilendian=getattr(self, '_byte', 'little') == 'little') - _prot = Enum_LinkType.get(_byte) - return _prot - def _make_magic(self, byteorder: 'Literal["big", "little"]' = sys.byteorder, lilendian: 'Optional[bool]' = None, bigendian: 'Optional[bool]' = None, nanosecond: bool = False) -> 'tuple[bytes, bool]': @@ -391,3 +373,21 @@ def _make_magic(self, byteorder: 'Literal["big", "little"]' = sys.byteorder, magic_number = _DECLARED_MAGIC[(byteorder, nanosecond)] return magic_number, (byteorder == 'little') + + def _read_protos(self, size: int) -> 'Enum_LinkType': + """Read next layer protocol type. + + Reads ``size`` octets as an unsigned integer in the file's byte order + (:attr:`byteorder`), falling back to little-endian before :meth:`read` + has set it (:issue:`2312`). + + Arguments: + size: buffer size + + Returns: + Link layer protocol enumeration. + + """ + _byte = self._read_unpack(size, lilendian=getattr(self, '_byte', 'little') == 'little') + _prot = Enum_LinkType.get(_byte) + return _prot diff --git a/pcapkit/protocols/misc/pcapng/protocol.py b/pcapkit/protocols/misc/pcapng/protocol.py index e9feb7e599..e4695b7961 100644 --- a/pcapkit/protocols/misc/pcapng/protocol.py +++ b/pcapkit/protocols/misc/pcapng/protocol.py @@ -836,17 +836,17 @@ class PCAPNG(ProtocolBase[Data_PCAPNG, Schema_PCAPNG], __block__ = collections.defaultdict( lambda: 'unknown', { - Enum_BlockType.Section_Header_Block: 'shb', Enum_BlockType.Interface_Description_Block: 'idb', - Enum_BlockType.Enhanced_Packet_Block: 'epb', + Enum_BlockType.Packet_Block: 'packet', Enum_BlockType.Simple_Packet_Block: 'spb', Enum_BlockType.Name_Resolution_Block: 'nrb', Enum_BlockType.Interface_Statistics_Block: 'isb', + Enum_BlockType.Enhanced_Packet_Block: 'epb', Enum_BlockType.systemd_Journal_Export_Block: 'systemd', Enum_BlockType.Decryption_Secrets_Block: 'dsb', Enum_BlockType.Custom_Block_that_rewriters_can_copy_into_new_files: 'cb', + Enum_BlockType.Section_Header_Block: 'shb', Enum_BlockType.Custom_Block_that_rewriters_should_not_copy_into_new_files: 'cb', - Enum_BlockType.Packet_Block: 'packet', }, ) # type: DefaultDict[Enum_BlockType | int, str | tuple[BlockParser, BlockConstructor]] @@ -925,8 +925,8 @@ class PCAPNG(ProtocolBase[Data_PCAPNG, Schema_PCAPNG], { Enum_SecretsType.TLS_Key_Log: 'tls', Enum_SecretsType.WireGuard_Key_Log: 'wireguard', - Enum_SecretsType.ZigBee_NWK_Key: 'zigbee_nwk', Enum_SecretsType.ZigBee_APS_Key: 'zigbee_aps', + Enum_SecretsType.ZigBee_NWK_Key: 'zigbee_nwk', }, ) # type: DefaultDict[Enum_SecretsType | int, str | tuple[SecretsParser, SecretsConstructor]] @@ -1700,67 +1700,6 @@ def _make_data(cls, data: 'Data_PCAPNG') -> 'dict[str, Any]': # type: ignore[ov 'block': data, } - def _check_block_floor(self, length: 'Optional[int]') -> 'None': - """Reject a tail too short to hold any block at all. - - Args: - length: Length of packet data, as the caller declared it, or - :obj:`None` to measure what is left in - :attr:`self._file `. - - Raises: - StreamEOFError: If fewer than twelve octets are left to read. - - A PCAP-NG block is twelve octets at its smallest -- Block Type, Block - Total Length and the trailing copy of it, over an empty body -- which is - what :meth:`__length_hint__` reports. A tail shorter than that is not a - block, so parsing one out of it can only invent fields from - :meth:`FieldBase.unpack `'s - zero padding, and once the four octets of :attr:`PCAPNG.type - ` have been padded out - of nothing ``__length__`` is negative -- which - :func:`~pcapkit.protocols.misc.pcapng.schema.pcapng_block_selector` hands - to :class:`~pcapkit.corekit.fields.misc.SchemaField`, where - :meth:`io.RawIOBase.read` raises the bare ``ValueError`` of :issue:`678`. - - Reporting the end of the stream instead is what keeps the :issue:`431` accommodation: - :exc:`~pcapkit.utilities.exceptions.StreamEOFError` is an - :exc:`EOFError`, which :meth:`Extractor.record_frames - ` already catches - to stop with the frames read so far, so -- unlike a refusal inside - :meth:`FieldBase.unpack `, - which has no catch point above it and would abort the whole extraction -- - this costs no frame that was actually in the file. It is the same signal - :func:`~pcapkit.corekit.decorators.prepare` raises for a *derived* - remainder of exactly zero; a remainder of one, two or three octets - passes that check and is caught here instead. - - Note: - This runs on both of :meth:`unpack`'s paths. On the parsing path the - tail is what the file has left to read; on the construction path it is - the octets of a block handed to :meth:`make` as raw :obj:`bytes`, which - :meth:`unpack` parses off the buffer :meth:`pack` has just filled - (:issue:`2354`) -- a caller's octets are no more trustworthy than a - file's. Octets too short for this floor are therefore refused here - rather than by :meth:`read`'s own Block Total Length check, which sees - only what gets past the floor and so names an impossible *declared* - length, never a tail too short to declare one. - - Measuring an undeclared length needs the stream to seek, and nothing - guards that here because :meth:`__post_init__` has already called - :meth:`~io.IOBase.tell` on it to record ``_seek_set``: a stream that - could not seek never reaches this method. - - """ - if length is None: - current = self._file.tell() - length = self._file.seek(0, io.SEEK_END) - current - self._file.seek(current, io.SEEK_SET) - - if length < GENERIC_BLOCK_MINIMUM: - raise StreamEOFError(f'PCAP-NG: block truncated: {length} octet(s) left, ' - f'fewer than the {GENERIC_BLOCK_MINIMUM} a block needs', quiet=True) - def _read_block_short(self, schema: 'Schema_EnhancedPacketBlock | Schema_PacketBlock | Schema_UnknownBlock', *, header: 'Schema_PCAPNG') -> 'Data_EnhancedPacketBlock | Data_PacketBlock': """Read a PCAP-NG EPB or packet block (obsolete) shorter than its fixed fields. @@ -1964,507 +1903,544 @@ def _read_block_cut(self, header: 'Schema_PCAPNG', offset: 'int', body=body, ) - def _get_payload(self) -> 'bytes': - """Get payload of :attr:`self.__header__ `. - - Returns: - Payload of :attr:`self.__header__ ` as :obj:`bytes`. - - See Also: - This is a wrapper function for :meth:`pcapkit.protocols.schema.Schema.get_payload`. - - Note: - A packet block kept as captured has no payload (see - :meth:`_read_block_short`). - - """ - if isinstance(self.__header__.block, Schema_UnknownBlock): - return b'' - return self.__header__.block.get_payload() - - @staticmethod - def _get_payload_name(block: 'Schema_BlockType | bytes') -> 'Optional[str]': - """Get the name of the field carrying a block's captured packet octets. + def _read_block_unknown(self, schema: 'Schema_UnknownBlock', *, + header: 'Schema_PCAPNG') -> 'Data_UnknownBlock': + """Read unknown PCAP-NG block. Args: - block: Parsed block schema. + schema: Parsed block schema. + header: Parsed PCAP-NG header schema. Returns: - The name of the block schema's payload field, or :obj:`None` when the - block declares none -- which is every block type outside - :attr:`PACKET_TYPES `, and also a block whose - body was handed over as raw :obj:`bytes` at construction time rather - than as a schema. + Parsed packet data. """ - name = getattr(block, '__payload__', None) - if name is None or name not in getattr(block, '__fields__', {}): - return None - return name - - @staticmethod - def _get_local_timezone() -> 'timezone': - """Get local timezone. + data = Data_UnknownBlock( + type=header.type, + length=schema.length, + body=schema.body, + ) + return data - Warning: - Not used when reconstructing a block timestamp, and must not be: - a PCAP-NG timestamp is an offset from the UNIX epoch, so mixing the - *reading* host's zone into it makes one file parse to different - instants on different machines. - :meth:`self._get_timezone <_get_timezone>` returns - :attr:`datetime.timezone.utc` instead when the capture names no - ``if_tzone``. + def _read_block_idb(self, schema: 'Schema_InterfaceDescriptionBlock', *, + header: 'Schema_PCAPNG') -> 'Data_InterfaceDescriptionBlock': + """Read PCAP-NG interface description block (IDB). - It also answers for *today* rather than for the capture's own - instant -- ``America/New_York`` reports ``-04:00`` all year, so a - December timestamp would get EDT's offset and not EST's -- which is - a second reason it cannot stand in for a capture's timezone. + Structure of Interface Description Block: - """ - tzinfo = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo - if tzinfo is None: - return datetime.timezone.utc - return cast('timezone', tzinfo) + .. code-block:: text - def _get_interface(self, interface_id: 'int') -> 'Data_InterfaceDescriptionBlock': - """Interface description that ``interface_id`` names. + 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 0 | Block Type = 0x00000001 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 4 | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 8 | LinkType | Reserved | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 12 | SnapLen | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 16 / / + / Options (variable) / + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - interface_id: Interface ID that the current block associates with. + schema: Parsed block schema. + header: Parsed PCAP-NG header schema. Returns: - Interface Description Block (IDB) of the current section that - ``interface_id`` identifies. - - Raises: - FormatError: If the current section describes no such interface. - - Note: - The PCAP-NG specification requires a block's interface ID to name an - Interface Description Block of the block's own section, so an ID that - names none is a malformed file rather than a programming error -- - hence :exc:`~pcapkit.utilities.exceptions.FormatError` rather than - the bare :exc:`IndexError` that indexing the list would raise. + Parsed packet data. """ - interfaces = self._ctx.interfaces - if not 0 <= interface_id < len(interfaces): - tag = self.INTERFACE_ID_BLOCK_TAGS.get(self._type, f'Block {self._type}') - raise FormatError(f'PCAP-NG: [{tag}] invalid interface ID: {interface_id}') - return interfaces[interface_id] - - def _get_resolution(self, interface_id: 'int' = 0) -> 'int': - """Timestamp resolution of the current block, in units per second. - - Args: - interface_id: Interface ID that the current block associates with. + data = Data_InterfaceDescriptionBlock( + type=header.type, + length=schema.length, + linktype=schema.linktype, + snaplen=schema.snaplen, + options=self._read_pcapng_options(schema.options), + ) + # NOTE: The reserved octets, if they are not all zeros (#1437). + self._read_padding(schema, data, 'reserved') + # NOTE: The octets after ``opt_endofopt``, if any (#1437). + self._read_padding(schema, data, 'padding', 'padding_opts', nonzero=False) + return data - Returns: - Timestamp resolution of the current block, in units per second. + def _read_block_packet(self, schema: 'Schema_PacketBlock', *, + header: 'Schema_PCAPNG') -> 'Data_PacketBlock': + """Read PCAP-NG packet block (obsolete). - """ - if self._ctx is None: - warn(f"'{self.__class__.__name__}' object has no attribute '_get_resolution'", - AttributeWarning, stacklevel=stacklevel()) - return DEFAULT_RESOLUTION + Structure of Packet Block: - options = self._get_interface(interface_id).options - tsresol = cast('Optional[Data_IF_TSResolOption]', - options.get(Enum_OptionType.if_tsresol)) - if tsresol is None: - return DEFAULT_RESOLUTION - return tsresol.resolution + .. code-block:: text - def _get_offset(self, interface_id: 'int' = 0) -> 'int': - """Timestamp offset of the current block, in seconds. + 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 0 | Block Type = 0x00000002 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 4 | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 8 | Interface ID | Drops Count | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 12 | Timestamp (High) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 16 | Timestamp (Low) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 20 | Captured Packet Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 24 | Original Packet Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 28 / / + / Packet Data / + / variable length, padded to 32 bits / + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / / + / Options (variable) / + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - interface_id: Interface ID that the current block associates with. + schema: Parsed block schema. + header: Parsed PCAP-NG header schema. Returns: - Timestamp offset of the current block, in seconds. + Parsed packet data. """ - if self._ctx is None: - warn(f"'{self.__class__.__name__}' object has no attribute '_get_offset'", - AttributeWarning, stacklevel=stacklevel()) - return 0 + warn('PCAP-NG: Packet Block has been obsolete! Please use Enhanced Packet Block and/or ' + 'Simple Packet Block instead.', DeprecatedFormatWarning, stacklevel=stacklevel()) - options = self._get_interface(interface_id).options - tsoffset = cast('Optional[Data_IF_TSOffsetOption]', - options.get(Enum_OptionType.if_tsoffset)) - if tsoffset is None: - return 0 - return tsoffset.offset + timestamp, timestamp_epoch = self._read_timestamp(schema.timestamp_high, schema.timestamp_low, + interface_id=schema.interface_id) - def _get_timezone(self, interface_id: 'int' = 0) -> 'timezone': - """Timezone of the current block. + data = Data_PacketBlock( + type=header.type, + length=schema.length, + section_number=self._sect, + number=self._fnum, + interface_id=schema.interface_id, + drop_count=schema.drop_count, + timestamp=timestamp, + timestamp_epoch=timestamp_epoch, + captured_len=schema.captured_length, + original_len=schema.original_length, + options=self._read_pcapng_options(schema.options), + ) + # NOTE: The padding of the packet data to 32 bits, if it is not all + # zeros (#1437). + self._read_padding(schema, data, 'padding_data') + # NOTE: The octets after ``opt_endofopt``, if any (#1437). + self._read_padding(schema, data, 'padding_opts', 'padding_opts', nonzero=False) - Args: - interface_id: Interface ID that the current block associates with. + # NOTE: A captured length running past the block is kept as declared, + # and the packet data is the octets the block holds (#1405). + if schema.captured_length > len(schema.packet_data): + warn(f'PCAP-NG: [Block {header.type}] captured length {schema.captured_length} runs past ' + f'the block, which holds {len(schema.packet_data)} octet(s); kept as captured', + ProtocolWarning, stacklevel=stacklevel()) + # NOTE: The layers below are told both lengths, as a PCAP frame tells them (#2653). + token = frame_length.set((data.captured_len, data.original_len)) + try: + return self._decode_next_layer(data, self._get_linktype(schema.interface_id), + schema.captured_length) # type: ignore[return-value] + finally: + frame_length.reset(token) - Returns: - Timezone of the current block. + def _read_block_spb(self, schema: 'Schema_SimplePacketBlock', *, + header: 'Schema_PCAPNG') -> 'Data_SimplePacketBlock': + """Read PCAP-NG simple packet block (SPB). - """ - # NOTE: UTC, not the host's timezone, is the default in both branches - # below. A capture carries no timezone of its own unless it says so, and - # draft-ietf-opsawg-pcapng-02 section 4.3, Enhanced Packet Block, - # defines the block timestamp as an offset from 1970-01-01 00:00:00 UTC - # with no timezone term at all, so substituting the *reading* machine's - # zone would make the same file parse to different instants on different - # hosts. ``_get_resolution`` - # and ``_get_offset`` fall back to the format's own defaults in this - # situation; this is the matching one. - if self._ctx is None: - warn(f"'{self.__class__.__name__}' object has no attribute '_get_timezone'", - AttributeWarning, stacklevel=stacklevel()) - return datetime.timezone.utc + Structure of Simple Packet Block: - options = self._get_interface(interface_id).options - tzone = cast('Optional[Data_IF_TZoneOption]', - options.get(Enum_OptionType.if_tzone)) - # an ``if_tzone`` kept as captured (a :class:`~datetime.timedelta` of 24 - # hours or more) names no zone a datetime can be rendered in - if tzone is None or not isinstance(tzone.timezone, datetime.timezone): - return datetime.timezone.utc - return tzone.timezone + .. code-block:: text - def _get_linktype(self, interface_id: 'int' = 0) -> 'Enum_LinkType': - """Data link layer protocol type. + 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 0 | Block Type = 0x00000003 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 4 | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 8 | Original Packet Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 12 / / + / Packet Data / + / variable length, padded to 32 bits / + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - interface_id: Interface ID that the current block associates with. + schema: Parsed block schema. + header: Parsed PCAP-NG header schema. Returns: - Data link layer protocol type. - - Raises: - UnsupportedCall: If current block is not a valid packet block, i.e., - EPB, SPB or obsolete Packet Block. + Parsed packet data. """ - if self._ctx is None or self._type not in self.PACKET_TYPES: - raise UnsupportedCall(f"'{self.__class__.__name__}' object has no attribute '_get_linktype'") - return self._get_interface(interface_id).linktype + data = Data_SimplePacketBlock( + type=header.type, + length=schema.length, + section_number=self._sect, + number=self._fnum, + captured_len=len(schema.packet_data), + original_len=schema.original_len, + ) + # NOTE: The padding of the packet data to 32 bits, if it is not all + # zeros (#1437). + self._read_padding(schema, data, 'padding', 'padding_data') - def _read_timestamp(self, timestamp_high: 'int', timestamp_low: 'int', *, - interface_id: 'int' = 0) -> 'tuple[dt_type, Decimal]': - """Read timestamp. + # NOTE: A block holding more packet data than the snaplen of interface 0 + # allows keeps the excess (#1384); the specification says the + # snaplen determines the length, so the excess is reported. + snaplen = self._ctx.interfaces[0].snaplen if self._ctx is not None and self._ctx.interfaces else 0 + if snaplen and data.captured_len > snaplen: + warn(f'PCAP-NG: [Block {header.type}] packet data of {data.captured_len} octet(s) ' + f'exceeds the snaplen of {snaplen}; kept as captured', + ProtocolWarning, stacklevel=stacklevel()) + # NOTE: Likewise for more packet data than the original length, padding + # aside (#1414). + if data.captured_len > data.original_len + pad_length(data.original_len, 4): + warn(f'PCAP-NG: [Block {header.type}] packet data of {data.captured_len} octet(s) ' + f'exceeds the original length of {data.original_len}; kept as captured', + ProtocolWarning, stacklevel=stacklevel()) + # NOTE: The layers below are told both lengths, as a PCAP frame tells them (#2653). + token = frame_length.set((data.captured_len, data.original_len)) + try: + return self._decode_next_layer(data, self._get_linktype(0), data.captured_len) # type: ignore[return-value] + finally: + frame_length.reset(token) + + def _read_block_nrb(self, schema: 'Schema_NameResolutionBlock', *, + header: 'Schema_PCAPNG') -> 'Data_NameResolutionBlock': + """Read PCAP-NG name resolution block (NRB). + + Structure of Name Resolution Block: + + .. code-block:: text + + 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 0 | Block Type = 0x00000004 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 4 | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 8 | Record Type | Record Value Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 12 / Record Value / + / variable length, padded to 32 bits / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + . . + . . . . other records . . . . + . . + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Record Type = nrb_record_end | Record Value Length = 0 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / / + / Options (variable) / + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - timestamp_high: Higher 32-bit integer value of timestamp. - timestamp_low: Lower 32-bit integer value of timestamp. - interface_id: Interface ID that the current block associates with. + schema: Parsed block schema. + header: Parsed PCAP-NG header schema. Returns: - Tuple of timestamp in :class:`~datetime.datetime` object with - timezone information and :class:`decimal.Decimal` object since - UNIX-Epoch in UTC timezone. + Parsed packet data. """ - tzone = self._get_timezone(interface_id) + data = Data_NameResolutionBlock( + type=header.type, + length=schema.length, + records=self._read_nrb_records(schema.records), + options=self._read_pcapng_options(schema.options), + ) + # NOTE: The octets after ``opt_endofopt``, if any (#1437). + self._read_padding(schema, data, 'padding', 'padding_opts', nonzero=False) + return data - timestamp_raw = (timestamp_high << 32) | timestamp_low - with localcontext(prec=64): - # NOTE: This is the UTC epoch the docstring above promises, with no - # ``tzone.utcoffset(None)`` added. The block - # timestamp is an offset from the UNIX epoch, so it is - # timezone-independent by definition (draft-ietf-opsawg-pcapng-02 - # section 4.3, Enhanced Packet Block, spells out the arithmetic and - # has no timezone term), and the same draft's section 4.2, Interface - # Description Block, says of ``if_tzone`` that it "SHOULD NOT be used" - # and of ``if_tsoffset`` that it is "not intended to be used as an - # offset between local time and UTC". A shifted epoch would also - # contradict ``ts_datetime`` below, which is built from the - # unshifted value. - timestamp_epoch = decimal.Decimal(timestamp_raw) / self._get_resolution(interface_id) + \ - self._get_offset(interface_id) + def _read_block_isb(self, schema: 'Schema_InterfaceStatisticsBlock', *, + header: 'Schema_PCAPNG') -> 'Data_InterfaceStatisticsBlock': + """Read PCAP-NG interface statistics block (ISB). - ts_ratio = timestamp_epoch.as_integer_ratio() - try: - # NOTE: ``tzone`` survives only here, as the zone the instant is - # *rendered* in -- which is all a ``tzinfo`` can affect on an aware - # datetime. It never moves the instant itself. Not the stdlib's - # ``fromtimestamp``, whose range is the C library's: Windows' takes - # nothing before 1970 or after 3000 (#2099). - ts_datetime = fromtimestamp(ts_ratio[0] / ts_ratio[1], tzone) - # an epoch past what ``datetime`` or a 64-bit ``time_t`` holds, e.g. - # from an extreme ``if_tsoffset``, raises one of these - except (ValueError, OverflowError, OSError): - warn(f'PCAP-NG: [Block {self._type}] invalid timestamp: {timestamp_epoch}', - ProtocolWarning, stacklevel=stacklevel()) - ts_datetime = datetime.datetime.fromtimestamp(0, datetime.timezone.utc) + Structure of Interface Statistics Block: - return (ts_datetime, timestamp_epoch) + .. code-block:: text - @staticmethod - def _block_area(block: 'Data_PCAPNG', name: 'str') -> 'Any': - """Option or record area of a parsed block, for its ``make``. + 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 0 | Block Type = 0x00000005 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 4 | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 8 | Interface ID | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 12 | Timestamp (High) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 16 | Timestamp (Low) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 20 / / + / Options (variable) / + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - block: Block data model. - name: ``'options'`` or ``'records'``. + schema: Parsed block schema. + header: Parsed PCAP-NG header schema. Returns: - The area's octets if :meth:`read` kept it as captured, as - ``_raw``; otherwise the parsed ````. + Parsed packet data. """ - return getattr(block, f'{name}_raw', getattr(block, name)) + timestamp, timestamp_epoch = self._read_timestamp(schema.timestamp_high, schema.timestamp_low, + interface_id=schema.interface_id) + self._isb_interface_id = schema.interface_id - @staticmethod - def _read_padding(schema: 'Schema', data: 'Any', name: 'str' = 'padding', - key: 'Optional[str]' = None, *, nonzero: 'bool' = True) -> 'None': - """Keep a padding field of a parsed schema on its data model. + data = Data_InterfaceStatisticsBlock( + type=header.type, + length=schema.length, + interface_id=schema.interface_id, + timestamp=timestamp, + timestamp_epoch=timestamp_epoch, + options=self._read_pcapng_options(schema.options), + ) + # NOTE: The octets after ``opt_endofopt``, if any (#1437). + self._read_padding(schema, data, 'padding', 'padding_opts', nonzero=False) + return data - Args: - schema: Parsed block, option or record schema. - data: Its data model, updated in place. - name: Name of the padding field on ``schema``. - key: Name to keep it as on ``data``; defaults to ``name``. - nonzero: Keep the octets only when they are not all zeros -- the - form a fresh build writes -- rather than whenever there are any. + def _read_block_epb(self, schema: 'Schema_EnhancedPacketBlock', *, + header: 'Schema_PCAPNG') -> 'Data_EnhancedPacketBlock': + """Read PCAP-NG enhanced packet block (EPB). - The octets are kept as ``key`` only when there is something a rebuild - would otherwise lose, so the data model of well-formed input gains no - key (:issue:`1437`). :meth:`_make_padding` writes them back. + Structure of Enhanced Packet Block: - """ - padding = schema.__dict__.get(name) - if isinstance(padding, bytes) and (any(padding) if nonzero else padding): - data.__update__([(key or name, padding)]) + .. code-block:: text - @staticmethod - def _make_padding(schema: '_S', source: 'Any', name: 'str' = 'padding', - key: 'Optional[str]' = None) -> '_S': - """Write the padding octets :meth:`_read_padding` kept back onto a schema. + 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 0 | Block Type = 0x00000006 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 4 | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 8 | Interface ID | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 12 | Timestamp (High) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 16 | Timestamp (Low) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 20 | Captured Packet Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 24 | Original Packet Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 28 / / + / Packet Data / + / variable length, padded to 32 bits / + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / / + / Options (variable) / + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - schema: Constructed block, option or record schema, updated in place. - source: Data model or keyword arguments it was made from, if any. - name: Name of the padding field on ``schema``. - key: Name the octets are kept as on ``source``; defaults to ``name``. + schema: Parsed block schema. + header: Parsed PCAP-NG header schema. Returns: - The ``schema`` given. Without kept octets its padding stays zeros. + Parsed packet data. """ - if source is None or isinstance(schema, bytes): - return schema - padding = source.get(key or name) - if isinstance(padding, (bytes, bytearray)) and name in schema.__fields__: - setattr(schema, name, bytes(padding)) - return schema + timestamp, timestamp_epoch = self._read_timestamp(schema.timestamp_high, schema.timestamp_low, + interface_id=schema.interface_id) - def _make_options_trailer(self, block: 'Optional[Data_PCAPNG]', - options: 'list[Schema_Option | bytes]') -> 'int': - """Append the octets a parsed block held past its ``opt_endofopt``. + data = Data_EnhancedPacketBlock( + type=header.type, + length=schema.length, + section_number=self._sect, + number=self._fnum, + interface_id=schema.interface_id, + timestamp=timestamp, + timestamp_epoch=timestamp_epoch, + captured_len=schema.captured_len, + original_len=schema.original_len, + options=self._read_pcapng_options(schema.options), + ) + # NOTE: The padding of the packet data to 32 bits, if it is not all + # zeros (#1437). + self._read_padding(schema, data, 'padding_data') + # NOTE: The octets after ``opt_endofopt``, if any (#1437). + self._read_padding(schema, data, 'padding_opts', 'padding_opts', nonzero=False) - Args: - block: Block data model, if any. - options: Constructed options, extended in place. + # NOTE: A captured length running past the block is kept as declared, + # and the packet data is the octets the block holds (#1405). + if schema.captured_len > len(schema.packet_data): + warn(f'PCAP-NG: [Block {header.type}] captured length {schema.captured_len} runs past ' + f'the block, which holds {len(schema.packet_data)} octet(s); kept as captured', + ProtocolWarning, stacklevel=stacklevel()) + # NOTE: The layers below are told both lengths, as a PCAP frame tells them (#2653). + token = frame_length.set((data.captured_len, data.original_len)) + try: + return self._decode_next_layer(data, self._get_linktype(schema.interface_id), + schema.captured_len) # type: ignore[return-value] + finally: + frame_length.reset(token) - Returns: - Number of octets appended. + def _read_block_systemd(self, schema: 'Schema_SystemdJournalExportBlock', *, + header: 'Schema_PCAPNG') -> 'Data_SystemdJournalExportBlock': + """Read PCAP-NG :manpage:`systemd(1)` journal export block. - An option area ends at its ``opt_endofopt`` and the octets after it are - read as padding. :meth:`_read_padding` keeps them as ``padding_opts``, - and they are written back after the options, verbatim (:issue:`1437`). + Structure of :manpage:`systemd(1)` Journal Export Block: - """ - trailer = block.get('padding_opts') if block is not None else None - if not isinstance(trailer, bytes) or not trailer: - return 0 - options.append(trailer) - return len(trailer) + .. code-block:: text - @classmethod - def _make_packet_data(cls, block: 'Data_PCAPNG') -> 'bytes | ProtocolBase': - """Create the packet data of an EPB, SPB or PB from its data model. + 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 0 | Block Type = 0x00000009 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 4 | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 8 / / + / Journal Entry / + / variable length, padded to 32 bits / + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - block: Block data model. + schema: Parsed block schema. + header: Parsed PCAP-NG header schema. Returns: - The captured packet octets the block was parsed from, as - :attr:`~pcapkit.protocols.data.Protocol.packet` holds them, - cut to the block's ``captured_len``; or, for a data model that - carries none, the payload rebuilt from its decoded next layer. - - Note: - The cut is what keeps a block with ``captured_len`` of zero intact: - its ``packet`` can hold octets past the payload (:issue:`1275`), and - writing those back would grow the block. - - The fallback is lossy. A next layer rebuilt from its own data model - drops anything it did not decode, such as Ethernet pad octets, so - the block it yields can be shorter than its ``captured_len`` says. + Parsed packet data. """ - packet = block.get('packet') - if isinstance(packet, bytes): - return packet[:block.captured_len] - return cls._make_payload(block) - - def _make_timestamp(self, timestamp: 'Optional[float | Decimal | dt_type | int]' = None, *, - interface_id: 'int' = 0) -> 'tuple[int, int]': - """Make timestamp. + entries = schema.data + # NOTE: A malformed entry is kept as the octets captured, as one + # :obj:`bytes` item, and parses as no entries. The octets are kept on + # the block as ``entry_raw``, which the block's ``make`` writes back + # verbatim (#1406). + kept = len(entries) == 1 and isinstance(entries[0], bytes) + data = Data_SystemdJournalExportBlock( + type=header.type, + length=schema.length, + # NOTE: each entry as an OrderedMultiInfo, which ``Info`` does not + # do for a mapping held in a tuple (#1484). + data=() if kept else tuple(OrderedMultiInfo(entry) for entry in entries), # type: ignore[arg-type] + ) + if kept: + data.__update__([('entry_raw', entries[0])]) + return data - Args: - timestamp: Timestamp in seconds since UNIX-Epoch, in UTC, i.e. as - :meth:`self._read_timestamp <_read_timestamp>` returns it. - interface_id: Interface ID that the current block associates with. + def _read_block_dsb(self, schema: 'Schema_DecryptionSecretsBlock', *, + header: 'Schema_PCAPNG') -> 'Data_DecryptionSecretsBlock': + """Read PCAP-NG decryption secrets block (DSB). - Returns: - Tuple of timestamp in higher and lower 32-bit integer value, - scaled by the interface's ``if_tsresol`` and shifted by its - ``if_tsoffset``. + Structure of Decryption Secrets Block: - Notes: - No timezone conversion happens here, and none should: the field this - builds is defined as an offset from the UNIX epoch, and the read - side applies no timezone shift for it to mirror. + .. code-block:: text - Which clock a defaulted ``timestamp`` is read to follows the - resolution of the interface ``interface_id`` names, not - :attr:`self.nanosecond `. That property resolves its - interface through the parsed data model, which a block being made - has not got, so reading it here would make a ``make`` that names - no ``timestamp`` -- of an Enhanced Packet Block, an obsolete - Packet Block or an Interface Statistics Block -- raise - :exc:`AttributeError` on ``_info`` (:issue:`2362`). + 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 0 | Block Type = 0x0000000A | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 4 | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 8 | Secrets Type | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 12 | Secrets Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 16 / / + / Secrets Data / + / (variable length, padded to 32 bits) / + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / / + / Options (variable) / + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / Block Total Length / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - .. versionchanged:: 1.5.0 + Args: + schema: Parsed block schema. + header: Parsed PCAP-NG header schema. - A defaulted ``timestamp`` no longer reads - :attr:`self.nanosecond `, so it no longer requires a - parsed data model (:issue:`2362`). + Returns: + Parsed packet data. """ - with localcontext(prec=64): - resolution = self._get_resolution(interface_id) - if timestamp is None: - if resolution > RESOLUTION[False]: - timestamp = decimal.Decimal(time.time_ns()) / 1_000_000_000 - else: - timestamp = decimal.Decimal(time.time()) - else: - if isinstance(timestamp, datetime.datetime): - timestamp = totimestamp(timestamp) - timestamp = decimal.Decimal(timestamp) - - ts_info = int( - (timestamp - self._get_offset(interface_id)) * resolution - ) - return (ts_info >> 32) & 0xFFFF_FFFF, ts_info & 0xFFFF_FFFF - - def _make_mac_addr(self, addr: 'str | bytes | bytearray', *, size: 'Optional[int]' = 6, - name: 'str' = 'MAC address', alias: 'Optional[str]' = None) -> 'bytes': - """Make a MAC address, or an EUI-64 one with ``size=8``. - - Args: - addr: MAC or EUI address. - size: Octets in the address, as for :meth:`Protocol._make_mac_addr - `. - name: Name of the address, for the error message. - alias: Prefix of the error message, ``'PCAP-NG'`` by default. - - Returns: - The address packed as ``size`` octets, 6 by default. - - Raises: - ProtocolError: If ``addr`` is not a :class:`str`, :class:`bytes` - or :class:`bytearray` (a :obj:`bool` included), or is not a - ``:``- or ``-``-separated *hex* encoded address of ``size`` - octets. - - """ - prefix = 'PCAP-NG' if alias is None else alias - return super()._make_mac_addr(addr, size=size, name=name, alias=prefix) - - def _decode_next_layer(self, dict_: 'Data_PCAPNG', proto: 'Optional[int]' = None, - length: 'Optional[int]' = None, *, packet: 'Optional[dict[str, Any]]' = None) -> 'Data_PCAPNG': # pylint: disable=arguments-differ - r"""Decode next layer protocol. - - Arguments: - dict\_: info buffer - proto: next layer protocol index - length: valid (*non-padding*) length - packet: packet info (passed from :meth:`self.unpack `) - - Returns: - Current protocol with packet extracted. - - Notes: - We added a new key ``__next_type__`` to ``dict_`` to store the - next layer protocol type, and a new key ``__next_name__`` to - store the next layer protocol name. :meth:`Info.to_dict - ` carries both, and the - dumpers leave them out. - - We also added a new key ``protocols`` to ``dict_`` to store the - protocol chain of the current packet (frame). - - ``proto`` is dispatched as :meth:`_dispatch_linktype` says, so - ``LINKTYPE_RAW`` data is read as IPv4 or IPv6 (:issue:`1502`). - - """ - proto = self._dispatch_linktype(proto, length) - next_ = cast('ProtocolBase', self._import_next_layer(proto, length, packet=packet)) # type: ignore[misc,call-arg,redundant-cast] - info, chain = next_.info, next_.protochain - - # make next layer protocol name - layer = next_.info_name - # proto = next_.__class__.__name__ - - # write info and protocol chain into dict - dict_.__update__({ - layer: info, - 'protocols': chain.chain if chain else '', - '__next_type__': type(next_), - '__next_name__': layer, - }) - self._next = next_ # pylint: disable=attribute-defined-outside-init - self._protos = chain # pylint: disable=attribute-defined-outside-init - return dict_ - - def _dispatch_linktype(self, proto: 'Optional[int]', length: 'Optional[int]' = None) -> 'Optional[int]': - """Link type a packet block's data is dispatched under. + name = self._lookup_registry(self.__secrets__, schema.secrets_type) + if isinstance(name, str): + meth_name = f'_read_secrets_{name}' + meth = cast('SecretsParser', + getattr(self, meth_name, self._read_secrets_unknown)) + else: + meth = name[0] + secrets_data = meth(schema.secrets_data, block=schema) - Arguments: - proto: link type of the interface the block was captured on - length: valid (*non-padding*) length + data = Data_DecryptionSecretsBlock( + type=header.type, + length=schema.length, + secrets_type=schema.secrets_type, + secrets_length=schema.secrets_length, + secrets_data=secrets_data, + options=self._read_pcapng_options(schema.options), + ) + # NOTE: The padding of the secrets to 32 bits, if it is not all zeros + # (#1437). + self._read_padding(schema, data, 'padding_data') + # NOTE: The octets after ``opt_endofopt``, if any (#1437). + self._read_padding(schema, data, 'padding_opts', 'padding_opts', nonzero=False) + return data - Returns: - For :attr:`LinkType.RAW ` - data whose first octet carries IP version 4 or 6, - :attr:`LinkType.IPV4 ` or - :attr:`LinkType.IPV6 `; - otherwise ``proto``. + def _read_block_cb(self, schema: 'Schema_CustomBlock', *, + header: 'Schema_PCAPNG') -> 'Data_CustomBlock': + """Read PCAP-NG custom block (CB). - Note: - As :meth:`Frame._dispatch_linktype - ` does - for PCAP: ``LINKTYPE_RAW`` (101) data is IPv4 or IPv6 by its version - nibble, an entry registered under ``RAW`` itself is used as it - stands, and data of any other version is kept as - :class:`~pcapkit.protocols.misc.raw.protocol.Raw` (:issue:`1502`). + Structure of Custom Block: - """ - if proto != Enum_LinkType.RAW or Enum_LinkType.RAW in self.__proto__: - return proto - data = self._get_payload()[:length] - version = data[0] >> 4 if data else None - if version == 4: - return Enum_LinkType.IPV4 - if version == 6: - return Enum_LinkType.IPV6 - return proto + .. code-block:: text - def _read_block_unknown(self, schema: 'Schema_UnknownBlock', *, - header: 'Schema_PCAPNG') -> 'Data_UnknownBlock': - """Read unknown PCAP-NG block. + 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 0 | Block Type = 0x00000BAD or 0x40000BAD | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 4 | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 8 | Private Enterprise Number (PEN) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 12 / / + / Custom Data / + / variable length, padded to 32 bits / + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / / + / Options (variable) / + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: schema: Parsed block schema. @@ -2474,10 +2450,11 @@ def _read_block_unknown(self, schema: 'Schema_UnknownBlock', *, Parsed packet data. """ - data = Data_UnknownBlock( + data = Data_CustomBlock( type=header.type, length=schema.length, - body=schema.body, + pen=schema.pen, + data=schema.data, ) return data @@ -2535,604 +2512,365 @@ def _read_block_shb(self, schema: 'Schema_SectionHeaderBlock', *, self._read_padding(schema, data, 'padding', 'padding_opts', nonzero=False) return data - def _read_block_idb(self, schema: 'Schema_InterfaceDescriptionBlock', *, - header: 'Schema_PCAPNG') -> 'Data_InterfaceDescriptionBlock': - """Read PCAP-NG interface description block (IDB). + def _read_option_unknown(self, schema: 'Schema_UnknownOption', *, + options: 'Option') -> 'Data_UnknownOption': + """Read unknown PCAP-NG option. - Structure of Interface Description Block: + Args: + schema: Parsed option schema. + options: Parsed PCAP-NG options. - .. code-block:: text + Returns: + Parsed option data. - 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 0 | Block Type = 0x00000001 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 4 | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 8 | LinkType | Reserved | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 12 | SnapLen | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 16 / / - / Options (variable) / - / / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + """ + option = Data_UnknownOption( + type=schema.type, + length=schema.length, + data=schema.data, + ) + return option + + def _read_option_endofopt(self, schema: 'Schema_EndOfOption', *, + options: 'Option') -> 'Data_EndOfOption': + """Read PCAP-NG ``opt_endofopt`` option. Args: - schema: Parsed block schema. - header: Parsed PCAP-NG header schema. + schema: Parsed option schema. + options: Parsed PCAP-NG options. Returns: - Parsed packet data. + Parsed option data. """ - data = Data_InterfaceDescriptionBlock( - type=header.type, + if self._opt[schema.type] > 0: + raise ProtocolError(f'PCAP-NG: [opt_endofopt] option must be only one, ' + f'not {self._opt[schema.type] + 1}') + if schema.length != 0: + raise ProtocolError(f'PCAP-NG: [opt_endofopt] invalid length (expected 0, got {schema.length})') + + option = Data_EndOfOption( + type=schema.type, length=schema.length, - linktype=schema.linktype, - snaplen=schema.snaplen, - options=self._read_pcapng_options(schema.options), ) - # NOTE: The reserved octets, if they are not all zeros (#1437). - self._read_padding(schema, data, 'reserved') - # NOTE: The octets after ``opt_endofopt``, if any (#1437). - self._read_padding(schema, data, 'padding', 'padding_opts', nonzero=False) - return data + return option - def _read_block_epb(self, schema: 'Schema_EnhancedPacketBlock', *, - header: 'Schema_PCAPNG') -> 'Data_EnhancedPacketBlock': - """Read PCAP-NG enhanced packet block (EPB). + def _read_option_comment(self, schema: 'Schema_CommentOption', *, + options: 'Option') -> 'Data_CommentOption': + """Read PCAP-NG ``opt_comment`` option. - Structure of Enhanced Packet Block: + Args: + schema: Parsed option schema. + options: Parsed PCAP-NG options. + + Returns: + Parsed option data. + + """ + option = Data_CommentOption( + type=schema.type, + length=schema.length, + comment=schema.comment, + ) + return option + + def _read_option_custom(self, schema: 'Schema_CustomOption', *, + options: 'Option') -> 'Data_CustomOption': + """Read PCAP-NG ``opt_custom`` option. + + Structure of PCAP-NG ``opt_custom`` option: .. code-block:: text - 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 0 | Block Type = 0x00000006 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 4 | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 8 | Interface ID | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 12 | Timestamp (High) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 16 | Timestamp (Low) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 20 | Captured Packet Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 24 | Original Packet Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 28 / / - / Packet Data / - / variable length, padded to 32 bits / - / / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - / / - / Options (variable) / - / / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Custom Option Code | Option Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Private Enterprise Number (PEN) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / Custom Data / + / variable length, padded to 32 bits / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - schema: Parsed block schema. - header: Parsed PCAP-NG header schema. + schema: Parsed option schema. + options: Parsed PCAP-NG options. Returns: - Parsed packet data. + Parsed option data. + + Raises: + ProtocolError: If the option is shorter than its 4-octet private + enterprise number, which Option Length includes. """ - timestamp, timestamp_epoch = self._read_timestamp(schema.timestamp_high, schema.timestamp_low, - interface_id=schema.interface_id) + if schema.length < 4: + raise ProtocolError(f'PCAP-NG: [opt_custom] invalid length (expected at least 4, got {schema.length})') - data = Data_EnhancedPacketBlock( - type=header.type, + option = Data_CustomOption( + type=schema.type, length=schema.length, - section_number=self._sect, - number=self._fnum, - interface_id=schema.interface_id, - timestamp=timestamp, - timestamp_epoch=timestamp_epoch, - captured_len=schema.captured_len, - original_len=schema.original_len, - options=self._read_pcapng_options(schema.options), + pen=schema.pen, + data=schema.data, ) - # NOTE: The padding of the packet data to 32 bits, if it is not all - # zeros (#1437). - self._read_padding(schema, data, 'padding_data') - # NOTE: The octets after ``opt_endofopt``, if any (#1437). - self._read_padding(schema, data, 'padding_opts', 'padding_opts', nonzero=False) + return option - # NOTE: A captured length running past the block is kept as declared, - # and the packet data is the octets the block holds (#1405). - if schema.captured_len > len(schema.packet_data): - warn(f'PCAP-NG: [Block {header.type}] captured length {schema.captured_len} runs past ' - f'the block, which holds {len(schema.packet_data)} octet(s); kept as captured', - ProtocolWarning, stacklevel=stacklevel()) - # NOTE: The layers below are told both lengths, as a PCAP frame tells them (#2653). - token = frame_length.set((data.captured_len, data.original_len)) - try: - return self._decode_next_layer(data, self._get_linktype(schema.interface_id), - schema.captured_len) # type: ignore[return-value] - finally: - frame_length.reset(token) + def _read_option_if_name(self, schema: 'Schema_IF_NameOption', *, + options: 'Option') -> 'Data_IF_NameOption': + """Read PCAP-NG ``if_name`` option. - def _read_block_spb(self, schema: 'Schema_SimplePacketBlock', *, - header: 'Schema_PCAPNG') -> 'Data_SimplePacketBlock': - """Read PCAP-NG simple packet block (SPB). + Args: + schema: Parsed option schema. + options: Parsed PCAP-NG options. - Structure of Simple Packet Block: + Returns: + Parsed option data. - .. code-block:: text + """ + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_name] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[schema.type] > 0: + raise ProtocolError(f'PCAP-NG: [if_name] option must be only one, ' + f'not {self._opt[schema.type] + 1}') - 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 0 | Block Type = 0x00000003 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 4 | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 8 | Original Packet Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 12 / / - / Packet Data / - / variable length, padded to 32 bits / - / / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + option = Data_IF_NameOption( + type=schema.type, + length=schema.length, + name=schema.name, + ) + return option + + def _read_option_if_description(self, schema: 'Schema_IF_DescriptionOption', *, + options: 'Option') -> 'Data_IF_DescriptionOption': + """Read PCAP-NG ``if_description`` option. Args: - schema: Parsed block schema. - header: Parsed PCAP-NG header schema. + schema: Parsed option schema. + options: Parsed PCAP-NG options. Returns: - Parsed packet data. + Parsed option data. """ - data = Data_SimplePacketBlock( - type=header.type, + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_description] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[schema.type] > 0: + raise ProtocolError(f'PCAP-NG: [if_description] option must be only one, ' + f'not {self._opt[schema.type] + 1}') + + option = Data_IF_DescriptionOption( + type=schema.type, length=schema.length, - section_number=self._sect, - number=self._fnum, - captured_len=len(schema.packet_data), - original_len=schema.original_len, + description=schema.description, ) - # NOTE: The padding of the packet data to 32 bits, if it is not all - # zeros (#1437). - self._read_padding(schema, data, 'padding', 'padding_data') + return option - # NOTE: A block holding more packet data than the snaplen of interface 0 - # allows keeps the excess (#1384); the specification says the - # snaplen determines the length, so the excess is reported. - snaplen = self._ctx.interfaces[0].snaplen if self._ctx is not None and self._ctx.interfaces else 0 - if snaplen and data.captured_len > snaplen: - warn(f'PCAP-NG: [Block {header.type}] packet data of {data.captured_len} octet(s) ' - f'exceeds the snaplen of {snaplen}; kept as captured', - ProtocolWarning, stacklevel=stacklevel()) - # NOTE: Likewise for more packet data than the original length, padding - # aside (#1414). - if data.captured_len > data.original_len + pad_length(data.original_len, 4): - warn(f'PCAP-NG: [Block {header.type}] packet data of {data.captured_len} octet(s) ' - f'exceeds the original length of {data.original_len}; kept as captured', - ProtocolWarning, stacklevel=stacklevel()) - # NOTE: The layers below are told both lengths, as a PCAP frame tells them (#2653). - token = frame_length.set((data.captured_len, data.original_len)) - try: - return self._decode_next_layer(data, self._get_linktype(0), data.captured_len) # type: ignore[return-value] - finally: - frame_length.reset(token) + def _read_option_if_ipv4(self, schema: 'Schema_IF_IPv4AddrOption', *, + options: 'Option') -> 'Data_IF_IPv4AddrOption': + """Read PCAP-NG ``if_IPv4addr`` option. - def _read_block_nrb(self, schema: 'Schema_NameResolutionBlock', *, - header: 'Schema_PCAPNG') -> 'Data_NameResolutionBlock': - """Read PCAP-NG name resolution block (NRB). + Args: + schema: Parsed option schema. + options: Parsed PCAP-NG options. - Structure of Name Resolution Block: + Returns: + Parsed option data. - .. code-block:: text + """ + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_IPv4addr] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if schema.length != 8: + raise ProtocolError(f'PCAP-NG: [if_IPv4addr] invalid length (expected 8, got {schema.length})') - 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 0 | Block Type = 0x00000004 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 4 | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 8 | Record Type | Record Value Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 12 / Record Value / - / variable length, padded to 32 bits / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - . . - . . . . other records . . . . - . . - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Record Type = nrb_record_end | Record Value Length = 0 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - / / - / Options (variable) / - / / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + option = Data_IF_IPv4AddrOption( + type=schema.type, + length=schema.length, + interface=schema.interface, + ) + return option + + def _read_option_if_ipv6(self, schema: 'Schema_IF_IPv6AddrOption', *, + options: 'Option') -> 'Data_IF_IPv6AddrOption': + """Read PCAP-NG ``if_IPv6addr`` option. Args: - schema: Parsed block schema. - header: Parsed PCAP-NG header schema. + schema: Parsed option schema. + options: Parsed PCAP-NG options. Returns: - Parsed packet data. + Parsed option data. """ - data = Data_NameResolutionBlock( - type=header.type, + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_IPv6addr] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if schema.length != 17: + raise ProtocolError(f'PCAP-NG: [if_IPv6addr] invalid length (expected 17, got {schema.length})') + + option = Data_IF_IPv6AddrOption( + type=schema.type, length=schema.length, - records=self._read_nrb_records(schema.records), - options=self._read_pcapng_options(schema.options), + interface=schema.interface, ) - # NOTE: The octets after ``opt_endofopt``, if any (#1437). - self._read_padding(schema, data, 'padding', 'padding_opts', nonzero=False) - return data - - def _read_block_isb(self, schema: 'Schema_InterfaceStatisticsBlock', *, - header: 'Schema_PCAPNG') -> 'Data_InterfaceStatisticsBlock': - """Read PCAP-NG interface statistics block (ISB). + return option - Structure of Interface Statistics Block: - - .. code-block:: text - - 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 0 | Block Type = 0x00000005 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 4 | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 8 | Interface ID | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 12 | Timestamp (High) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 16 | Timestamp (Low) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 20 / / - / Options (variable) / - / / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + def _read_option_if_mac(self, schema: 'Schema_IF_MACAddrOption', *, + options: 'Option') -> 'Data_IF_MACAddrOption': + """Read PCAP-NG ``if_MACaddr`` option. Args: - schema: Parsed block schema. - header: Parsed PCAP-NG header schema. + schema: Parsed option schema. + options: Parsed PCAP-NG options. Returns: - Parsed packet data. + Parsed option data. """ - timestamp, timestamp_epoch = self._read_timestamp(schema.timestamp_high, schema.timestamp_low, - interface_id=schema.interface_id) - self._isb_interface_id = schema.interface_id + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_MACaddr] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[schema.type] > 0: + raise ProtocolError(f'PCAP-NG: [if_MACaddr] option must be only one, ' + f'not {self._opt[schema.type] + 1}') + if schema.length != 6: + raise ProtocolError(f'PCAP-NG: [if_MACaddr] invalid length (expected 6, got {schema.length})') - data = Data_InterfaceStatisticsBlock( - type=header.type, + option = Data_IF_MACAddrOption( + type=schema.type, length=schema.length, - interface_id=schema.interface_id, - timestamp=timestamp, - timestamp_epoch=timestamp_epoch, - options=self._read_pcapng_options(schema.options), + interface=self._read_mac_addr(schema.interface), ) - # NOTE: The octets after ``opt_endofopt``, if any (#1437). - self._read_padding(schema, data, 'padding', 'padding_opts', nonzero=False) - return data - - def _read_block_systemd(self, schema: 'Schema_SystemdJournalExportBlock', *, - header: 'Schema_PCAPNG') -> 'Data_SystemdJournalExportBlock': - """Read PCAP-NG :manpage:`systemd(1)` journal export block. - - Structure of :manpage:`systemd(1)` Journal Export Block: - - .. code-block:: text + return option - 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 0 | Block Type = 0x00000009 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 4 | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 8 / / - / Journal Entry / - / variable length, padded to 32 bits / - / / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + def _read_option_if_eui(self, schema: 'Schema_IF_EUIAddrOption', *, + options: 'Option') -> 'Data_IF_EUIAddrOption': + """Read PCAP-NG ``if_EUIaddr`` option. Args: - schema: Parsed block schema. - header: Parsed PCAP-NG header schema. + schema: Parsed option schema. + options: Parsed PCAP-NG options. Returns: - Parsed packet data. + Parsed option data. """ - entries = schema.data - # NOTE: A malformed entry is kept as the octets captured, as one - # :obj:`bytes` item, and parses as no entries. The octets are kept on - # the block as ``entry_raw``, which the block's ``make`` writes back - # verbatim (#1406). - kept = len(entries) == 1 and isinstance(entries[0], bytes) - data = Data_SystemdJournalExportBlock( - type=header.type, + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_EUIaddr] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[schema.type] > 0: + raise ProtocolError(f'PCAP-NG: [if_EUIaddr] option must be only one, ' + f'not {self._opt[schema.type] + 1}') + if schema.length != 8: + raise ProtocolError(f'PCAP-NG: [if_EUIaddr] invalid length (expected 8, got {schema.length})') + + option = Data_IF_EUIAddrOption( + type=schema.type, length=schema.length, - # NOTE: each entry as an OrderedMultiInfo, which ``Info`` does not - # do for a mapping held in a tuple (#1484). - data=() if kept else tuple(OrderedMultiInfo(entry) for entry in entries), # type: ignore[arg-type] + interface=self._read_mac_addr(schema.interface), ) - if kept: - data.__update__([('entry_raw', entries[0])]) - return data - - def _read_block_dsb(self, schema: 'Schema_DecryptionSecretsBlock', *, - header: 'Schema_PCAPNG') -> 'Data_DecryptionSecretsBlock': - """Read PCAP-NG decryption secrets block (DSB). - - Structure of Decryption Secrets Block: - - .. code-block:: text + return option - 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 0 | Block Type = 0x0000000A | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 4 | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 8 | Secrets Type | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 12 | Secrets Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 16 / / - / Secrets Data / - / (variable length, padded to 32 bits) / - / / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - / / - / Options (variable) / - / / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - / Block Total Length / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + def _read_option_if_speed(self, schema: 'Schema_IF_SpeedOption', *, + options: 'Option') -> 'Data_IF_SpeedOption': + """Read PCAP-NG ``if_speed`` option. Args: - schema: Parsed block schema. - header: Parsed PCAP-NG header schema. + schema: Parsed option schema. + options: Parsed PCAP-NG options. Returns: - Parsed packet data. + Parsed option data. """ - name = self._lookup_registry(self.__secrets__, schema.secrets_type) - if isinstance(name, str): - meth_name = f'_read_secrets_{name}' - meth = cast('SecretsParser', - getattr(self, meth_name, self._read_secrets_unknown)) - else: - meth = name[0] - secrets_data = meth(schema.secrets_data, block=schema) + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_speed] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[schema.type] > 0: + raise ProtocolError(f'PCAP-NG: [if_speed] option must be only one, ' + f'not {self._opt[schema.type] + 1}') + if schema.length != 8: + raise ProtocolError(f'PCAP-NG: [if_speed] invalid length (expected 8, got {schema.length})') - data = Data_DecryptionSecretsBlock( - type=header.type, + option = Data_IF_SpeedOption( + type=schema.type, length=schema.length, - secrets_type=schema.secrets_type, - secrets_length=schema.secrets_length, - secrets_data=secrets_data, - options=self._read_pcapng_options(schema.options), + speed=schema.speed, ) - # NOTE: The padding of the secrets to 32 bits, if it is not all zeros - # (#1437). - self._read_padding(schema, data, 'padding_data') - # NOTE: The octets after ``opt_endofopt``, if any (#1437). - self._read_padding(schema, data, 'padding_opts', 'padding_opts', nonzero=False) - return data - - def _read_block_cb(self, schema: 'Schema_CustomBlock', *, - header: 'Schema_PCAPNG') -> 'Data_CustomBlock': - """Read PCAP-NG custom block (CB). - - Structure of Custom Block: - - .. code-block:: text + return option - 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 0 | Block Type = 0x00000BAD or 0x40000BAD | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 4 | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 8 | Private Enterprise Number (PEN) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 12 / / - / Custom Data / - / variable length, padded to 32 bits / - / / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - / / - / Options (variable) / - / / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + def _read_option_if_tsresol(self, schema: 'Schema_IF_TSResolOption', *, + options: 'Option') -> 'Data_IF_TSResolOption': + """Read PCAP-NG ``if_tsresol`` option. Args: - schema: Parsed block schema. - header: Parsed PCAP-NG header schema. + schema: Parsed option schema. + options: Parsed PCAP-NG options. Returns: - Parsed packet data. + Parsed option data. """ - data = Data_CustomBlock( - type=header.type, + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_tsresol] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[schema.type] > 0: + raise ProtocolError(f'PCAP-NG: [if_tsresol] option must be only one, ' + f'not {self._opt[schema.type] + 1}') + if schema.length != 1: + raise ProtocolError(f'PCAP-NG: [if_tsresol] invalid length (expected 1, got {schema.length})') + + option = Data_IF_TSResolOption( + type=schema.type, length=schema.length, - pen=schema.pen, - data=schema.data, + resolution=schema.resolution, ) - return data + return option - def _read_block_packet(self, schema: 'Schema_PacketBlock', *, - header: 'Schema_PCAPNG') -> 'Data_PacketBlock': - """Read PCAP-NG packet block (obsolete). + def _read_option_if_tzone(self, schema: 'Schema_IF_TZoneOption', *, + options: 'Option') -> 'Data_IF_TZoneOption': + """Read PCAP-NG ``if_tzone`` option. - Structure of Packet Block: + Args: + schema: Parsed option schema. + options: Parsed PCAP-NG options. - .. code-block:: text - - 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 0 | Block Type = 0x00000002 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 4 | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 8 | Interface ID | Drops Count | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 12 | Timestamp (High) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 16 | Timestamp (Low) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 20 | Captured Packet Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 24 | Original Packet Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 28 / / - / Packet Data / - / variable length, padded to 32 bits / - / / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - / / - / Options (variable) / - / / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - Args: - schema: Parsed block schema. - header: Parsed PCAP-NG header schema. - - Returns: - Parsed packet data. + Returns: + Parsed option data. """ - warn('PCAP-NG: Packet Block has been obsolete! Please use Enhanced Packet Block and/or ' - 'Simple Packet Block instead.', DeprecatedFormatWarning, stacklevel=stacklevel()) + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_tzone] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[schema.type] > 0: + raise ProtocolError(f'PCAP-NG: [if_tzone] option must be only one, ' + f'not {self._opt[schema.type] + 1}') + if schema.length != 4: + raise ProtocolError(f'PCAP-NG: [if_tzone] invalid length (expected 4, got {schema.length})') - timestamp, timestamp_epoch = self._read_timestamp(schema.timestamp_high, schema.timestamp_low, - interface_id=schema.interface_id) + offset = datetime.timedelta(seconds=schema.tzone) + try: + timezone = datetime.timezone(offset) # type: datetime.timezone | datetime.timedelta + except ValueError: + # a :class:`~datetime.timezone` holds strictly less than 24 hours; + # the signed 32-bit field holds far more, so keep it as captured + warn(f'PCAP-NG: [if_tzone] offset of {schema.tzone} second(s) is not within 24 hours ' + f'of UTC; kept as captured', ProtocolWarning, stacklevel=stacklevel()) + timezone = offset - data = Data_PacketBlock( - type=header.type, + option = Data_IF_TZoneOption( + type=schema.type, length=schema.length, - section_number=self._sect, - number=self._fnum, - interface_id=schema.interface_id, - drop_count=schema.drop_count, - timestamp=timestamp, - timestamp_epoch=timestamp_epoch, - captured_len=schema.captured_length, - original_len=schema.original_length, - options=self._read_pcapng_options(schema.options), + timezone=timezone, ) - # NOTE: The padding of the packet data to 32 bits, if it is not all - # zeros (#1437). - self._read_padding(schema, data, 'padding_data') - # NOTE: The octets after ``opt_endofopt``, if any (#1437). - self._read_padding(schema, data, 'padding_opts', 'padding_opts', nonzero=False) - - # NOTE: A captured length running past the block is kept as declared, - # and the packet data is the octets the block holds (#1405). - if schema.captured_length > len(schema.packet_data): - warn(f'PCAP-NG: [Block {header.type}] captured length {schema.captured_length} runs past ' - f'the block, which holds {len(schema.packet_data)} octet(s); kept as captured', - ProtocolWarning, stacklevel=stacklevel()) - # NOTE: The layers below are told both lengths, as a PCAP frame tells them (#2653). - token = frame_length.set((data.captured_len, data.original_len)) - try: - return self._decode_next_layer(data, self._get_linktype(schema.interface_id), - schema.captured_length) # type: ignore[return-value] - finally: - frame_length.reset(token) - - def _read_pcapng_options(self, options_schema: 'list[Schema_Option]') -> 'Option': - """Read PCAP-NG options. - - Structure of PCAP-NG option: - - .. code-block:: text - - 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option Code | Option Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - / Option Value / - / variable length, padded to 32 bits / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - / / - / . . . other options . . . / - / / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Option Code == opt_endofopt | Option Length == 0 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - Args: - options_schema: Parsed PCAP-NG options. - - Returns: - Parsed PCAP-NG options data. - - """ - options = OrderedMultiDict() # type: Option - - # an area kept as captured holds no options to read - if any(isinstance(schema, bytes) for schema in options_schema): - return options - - for schema in options_schema: - type = schema.type - name = self._lookup_registry(self.__option__, _option_key(type)) - - if isinstance(name, str): - meth_name = f'_read_option_{name}' - meth = cast('OptionParser', - getattr(self, meth_name, self._read_option_unknown)) - else: - meth = name[0] - data = meth(schema, options=options) - # NOTE: The padding to 32 bits, if it is not all zeros (#1437). - self._read_padding(schema, data) - - # record option data - options.add(type, data) - self._opt[type] += 1 - - # break when ``opt_endofopt`` is reached - if type == Enum_OptionType.opt_endofopt: - break - - return options + return option - def _read_option_unknown(self, schema: 'Schema_UnknownOption', *, - options: 'Option') -> 'Data_UnknownOption': - """Read unknown PCAP-NG option. + def _read_option_if_filter(self, schema: 'Schema_IF_FilterOption', *, + options: 'Option') -> 'Data_IF_FilterOption': + """Read PCAP-NG ``if_filter`` option. Args: schema: Parsed option schema. @@ -3142,16 +2880,26 @@ def _read_option_unknown(self, schema: 'Schema_UnknownOption', *, Parsed option data. """ - option = Data_UnknownOption( + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_filter] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[schema.type] > 0: + raise ProtocolError(f'PCAP-NG: [if_filter] option must be only one, ' + f'not {self._opt[schema.type] + 1}') + if schema.length < 1: + raise ProtocolError(f'PCAP-NG: [if_filter] invalid length (expected 1+, got {schema.length})') + + option = Data_IF_FilterOption( type=schema.type, length=schema.length, - data=schema.data, + code=schema.code, + expression=schema.filter, ) return option - def _read_option_endofopt(self, schema: 'Schema_EndOfOption', *, - options: 'Option') -> 'Data_EndOfOption': - """Read PCAP-NG ``opt_endofopt`` option. + def _read_option_if_os(self, schema: 'Schema_IF_OSOption', *, + options: 'Option') -> 'Data_IF_OSOption': + """Read PCAP-NG ``if_os`` option. Args: schema: Parsed option schema. @@ -3161,21 +2909,23 @@ def _read_option_endofopt(self, schema: 'Schema_EndOfOption', *, Parsed option data. """ + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_os] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [opt_endofopt] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [if_os] option must be only one, ' f'not {self._opt[schema.type] + 1}') - if schema.length != 0: - raise ProtocolError(f'PCAP-NG: [opt_endofopt] invalid length (expected 0, got {schema.length})') - option = Data_EndOfOption( + option = Data_IF_OSOption( type=schema.type, length=schema.length, + os=schema.os, ) return option - def _read_option_comment(self, schema: 'Schema_CommentOption', *, - options: 'Option') -> 'Data_CommentOption': - """Read PCAP-NG ``opt_comment`` option. + def _read_option_if_fcslen(self, schema: 'Schema_IF_FCSLenOption', *, + options: 'Option') -> 'Data_IF_FCSLenOption': + """Read PCAP-NG ``if_fcslen`` option. Args: schema: Parsed option schema. @@ -3185,31 +2935,25 @@ def _read_option_comment(self, schema: 'Schema_CommentOption', *, Parsed option data. """ - option = Data_CommentOption( + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_fcslen] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[schema.type] > 0: + raise ProtocolError(f'PCAP-NG: [if_fcslen] option must be only one, ' + f'not {self._opt[schema.type] + 1}') + if schema.length != 1: + raise ProtocolError(f'PCAP-NG: [if_fcslen] invalid length (expected 1, got {schema.length})') + + option = Data_IF_FCSLenOption( type=schema.type, length=schema.length, - comment=schema.comment, + fcs_length=schema.fcslen, ) return option - def _read_option_custom(self, schema: 'Schema_CustomOption', *, - options: 'Option') -> 'Data_CustomOption': - """Read PCAP-NG ``opt_custom`` option. - - Structure of PCAP-NG ``opt_custom`` option: - - .. code-block:: text - - 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Custom Option Code | Option Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Private Enterprise Number (PEN) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - / Custom Data / - / variable length, padded to 32 bits / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + def _read_option_if_tsoffset(self, schema: 'Schema_IF_TSOffsetOption', *, + options: 'Option') -> 'Data_IF_TSOffsetOption': + """Read PCAP-NG ``if_tsoffset`` option. Args: schema: Parsed option schema. @@ -3218,25 +2962,26 @@ def _read_option_custom(self, schema: 'Schema_CustomOption', *, Returns: Parsed option data. - Raises: - ProtocolError: If the option is shorter than its 4-octet private - enterprise number, which Option Length includes. - """ - if schema.length < 4: - raise ProtocolError(f'PCAP-NG: [opt_custom] invalid length (expected at least 4, got {schema.length})') + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_tsoffset] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[schema.type] > 0: + raise ProtocolError(f'PCAP-NG: [if_tsoffset] option must be only one, ' + f'not {self._opt[schema.type] + 1}') + if schema.length != 8: + raise ProtocolError(f'PCAP-NG: [if_tsoffset] invalid length (expected 8, got {schema.length})') - option = Data_CustomOption( + option = Data_IF_TSOffsetOption( type=schema.type, length=schema.length, - pen=schema.pen, - data=schema.data, + offset=schema.tsoffset, ) return option - def _read_option_if_name(self, schema: 'Schema_IF_NameOption', *, - options: 'Option') -> 'Data_IF_NameOption': - """Read PCAP-NG ``if_name`` option. + def _read_option_if_hardware(self, schema: 'Schema_IF_HardwareOption', *, + options: 'Option') -> 'Data_IF_HardwareOption': + """Read PCAP-NG ``if_hardware`` option. Args: schema: Parsed option schema. @@ -3247,22 +2992,22 @@ def _read_option_if_name(self, schema: 'Schema_IF_NameOption', *, """ if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_name] option must be in Interface Description Block, ' + raise ProtocolError(f'PCAP-NG: [if_hardware] option must be in Interface Description Block, ' f'not {_block_type_name(self._type)}') if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [if_name] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [if_hardware] option must be only one, ' f'not {self._opt[schema.type] + 1}') - option = Data_IF_NameOption( + option = Data_IF_HardwareOption( type=schema.type, length=schema.length, - name=schema.name, + hardware=schema.hardware, ) return option - def _read_option_if_description(self, schema: 'Schema_IF_DescriptionOption', *, - options: 'Option') -> 'Data_IF_DescriptionOption': - """Read PCAP-NG ``if_description`` option. + def _read_option_if_txspeed(self, schema: 'Schema_IF_TxSpeedOption', *, + options: 'Option') -> 'Data_IF_TxSpeedOption': + """Read PCAP-NG ``if_txspeed`` option. Args: schema: Parsed option schema. @@ -3273,22 +3018,24 @@ def _read_option_if_description(self, schema: 'Schema_IF_DescriptionOption', *, """ if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_description] option must be in Interface Description Block, ' + raise ProtocolError(f'PCAP-NG: [if_txspeed] option must be in Interface Description Block, ' f'not {_block_type_name(self._type)}') if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [if_description] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [if_txspeed] option must be only one, ' f'not {self._opt[schema.type] + 1}') + if schema.length != 8: + raise ProtocolError(f'PCAP-NG: [if_txspeed] invalid length (expected 8, got {schema.length})') - option = Data_IF_DescriptionOption( + option = Data_IF_TxSpeedOption( type=schema.type, length=schema.length, - description=schema.description, + speed=schema.tx_speed, ) return option - def _read_option_if_ipv4(self, schema: 'Schema_IF_IPv4AddrOption', *, - options: 'Option') -> 'Data_IF_IPv4AddrOption': - """Read PCAP-NG ``if_IPv4addr`` option. + def _read_option_if_rxspeed(self, schema: 'Schema_IF_RxSpeedOption', *, + options: 'Option') -> 'Data_IF_RxSpeedOption': + """Read PCAP-NG ``if_rxspeed`` option. Args: schema: Parsed option schema. @@ -3299,21 +3046,24 @@ def _read_option_if_ipv4(self, schema: 'Schema_IF_IPv4AddrOption', *, """ if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_IPv4addr] option must be in Interface Description Block, ' + raise ProtocolError(f'PCAP-NG: [if_rxspeed] option must be in Interface Description Block, ' f'not {_block_type_name(self._type)}') + if self._opt[schema.type] > 0: + raise ProtocolError(f'PCAP-NG: [if_rxspeed] option must be only one, ' + f'not {self._opt[schema.type] + 1}') if schema.length != 8: - raise ProtocolError(f'PCAP-NG: [if_IPv4addr] invalid length (expected 8, got {schema.length})') + raise ProtocolError(f'PCAP-NG: [if_rxspeed] invalid length (expected 8, got {schema.length})') - option = Data_IF_IPv4AddrOption( + option = Data_IF_RxSpeedOption( type=schema.type, length=schema.length, - interface=schema.interface, + speed=schema.rx_speed, ) return option - def _read_option_if_ipv6(self, schema: 'Schema_IF_IPv6AddrOption', *, - options: 'Option') -> 'Data_IF_IPv6AddrOption': - """Read PCAP-NG ``if_IPv6addr`` option. + def _read_option_epb_flags(self, schema: 'Schema_EPB_FlagsOption', *, + options: 'Option') -> 'Data_EPB_FlagsOption': + """Read PCAP-NG ``epb_flags`` option. Args: schema: Parsed option schema. @@ -3323,22 +3073,39 @@ def _read_option_if_ipv6(self, schema: 'Schema_IF_IPv6AddrOption', *, Parsed option data. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_IPv6addr] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Enhanced_Packet_Block: + raise ProtocolError(f'PCAP-NG: [epb_flags] option must be in Enhanced Packet Block, ' f'not {_block_type_name(self._type)}') - if schema.length != 17: - raise ProtocolError(f'PCAP-NG: [if_IPv6addr] invalid length (expected 17, got {schema.length})') + if self._opt[schema.type] > 0: + raise ProtocolError(f'PCAP-NG: [epb_flags] option must be only one, ' + f'not {self._opt[schema.type] + 1}') + if schema.length != 4: + raise ProtocolError(f'PCAP-NG: [epb_flags] invalid length (expected 4, got {schema.length})') - option = Data_IF_IPv6AddrOption( + option = Data_EPB_FlagsOption( type=schema.type, length=schema.length, - interface=schema.interface, + direction=PacketDirection(schema.flags['direction']), + reception=PacketReception(schema.flags['reception']), + fcs_len=schema.flags['fcs_len'], + checksum_not_ready=bool(schema.flags['checksum_not_ready']), + checksum_valid=bool(schema.flags['checksum_valid']), + tcp_segmentation_offloaded=bool(schema.flags['tcp_segmentation_offloaded']), + reserved=schema.flags['reserved'], + crc_error=bool(schema.flags['crc_error']), + too_long=bool(schema.flags['too_long']), + too_short=bool(schema.flags['too_short']), + gap_error=bool(schema.flags['gap_error']), + unaligned_error=bool(schema.flags['unaligned_error']), + delimiter_error=bool(schema.flags['delimiter_error']), + preamble_error=bool(schema.flags['preamble_error']), + symbol_error=bool(schema.flags['symbol_error']), ) return option - def _read_option_if_mac(self, schema: 'Schema_IF_MACAddrOption', *, - options: 'Option') -> 'Data_IF_MACAddrOption': - """Read PCAP-NG ``if_MACaddr`` option. + def _read_option_epb_hash(self, schema: 'Schema_EPB_HashOption', *, + options: 'Option') -> 'Data_EPB_HashOption': + """Read PCAP-NG ``epb_hash`` option. Args: schema: Parsed option schema. @@ -3348,25 +3115,21 @@ def _read_option_if_mac(self, schema: 'Schema_IF_MACAddrOption', *, Parsed option data. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_MACaddr] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Enhanced_Packet_Block: + raise ProtocolError(f'PCAP-NG: [epb_hash] option must be in Enhanced Packet Block, ' f'not {_block_type_name(self._type)}') - if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [if_MACaddr] option must be only one, ' - f'not {self._opt[schema.type] + 1}') - if schema.length != 6: - raise ProtocolError(f'PCAP-NG: [if_MACaddr] invalid length (expected 6, got {schema.length})') - option = Data_IF_MACAddrOption( + option = Data_EPB_HashOption( type=schema.type, length=schema.length, - interface=self._read_mac_addr(schema.interface), + algorithm=schema.func, + hash=schema.data, ) return option - def _read_option_if_eui(self, schema: 'Schema_IF_EUIAddrOption', *, - options: 'Option') -> 'Data_IF_EUIAddrOption': - """Read PCAP-NG ``if_EUIaddr`` option. + def _read_option_epb_dropcount(self, schema: 'Schema_EPB_DropCountOption', *, + options: 'Option') -> 'Data_EPB_DropCountOption': + """Read PCAP-NG ``epb_dropcount`` option. Args: schema: Parsed option schema. @@ -3376,25 +3139,25 @@ def _read_option_if_eui(self, schema: 'Schema_IF_EUIAddrOption', *, Parsed option data. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_EUIaddr] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Enhanced_Packet_Block: + raise ProtocolError(f'PCAP-NG: [epb_dropcount] option must be in Enhanced Packet Block, ' f'not {_block_type_name(self._type)}') if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [if_EUIaddr] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [epb_dropcount] option must be only one, ' f'not {self._opt[schema.type] + 1}') if schema.length != 8: - raise ProtocolError(f'PCAP-NG: [if_EUIaddr] invalid length (expected 8, got {schema.length})') + raise ProtocolError(f'PCAP-NG: [epb_dropcount] invalid length (expected 8, got {schema.length})') - option = Data_IF_EUIAddrOption( + option = Data_EPB_DropCountOption( type=schema.type, length=schema.length, - interface=self._read_mac_addr(schema.interface), + drop_count=schema.drop_count, ) return option - def _read_option_if_speed(self, schema: 'Schema_IF_SpeedOption', *, - options: 'Option') -> 'Data_IF_SpeedOption': - """Read PCAP-NG ``if_speed`` option. + def _read_option_epb_packetid(self, schema: 'Schema_EPB_PacketIDOption', *, + options: 'Option') -> 'Data_EPB_PacketIDOption': + """Read PCAP-NG ``epb_packetid`` option. Args: schema: Parsed option schema. @@ -3404,25 +3167,25 @@ def _read_option_if_speed(self, schema: 'Schema_IF_SpeedOption', *, Parsed option data. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_speed] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Enhanced_Packet_Block: + raise ProtocolError(f'PCAP-NG: [epb_packetid] option must be in Enhanced Packet Block, ' f'not {_block_type_name(self._type)}') if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [if_speed] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [epb_packetid] option must be only one, ' f'not {self._opt[schema.type] + 1}') if schema.length != 8: - raise ProtocolError(f'PCAP-NG: [if_speed] invalid length (expected 8, got {schema.length})') + raise ProtocolError(f'PCAP-NG: [epb_packetid] invalid length (expected 8, got {schema.length})') - option = Data_IF_SpeedOption( + option = Data_EPB_PacketIDOption( type=schema.type, length=schema.length, - speed=schema.speed, + packet_id=schema.packet_id, ) return option - def _read_option_if_tsresol(self, schema: 'Schema_IF_TSResolOption', *, - options: 'Option') -> 'Data_IF_TSResolOption': - """Read PCAP-NG ``if_tsresol`` option. + def _read_option_epb_queue(self, schema: 'Schema_EPB_QueueOption', *, + options: 'Option') -> 'Data_EPB_QueueOption': + """Read PCAP-NG ``epb_queue`` option. Args: schema: Parsed option schema. @@ -3432,25 +3195,25 @@ def _read_option_if_tsresol(self, schema: 'Schema_IF_TSResolOption', *, Parsed option data. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_tsresol] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Enhanced_Packet_Block: + raise ProtocolError(f'PCAP-NG: [epb_queue] option must be in Enhanced Packet Block, ' f'not {_block_type_name(self._type)}') if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [if_tsresol] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [epb_queue] option must be only one, ' f'not {self._opt[schema.type] + 1}') - if schema.length != 1: - raise ProtocolError(f'PCAP-NG: [if_tsresol] invalid length (expected 1, got {schema.length})') + if schema.length != 4: + raise ProtocolError(f'PCAP-NG: [epb_queue] invalid length (expected 4, got {schema.length})') - option = Data_IF_TSResolOption( + option = Data_EPB_QueueOption( type=schema.type, length=schema.length, - resolution=schema.resolution, + queue_id=schema.queue_id, ) return option - def _read_option_if_tzone(self, schema: 'Schema_IF_TZoneOption', *, - options: 'Option') -> 'Data_IF_TZoneOption': - """Read PCAP-NG ``if_tzone`` option. + def _read_option_epb_verdict(self, schema: 'Schema_EPB_VerdictOption', *, + options: 'Option') -> 'Data_EPB_VerdictOption': + """Read PCAP-NG ``epb_verdict`` option. Args: schema: Parsed option schema. @@ -3460,35 +3223,23 @@ def _read_option_if_tzone(self, schema: 'Schema_IF_TZoneOption', *, Parsed option data. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_tzone] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Enhanced_Packet_Block: + raise ProtocolError(f'PCAP-NG: [epb_verdict] option must be in Enhanced Packet Block, ' f'not {_block_type_name(self._type)}') - if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [if_tzone] option must be only one, ' - f'not {self._opt[schema.type] + 1}') - if schema.length != 4: - raise ProtocolError(f'PCAP-NG: [if_tzone] invalid length (expected 4, got {schema.length})') - - offset = datetime.timedelta(seconds=schema.tzone) - try: - timezone = datetime.timezone(offset) # type: datetime.timezone | datetime.timedelta - except ValueError: - # a :class:`~datetime.timezone` holds strictly less than 24 hours; - # the signed 32-bit field holds far more, so keep it as captured - warn(f'PCAP-NG: [if_tzone] offset of {schema.tzone} second(s) is not within 24 hours ' - f'of UTC; kept as captured', ProtocolWarning, stacklevel=stacklevel()) - timezone = offset + if schema.length < 1: + raise ProtocolError(f'PCAP-NG: [epb_verdict] invalid length (expected 1+, got {schema.length})') - option = Data_IF_TZoneOption( + option = Data_EPB_VerdictOption( type=schema.type, length=schema.length, - timezone=timezone, + verdict=schema.verdict, + value=schema.value, ) return option - def _read_option_if_filter(self, schema: 'Schema_IF_FilterOption', *, - options: 'Option') -> 'Data_IF_FilterOption': - """Read PCAP-NG ``if_filter`` option. + def _read_option_ns_dnsname(self, schema: 'Schema_NS_DNSNameOption', *, + options: 'Option') -> 'Data_NS_DNSNameOption': + """Read PCAP-NG ``ns_dnsname`` option. Args: schema: Parsed option schema. @@ -3498,26 +3249,23 @@ def _read_option_if_filter(self, schema: 'Schema_IF_FilterOption', *, Parsed option data. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_filter] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Name_Resolution_Block: + raise ProtocolError(f'PCAP-NG: [ns_dnsname] option must be in Name Resolution Block, ' f'not {_block_type_name(self._type)}') if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [if_filter] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [ns_dnsname] option must be only one, ' f'not {self._opt[schema.type] + 1}') - if schema.length < 1: - raise ProtocolError(f'PCAP-NG: [if_filter] invalid length (expected 1+, got {schema.length})') - option = Data_IF_FilterOption( + option = Data_NS_DNSNameOption( type=schema.type, length=schema.length, - code=schema.code, - expression=schema.filter, + name=schema.name, ) return option - def _read_option_if_os(self, schema: 'Schema_IF_OSOption', *, - options: 'Option') -> 'Data_IF_OSOption': - """Read PCAP-NG ``if_os`` option. + def _read_option_ns_dnsipv4(self, schema: 'Schema_NS_DNSIP4AddrOption', *, + options: 'Option') -> 'Data_NS_DNSIP4AddrOption': + """Read PCAP-NG ``ns_dnsIP4addr`` option. Args: schema: Parsed option schema. @@ -3527,23 +3275,25 @@ def _read_option_if_os(self, schema: 'Schema_IF_OSOption', *, Parsed option data. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_os] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Name_Resolution_Block: + raise ProtocolError(f'PCAP-NG: [ns_dnsIP4addr] option must be in Name Resolution Block, ' f'not {_block_type_name(self._type)}') if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [if_os] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [ns_dnsIP4addr] option must be only one, ' f'not {self._opt[schema.type] + 1}') + if schema.length != 4: + raise ProtocolError(f'PCAP-NG: [ns_dnsIP4addr] invalid length (expected 4, got {schema.length})') - option = Data_IF_OSOption( + option = Data_NS_DNSIP4AddrOption( type=schema.type, length=schema.length, - os=schema.os, + ip=schema.ip, ) return option - def _read_option_if_fcslen(self, schema: 'Schema_IF_FCSLenOption', *, - options: 'Option') -> 'Data_IF_FCSLenOption': - """Read PCAP-NG ``if_fcslen`` option. + def _read_option_ns_dnsipv6(self, schema: 'Schema_NS_DNSIP6AddrOption', *, + options: 'Option') -> 'Data_NS_DNSIP6AddrOption': + """Read PCAP-NG ``ns_dnsIP6addr`` option. Args: schema: Parsed option schema. @@ -3553,25 +3303,25 @@ def _read_option_if_fcslen(self, schema: 'Schema_IF_FCSLenOption', *, Parsed option data. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_fcslen] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Name_Resolution_Block: + raise ProtocolError(f'PCAP-NG: [ns_dnsIP6addr] option must be in Name Resolution Block, ' f'not {_block_type_name(self._type)}') if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [if_fcslen] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [ns_dnsIP6addr] option must be only one, ' f'not {self._opt[schema.type] + 1}') - if schema.length != 1: - raise ProtocolError(f'PCAP-NG: [if_fcslen] invalid length (expected 1, got {schema.length})') + if schema.length != 16: + raise ProtocolError(f'PCAP-NG: [ns_dnsIP6addr] invalid length (expected 16, got {schema.length})') - option = Data_IF_FCSLenOption( + option = Data_NS_DNSIP6AddrOption( type=schema.type, length=schema.length, - fcs_length=schema.fcslen, + ip=schema.ip, ) return option - def _read_option_if_tsoffset(self, schema: 'Schema_IF_TSOffsetOption', *, - options: 'Option') -> 'Data_IF_TSOffsetOption': - """Read PCAP-NG ``if_tsoffset`` option. + def _read_option_isb_starttime(self, schema: 'Schema_ISB_StartTimeOption', *, + options: 'Option') -> 'Data_ISB_StartTimeOption': + """Read PCAP-NG ``isb_starttime`` option. Args: schema: Parsed option schema. @@ -3581,25 +3331,29 @@ def _read_option_if_tsoffset(self, schema: 'Schema_IF_TSOffsetOption', *, Parsed option data. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_tsoffset] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Interface_Statistics_Block: + raise ProtocolError(f'PCAP-NG: [isb_starttime] option must be in Interface Statistics Block, ' f'not {_block_type_name(self._type)}') if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [if_tsoffset] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [isb_starttime] option must be only one, ' f'not {self._opt[schema.type] + 1}') if schema.length != 8: - raise ProtocolError(f'PCAP-NG: [if_tsoffset] invalid length (expected 8, got {schema.length})') + raise ProtocolError(f'PCAP-NG: [isb_starttime] invalid length (expected 8, got {schema.length})') - option = Data_IF_TSOffsetOption( + timestamp, timestamp_epoch = self._read_timestamp(schema.timestamp_high, schema.timestamp_low, + interface_id=self._isb_interface_id) + + option = Data_ISB_StartTimeOption( type=schema.type, length=schema.length, - offset=schema.tsoffset, + timestamp=timestamp, + timestamp_epoch=timestamp_epoch, ) return option - def _read_option_if_hardware(self, schema: 'Schema_IF_HardwareOption', *, - options: 'Option') -> 'Data_IF_HardwareOption': - """Read PCAP-NG ``if_hardware`` option. + def _read_option_isb_endtime(self, schema: 'Schema_ISB_EndTimeOption', *, + options: 'Option') -> 'Data_ISB_EndTimeOption': + """Read PCAP-NG ``isb_endtime`` option. Args: schema: Parsed option schema. @@ -3609,23 +3363,29 @@ def _read_option_if_hardware(self, schema: 'Schema_IF_HardwareOption', *, Parsed option data. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_hardware] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Interface_Statistics_Block: + raise ProtocolError(f'PCAP-NG: [isb_endtime] option must be in Interface Statistics Block, ' f'not {_block_type_name(self._type)}') if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [if_hardware] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [isb_endtime] option must be only one, ' f'not {self._opt[schema.type] + 1}') + if schema.length != 8: + raise ProtocolError(f'PCAP-NG: [isb_endtime] invalid length (expected 8, got {schema.length})') - option = Data_IF_HardwareOption( + timestamp, timestamp_epoch = self._read_timestamp(schema.timestamp_high, schema.timestamp_low, + interface_id=self._isb_interface_id) + + option = Data_ISB_EndTimeOption( type=schema.type, length=schema.length, - hardware=schema.hardware, + timestamp=timestamp, + timestamp_epoch=timestamp_epoch, ) return option - def _read_option_if_txspeed(self, schema: 'Schema_IF_TxSpeedOption', *, - options: 'Option') -> 'Data_IF_TxSpeedOption': - """Read PCAP-NG ``if_txspeed`` option. + def _read_option_isb_ifrecv(self, schema: 'Schema_ISB_IFRecvOption', *, + options: 'Option') -> 'Data_ISB_IFRecvOption': + """Read PCAP-NG ``isb_ifrecv`` option. Args: schema: Parsed option schema. @@ -3635,25 +3395,25 @@ def _read_option_if_txspeed(self, schema: 'Schema_IF_TxSpeedOption', *, Parsed option data. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_txspeed] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Interface_Statistics_Block: + raise ProtocolError(f'PCAP-NG: [isb_ifrecv] option must be in Interface Statistics Block, ' f'not {_block_type_name(self._type)}') if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [if_txspeed] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [isb_ifrecv] option must be only one, ' f'not {self._opt[schema.type] + 1}') if schema.length != 8: - raise ProtocolError(f'PCAP-NG: [if_txspeed] invalid length (expected 8, got {schema.length})') + raise ProtocolError(f'PCAP-NG: [isb_ifrecv] invalid length (expected 8, got {schema.length})') - option = Data_IF_TxSpeedOption( + option = Data_ISB_IFRecvOption( type=schema.type, length=schema.length, - speed=schema.tx_speed, + packets=schema.packets, ) return option - def _read_option_if_rxspeed(self, schema: 'Schema_IF_RxSpeedOption', *, - options: 'Option') -> 'Data_IF_RxSpeedOption': - """Read PCAP-NG ``if_rxspeed`` option. + def _read_option_isb_ifdrop(self, schema: 'Schema_ISB_IFDropOption', *, + options: 'Option') -> 'Data_ISB_IFDropOption': + """Read PCAP-NG ``isb_ifdrop`` option. Args: schema: Parsed option schema. @@ -3663,25 +3423,25 @@ def _read_option_if_rxspeed(self, schema: 'Schema_IF_RxSpeedOption', *, Parsed option data. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_rxspeed] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Interface_Statistics_Block: + raise ProtocolError(f'PCAP-NG: [isb_ifdrop] option must be in Interface Statistics Block, ' f'not {_block_type_name(self._type)}') if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [if_rxspeed] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [isb_ifdrop] option must be only one, ' f'not {self._opt[schema.type] + 1}') if schema.length != 8: - raise ProtocolError(f'PCAP-NG: [if_rxspeed] invalid length (expected 8, got {schema.length})') + raise ProtocolError(f'PCAP-NG: [isb_ifdrop] invalid length (expected 8, got {schema.length})') - option = Data_IF_RxSpeedOption( + option = Data_ISB_IFDropOption( type=schema.type, length=schema.length, - speed=schema.rx_speed, + packets=schema.packets, ) return option - def _read_option_epb_flags(self, schema: 'Schema_EPB_FlagsOption', *, - options: 'Option') -> 'Data_EPB_FlagsOption': - """Read PCAP-NG ``epb_flags`` option. + def _read_option_isb_filteraccept(self, schema: 'Schema_ISB_FilterAcceptOption', *, + options: 'Option') -> 'Data_ISB_FilterAcceptOption': + """Read PCAP-NG ``isb_filteraccept`` option. Args: schema: Parsed option schema. @@ -3691,39 +3451,25 @@ def _read_option_epb_flags(self, schema: 'Schema_EPB_FlagsOption', *, Parsed option data. """ - if self._type != Enum_BlockType.Enhanced_Packet_Block: - raise ProtocolError(f'PCAP-NG: [epb_flags] option must be in Enhanced Packet Block, ' + if self._type != Enum_BlockType.Interface_Statistics_Block: + raise ProtocolError(f'PCAP-NG: [isb_filteraccept] option must be in Interface Statistics Block, ' f'not {_block_type_name(self._type)}') if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [epb_flags] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [isb_filteraccept] option must be only one, ' f'not {self._opt[schema.type] + 1}') - if schema.length != 4: - raise ProtocolError(f'PCAP-NG: [epb_flags] invalid length (expected 4, got {schema.length})') + if schema.length != 8: + raise ProtocolError(f'PCAP-NG: [isb_filteraccept] invalid length (expected 8, got {schema.length})') - option = Data_EPB_FlagsOption( + option = Data_ISB_FilterAcceptOption( type=schema.type, length=schema.length, - direction=PacketDirection(schema.flags['direction']), - reception=PacketReception(schema.flags['reception']), - fcs_len=schema.flags['fcs_len'], - checksum_not_ready=bool(schema.flags['checksum_not_ready']), - checksum_valid=bool(schema.flags['checksum_valid']), - tcp_segmentation_offloaded=bool(schema.flags['tcp_segmentation_offloaded']), - reserved=schema.flags['reserved'], - crc_error=bool(schema.flags['crc_error']), - too_long=bool(schema.flags['too_long']), - too_short=bool(schema.flags['too_short']), - gap_error=bool(schema.flags['gap_error']), - unaligned_error=bool(schema.flags['unaligned_error']), - delimiter_error=bool(schema.flags['delimiter_error']), - preamble_error=bool(schema.flags['preamble_error']), - symbol_error=bool(schema.flags['symbol_error']), + packets=schema.packets, ) return option - def _read_option_epb_hash(self, schema: 'Schema_EPB_HashOption', *, - options: 'Option') -> 'Data_EPB_HashOption': - """Read PCAP-NG ``epb_hash`` option. + def _read_option_isb_osdrop(self, schema: 'Schema_ISB_OSDropOption', *, + options: 'Option') -> 'Data_ISB_OSDropOption': + """Read PCAP-NG ``isb_osdrop`` option. Args: schema: Parsed option schema. @@ -3733,21 +3479,25 @@ def _read_option_epb_hash(self, schema: 'Schema_EPB_HashOption', *, Parsed option data. """ - if self._type != Enum_BlockType.Enhanced_Packet_Block: - raise ProtocolError(f'PCAP-NG: [epb_hash] option must be in Enhanced Packet Block, ' + if self._type != Enum_BlockType.Interface_Statistics_Block: + raise ProtocolError(f'PCAP-NG: [isb_osdrop] option must be in Interface Statistics Block, ' f'not {_block_type_name(self._type)}') + if self._opt[schema.type] > 0: + raise ProtocolError(f'PCAP-NG: [isb_osdrop] option must be only one, ' + f'not {self._opt[schema.type] + 1}') + if schema.length != 8: + raise ProtocolError(f'PCAP-NG: [isb_osdrop] invalid length (expected 8, got {schema.length})') - option = Data_EPB_HashOption( + option = Data_ISB_OSDropOption( type=schema.type, length=schema.length, - algorithm=schema.func, - hash=schema.data, + packets=schema.packets, ) return option - def _read_option_epb_dropcount(self, schema: 'Schema_EPB_DropCountOption', *, - options: 'Option') -> 'Data_EPB_DropCountOption': - """Read PCAP-NG ``epb_dropcount`` option. + def _read_option_isb_usrdeliv(self, schema: 'Schema_ISB_UsrDelivOption', *, + options: 'Option') -> 'Data_ISB_UsrDelivOption': + """Read PCAP-NG ``isb_usrdeliv`` option. Args: schema: Parsed option schema. @@ -3757,25 +3507,25 @@ def _read_option_epb_dropcount(self, schema: 'Schema_EPB_DropCountOption', *, Parsed option data. """ - if self._type != Enum_BlockType.Enhanced_Packet_Block: - raise ProtocolError(f'PCAP-NG: [epb_dropcount] option must be in Enhanced Packet Block, ' + if self._type != Enum_BlockType.Interface_Statistics_Block: + raise ProtocolError(f'PCAP-NG: [isb_usrdeliv] option must be in Interface Statistics Block, ' f'not {_block_type_name(self._type)}') if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [epb_dropcount] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [isb_usrdeliv] option must be only one, ' f'not {self._opt[schema.type] + 1}') if schema.length != 8: - raise ProtocolError(f'PCAP-NG: [epb_dropcount] invalid length (expected 8, got {schema.length})') + raise ProtocolError(f'PCAP-NG: [isb_usrdeliv] invalid length (expected 8, got {schema.length})') - option = Data_EPB_DropCountOption( + option = Data_ISB_UsrDelivOption( type=schema.type, length=schema.length, - drop_count=schema.drop_count, + packets=schema.packets, ) return option - def _read_option_epb_packetid(self, schema: 'Schema_EPB_PacketIDOption', *, - options: 'Option') -> 'Data_EPB_PacketIDOption': - """Read PCAP-NG ``epb_packetid`` option. + def _read_option_pack_flags(self, schema: 'Schema_PACK_FlagsOption', *, + options: 'Option') -> 'Data_PACK_FlagsOption': + """Read PCAP-NG ``pack_flags`` option. Args: schema: Parsed option schema. @@ -3785,25 +3535,39 @@ def _read_option_epb_packetid(self, schema: 'Schema_EPB_PacketIDOption', *, Parsed option data. """ - if self._type != Enum_BlockType.Enhanced_Packet_Block: - raise ProtocolError(f'PCAP-NG: [epb_packetid] option must be in Enhanced Packet Block, ' + if self._type != Enum_BlockType.Packet_Block: + raise ProtocolError(f'PCAP-NG: [pack_flags] option must be in Packet Block, ' f'not {_block_type_name(self._type)}') if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [epb_packetid] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [pack_flags] option must be only one, ' f'not {self._opt[schema.type] + 1}') - if schema.length != 8: - raise ProtocolError(f'PCAP-NG: [epb_packetid] invalid length (expected 8, got {schema.length})') + if schema.length != 4: + raise ProtocolError(f'PCAP-NG: [pack_flags] invalid length (expected 4, got {schema.length})') - option = Data_EPB_PacketIDOption( + option = Data_PACK_FlagsOption( type=schema.type, length=schema.length, - packet_id=schema.packet_id, + direction=PacketDirection(schema.flags['direction']), + reception=PacketReception(schema.flags['reception']), + fcs_len=schema.flags['fcs_len'], + checksum_not_ready=bool(schema.flags['checksum_not_ready']), + checksum_valid=bool(schema.flags['checksum_valid']), + tcp_segmentation_offloaded=bool(schema.flags['tcp_segmentation_offloaded']), + reserved=schema.flags['reserved'], + crc_error=bool(schema.flags['crc_error']), + too_long=bool(schema.flags['too_long']), + too_short=bool(schema.flags['too_short']), + gap_error=bool(schema.flags['gap_error']), + unaligned_error=bool(schema.flags['unaligned_error']), + delimiter_error=bool(schema.flags['delimiter_error']), + preamble_error=bool(schema.flags['preamble_error']), + symbol_error=bool(schema.flags['symbol_error']), ) return option - def _read_option_epb_queue(self, schema: 'Schema_EPB_QueueOption', *, - options: 'Option') -> 'Data_EPB_QueueOption': - """Read PCAP-NG ``epb_queue`` option. + def _read_option_pack_hash(self, schema: 'Schema_PACK_HashOption', *, + options: 'Option') -> 'Data_PACK_HashOption': + """Read PCAP-NG ``pack_hash`` option. Args: schema: Parsed option schema. @@ -3813,691 +3577,796 @@ def _read_option_epb_queue(self, schema: 'Schema_EPB_QueueOption', *, Parsed option data. """ - if self._type != Enum_BlockType.Enhanced_Packet_Block: - raise ProtocolError(f'PCAP-NG: [epb_queue] option must be in Enhanced Packet Block, ' + if self._type != Enum_BlockType.Packet_Block: + raise ProtocolError(f'PCAP-NG: [pack_hash] option must be in Packet Block, ' f'not {_block_type_name(self._type)}') - if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [epb_queue] option must be only one, ' - f'not {self._opt[schema.type] + 1}') - if schema.length != 4: - raise ProtocolError(f'PCAP-NG: [epb_queue] invalid length (expected 4, got {schema.length})') - option = Data_EPB_QueueOption( + option = Data_PACK_HashOption( type=schema.type, length=schema.length, - queue_id=schema.queue_id, + algorithm=schema.func, + hash=schema.data, ) return option - def _read_option_epb_verdict(self, schema: 'Schema_EPB_VerdictOption', *, - options: 'Option') -> 'Data_EPB_VerdictOption': - """Read PCAP-NG ``epb_verdict`` option. + def _read_record_unknown(self, schema: 'Schema_UnknownRecord', *, + records: 'Record') -> 'Data_UnknownRecord': + """Read PCAP-NG unknown name resolution records. Args: - schema: Parsed option schema. - options: Parsed PCAP-NG options. + schema: Parsed name resolution record schema. + records: Parsed PCAP-NG records. Returns: - Parsed option data. + Parsed name resolution record data. """ - if self._type != Enum_BlockType.Enhanced_Packet_Block: - raise ProtocolError(f'PCAP-NG: [epb_verdict] option must be in Enhanced Packet Block, ' - f'not {_block_type_name(self._type)}') - if schema.length < 1: - raise ProtocolError(f'PCAP-NG: [epb_verdict] invalid length (expected 1+, got {schema.length})') - - option = Data_EPB_VerdictOption( + record = Data_UnknownRecord( type=schema.type, length=schema.length, - verdict=schema.verdict, - value=schema.value, + data=schema.data, ) - return option + return record - def _read_option_ns_dnsname(self, schema: 'Schema_NS_DNSNameOption', *, - options: 'Option') -> 'Data_NS_DNSNameOption': - """Read PCAP-NG ``ns_dnsname`` option. + def _read_record_end(self, schema: 'Schema_EndRecord', *, + records: 'Record') -> 'Data_EndRecord': + """Read PCAP-NG ``nrb_record_end`` name resolution records. Args: - schema: Parsed option schema. - options: Parsed PCAP-NG options. + schema: Parsed name resolution record schema. + records: Parsed PCAP-NG records. Returns: - Parsed option data. + Parsed name resolution record data. """ - if self._type != Enum_BlockType.Name_Resolution_Block: - raise ProtocolError(f'PCAP-NG: [ns_dnsname] option must be in Name Resolution Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [ns_dnsname] option must be only one, ' - f'not {self._opt[schema.type] + 1}') + if schema.length != 0: + raise ProtocolError(f'PCAP-NG: [nrb_record_end] invalid length (expected 0, got {schema.length})') - option = Data_NS_DNSNameOption( + record = Data_EndRecord( type=schema.type, length=schema.length, - name=schema.name, ) - return option + return record - def _read_option_ns_dnsipv4(self, schema: 'Schema_NS_DNSIP4AddrOption', *, - options: 'Option') -> 'Data_NS_DNSIP4AddrOption': - """Read PCAP-NG ``ns_dnsIP4addr`` option. + def _read_record_ipv4(self, schema: 'Schema_IPv4Record', *, + records: 'Record') -> 'Data_IPv4Record': + """Read PCAP-NG ``nrb_record_ipv4`` name resolution records. Args: - schema: Parsed option schema. - options: Parsed PCAP-NG options. + schema: Parsed name resolution record schema. + records: Parsed PCAP-NG records. Returns: - Parsed option data. + Parsed name resolution record data. """ - if self._type != Enum_BlockType.Name_Resolution_Block: - raise ProtocolError(f'PCAP-NG: [ns_dnsIP4addr] option must be in Name Resolution Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [ns_dnsIP4addr] option must be only one, ' - f'not {self._opt[schema.type] + 1}') - if schema.length != 4: - raise ProtocolError(f'PCAP-NG: [ns_dnsIP4addr] invalid length (expected 4, got {schema.length})') - - option = Data_NS_DNSIP4AddrOption( + record = Data_IPv4Record( type=schema.type, length=schema.length, ip=schema.ip, + records=tuple(schema.names), ) - return option + return record - def _read_option_ns_dnsipv6(self, schema: 'Schema_NS_DNSIP6AddrOption', *, - options: 'Option') -> 'Data_NS_DNSIP6AddrOption': - """Read PCAP-NG ``ns_dnsIP6addr`` option. + def _read_record_ipv6(self, schema: 'Schema_IPv6Record', *, + records: 'Record') -> 'Data_IPv6Record': + """Read PCAP-NG ``nrb_record_ipv6`` name resolution records. Args: - schema: Parsed option schema. - options: Parsed PCAP-NG options. + schema: Parsed name resolution record schema. + records: Parsed PCAP-NG records. Returns: - Parsed option data. + Parsed name resolution record data. """ - if self._type != Enum_BlockType.Name_Resolution_Block: - raise ProtocolError(f'PCAP-NG: [ns_dnsIP6addr] option must be in Name Resolution Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [ns_dnsIP6addr] option must be only one, ' - f'not {self._opt[schema.type] + 1}') - if schema.length != 16: - raise ProtocolError(f'PCAP-NG: [ns_dnsIP6addr] invalid length (expected 16, got {schema.length})') - - option = Data_NS_DNSIP6AddrOption( + record = Data_IPv6Record( type=schema.type, length=schema.length, ip=schema.ip, + records=tuple(schema.names), ) - return option + return record - def _read_option_isb_starttime(self, schema: 'Schema_ISB_StartTimeOption', *, - options: 'Option') -> 'Data_ISB_StartTimeOption': - """Read PCAP-NG ``isb_starttime`` option. + def _read_secrets_unknown(self, schema: 'Schema_UnknownSecrets', *, + block: 'Schema_DecryptionSecretsBlock') -> 'Data_UnknownSecrets': + """Read PCAP-NG unknown secrets. Args: - schema: Parsed option schema. - options: Parsed PCAP-NG options. + schema: Parsed secret schema. + block: Parsed PCAP-NG decryption secrets block. Returns: - Parsed option data. + Parsed decryption secrets data. """ - if self._type != Enum_BlockType.Interface_Statistics_Block: - raise ProtocolError(f'PCAP-NG: [isb_starttime] option must be in Interface Statistics Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [isb_starttime] option must be only one, ' - f'not {self._opt[schema.type] + 1}') - if schema.length != 8: - raise ProtocolError(f'PCAP-NG: [isb_starttime] invalid length (expected 8, got {schema.length})') + secrets = Data_UnknownSecrets( + data=schema.data, + ) + return secrets - timestamp, timestamp_epoch = self._read_timestamp(schema.timestamp_high, schema.timestamp_low, - interface_id=self._isb_interface_id) + def _read_secrets_tls(self, schema: 'Schema_TLSKeyLog', *, + block: 'Schema_DecryptionSecretsBlock') -> 'Data_TLSKeyLog': + """Read PCAP-NG TLS key log secrets. - option = Data_ISB_StartTimeOption( - type=schema.type, - length=schema.length, - timestamp=timestamp, - timestamp_epoch=timestamp_epoch, + Args: + schema: Parsed secret schema. + block: Parsed PCAP-NG decryption secrets block. + + Returns: + Parsed decryption secrets data. + + """ + secrets = Data_TLSKeyLog( + # NOTE: each label's entries become an OrderedMultiInfo as the model + # stores them (#1484, #2726). + entries=Data_TLSKeyLogEntries(schema.entries), ) - return option + # NOTE: the log text is kept as well, since :attr:`entries` holds + # neither its comments nor the order of its lines, and a rebuild writes + # it back verbatim. + secrets.__update__(data=schema.data) + return secrets - def _read_option_isb_endtime(self, schema: 'Schema_ISB_EndTimeOption', *, - options: 'Option') -> 'Data_ISB_EndTimeOption': - """Read PCAP-NG ``isb_endtime`` option. + def _read_secrets_wireguard(self, schema: 'Schema_WireGuardKeyLog', *, + block: 'Schema_DecryptionSecretsBlock') -> 'Data_WireGuardKeyLog': + """Read PCAP-NG WireGuard key log secrets. Args: - schema: Parsed option schema. - options: Parsed PCAP-NG options. + schema: Parsed secret schema. + block: Parsed PCAP-NG decryption secrets block. Returns: - Parsed option data. + Parsed decryption secrets data. """ - if self._type != Enum_BlockType.Interface_Statistics_Block: - raise ProtocolError(f'PCAP-NG: [isb_endtime] option must be in Interface Statistics Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [isb_endtime] option must be only one, ' - f'not {self._opt[schema.type] + 1}') - if schema.length != 8: - raise ProtocolError(f'PCAP-NG: [isb_endtime] invalid length (expected 8, got {schema.length})') + secrets = Data_WireGuardKeyLog( + entries=schema.entries, + ) + # NOTE: the log text is kept as well, since :attr:`entries` holds + # neither its comments nor the order of its lines, and a rebuild writes + # it back verbatim. + secrets.__update__(data=schema.data) + return secrets - timestamp, timestamp_epoch = self._read_timestamp(schema.timestamp_high, schema.timestamp_low, - interface_id=self._isb_interface_id) + def _read_secrets_zigbee_aps(self, schema: 'Schema_ZigBeeAPSKey', *, + block: 'Schema_DecryptionSecretsBlock') -> 'Data_ZigBeeAPSKey': + """Read PCAP-NG ZigBee APS Key secrets. - option = Data_ISB_EndTimeOption( - type=schema.type, - length=schema.length, - timestamp=timestamp, - timestamp_epoch=timestamp_epoch, - ) - return option + Structure of ZigBee APS Key secrets: - def _read_option_isb_ifrecv(self, schema: 'Schema_ISB_IFRecvOption', *, - options: 'Option') -> 'Data_ISB_IFRecvOption': - """Read PCAP-NG ``isb_ifrecv`` option. + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +---------------------------------------------------------------+ + 0 | Block Type = 0x0000000A | + +---------------------------------------------------------------+ + 4 | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 8 | Secrets Type = 0x5a415053 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 12 | Secrets Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 16 | AES-128 | + | APS Key | + | (16 octets) | + | (128 bits) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 32 | PAN ID | Low Node Short Address | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 36 | High Node Short Address | padding (0) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 40 / / + / Options (variable) / + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / Block Total Length / + +---------------------------------------------------------------+ Args: - schema: Parsed option schema. - options: Parsed PCAP-NG options. + schema: Parsed secret schema. + block: Parsed PCAP-NG decryption secrets block. Returns: - Parsed option data. + Parsed decryption secrets data. """ - if self._type != Enum_BlockType.Interface_Statistics_Block: - raise ProtocolError(f'PCAP-NG: [isb_ifrecv] option must be in Interface Statistics Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [isb_ifrecv] option must be only one, ' - f'not {self._opt[schema.type] + 1}') - if schema.length != 8: - raise ProtocolError(f'PCAP-NG: [isb_ifrecv] invalid length (expected 8, got {schema.length})') - - option = Data_ISB_IFRecvOption( - type=schema.type, - length=schema.length, - packets=schema.packets, + secrets = Data_ZigBeeAPSKey( + aps_key=schema.key, + pan_id=schema.panid, + short_address=schema.addr_high << 16 | schema.addr_low, ) - return option + # NOTE: kept as for ``_read_secrets_zigbee_nwk`` (#1986). + secrets.__update__(data=bytes(schema)) + return secrets - def _read_option_isb_ifdrop(self, schema: 'Schema_ISB_IFDropOption', *, - options: 'Option') -> 'Data_ISB_IFDropOption': - """Read PCAP-NG ``isb_ifdrop`` option. + def _read_secrets_zigbee_nwk(self, schema: 'Schema_ZigBeeNWKKey', *, + block: 'Schema_DecryptionSecretsBlock') -> 'Data_ZigBeeNWKKey': + """Read PCAP-NG ZigBee NWK Key secrets. + + Structure of ZigBee NWK Key secrets: + + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +---------------------------------------------------------------+ + 0 | Block Type = 0x0000000A | + +---------------------------------------------------------------+ + 4 | Block Total Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 8 | Secrets Type = 0x5a4e574b | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 12 | Secrets Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 16 | AES-128 | + | NWK Key | + | (16 octets) | + | (128 bits) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 32 | PAN ID | padding (0) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + 36 / / + / Options (variable) / + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / Block Total Length / + +---------------------------------------------------------------+ Args: - schema: Parsed option schema. - options: Parsed PCAP-NG options. + schema: Parsed secret schema. + block: Parsed PCAP-NG decryption secrets block. Returns: - Parsed option data. + Parsed decryption secrets data. """ - if self._type != Enum_BlockType.Interface_Statistics_Block: - raise ProtocolError(f'PCAP-NG: [isb_ifdrop] option must be in Interface Statistics Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [isb_ifdrop] option must be only one, ' - f'not {self._opt[schema.type] + 1}') - if schema.length != 8: - raise ProtocolError(f'PCAP-NG: [isb_ifdrop] invalid length (expected 8, got {schema.length})') - - option = Data_ISB_IFDropOption( - type=schema.type, - length=schema.length, - packets=schema.packets, + secrets = Data_ZigBeeNWKKey( + nwk_key=schema.key, + pan_id=schema.panid, ) - return option + # NOTE: the octets as the block holds them are kept as well, since a + # Secrets Length may declare fewer or more than the key's fixed width, + # and a rebuild writes them back verbatim (#1986). + secrets.__update__(data=bytes(schema)) + return secrets - def _read_option_isb_filteraccept(self, schema: 'Schema_ISB_FilterAcceptOption', *, - options: 'Option') -> 'Data_ISB_FilterAcceptOption': - """Read PCAP-NG ``isb_filteraccept`` option. + def _make_block_unknown(self, block: 'Optional[Data_UnknownBlock]' = None, *, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_UnknownBlock': + """Make unknown PCAP-NG block. Args: - schema: Parsed option schema. - options: Parsed PCAP-NG options. + block: Block data model. + data: Unspecified block data. + **kwargs: Arbitrary keyword arguments. Returns: - Parsed option data. + Constructed block schema. """ - if self._type != Enum_BlockType.Interface_Statistics_Block: - raise ProtocolError(f'PCAP-NG: [isb_filteraccept] option must be in Interface Statistics Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [isb_filteraccept] option must be only one, ' - f'not {self._opt[schema.type] + 1}') - if schema.length != 8: - raise ProtocolError(f'PCAP-NG: [isb_filteraccept] invalid length (expected 8, got {schema.length})') + if block is not None: + data = block.body - option = Data_ISB_FilterAcceptOption( - type=schema.type, - length=schema.length, - packets=schema.packets, + # NOTE: The body is padded to 32 bits, and Block Total Length counts it + # together with the 12 octets of block type and both length fields. + data += bytes(pad_length(len(data), 4)) + length = len(data) + GENERIC_BLOCK_MINIMUM + + return Schema_UnknownBlock( + length=length, + body=data, + length2=length, ) - return option - def _read_option_isb_osdrop(self, schema: 'Schema_ISB_OSDropOption', *, - options: 'Option') -> 'Data_ISB_OSDropOption': - """Read PCAP-NG ``isb_osdrop`` option. + def _make_block_idb(self, block: 'Optional[Data_InterfaceDescriptionBlock]' = None, *, + linktype: 'Enum_LinkType | int' = Enum_LinkType.NULL, + snaplen: 'int' = 0, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long + **kwargs: 'Any') -> 'Schema_InterfaceDescriptionBlock': + """Make PCAP-NG interface description block (IDB). Args: - schema: Parsed option schema. - options: Parsed PCAP-NG options. + block: Block data model. + linktype: Link layer protocol type. + snaplen: Snap length, an unsigned 32-bit value; 0, the default, + means no limit. + options: Block options. + **kwargs: Arbitrary keyword arguments. Returns: - Parsed option data. + Constructed block schema. - """ - if self._type != Enum_BlockType.Interface_Statistics_Block: - raise ProtocolError(f'PCAP-NG: [isb_osdrop] option must be in Interface Statistics Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [isb_osdrop] option must be only one, ' - f'not {self._opt[schema.type] + 1}') - if schema.length != 8: - raise ProtocolError(f'PCAP-NG: [isb_osdrop] invalid length (expected 8, got {schema.length})') + Raises: + ProtocolError: If ``linktype`` is a name, a :obj:`bool`, or a + member or code the 16-bit LinkType field cannot carry; or if + ``snaplen`` is not an :obj:`int` (a :obj:`bool` included), or + is outside ``0`` to ``0xFFFFFFFF``, which the 32-bit SnapLen + field would truncate (:issue:`2278`). - option = Data_ISB_OSDropOption( - type=schema.type, - length=schema.length, - packets=schema.packets, - ) - return option + .. versionchanged:: 1.5.0 - def _read_option_isb_usrdeliv(self, schema: 'Schema_ISB_UsrDelivOption', *, - options: 'Option') -> 'Data_ISB_UsrDelivOption': - """Read PCAP-NG ``isb_usrdeliv`` option. + ``snaplen``'s default changed from ``0xFFFF_FFFF_FFFF_FFFF`` -- which + the 32-bit field silently truncated to ``0xFFFFFFFF`` -- to ``0``, + and a value outside the field's range now raises instead of being + truncated (:issue:`2278`). - Args: - schema: Parsed option schema. - options: Parsed PCAP-NG options. + .. versionchanged:: 1.6.0 - Returns: - Parsed option data. + ``snaplen`` is checked by + :meth:`~pcapkit.protocols.protocol.Protocol._make_uint`, so a + :obj:`bool` or a non-integer raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError` too + (:issue:`2614`). """ - if self._type != Enum_BlockType.Interface_Statistics_Block: - raise ProtocolError(f'PCAP-NG: [isb_usrdeliv] option must be in Interface Statistics Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [isb_usrdeliv] option must be only one, ' - f'not {self._opt[schema.type] + 1}') - if schema.length != 8: - raise ProtocolError(f'PCAP-NG: [isb_usrdeliv] invalid length (expected 8, got {schema.length})') + if block is not None and hasattr(block, 'block_raw'): + return self._make_block_short(block) # type: ignore[return-value] + if block is not None: + linktype_val = block.linktype + snaplen = block.snaplen + options = self._block_area(block, 'options') + else: + linktype_val = self._make_index(linktype, Enum_LinkType, bits=16, name='[IDB] linktype', + alias='PCAP-NG') + snaplen = self._make_uint(snaplen, bits=32, name='[IDB] snaplen', alias='PCAP-NG') - option = Data_ISB_UsrDelivOption( - type=schema.type, - length=schema.length, - packets=schema.packets, - ) - return option + if options is not None: + options_value, total_length = self._make_pcapng_options(options, namespace='if') + else: + options_value, total_length = [], 0 + total_length += self._make_options_trailer(block, options_value) - def _read_option_pack_flags(self, schema: 'Schema_PACK_FlagsOption', *, - options: 'Option') -> 'Data_PACK_FlagsOption': - """Read PCAP-NG ``pack_flags`` option. + return self._make_padding(Schema_InterfaceDescriptionBlock( + length=total_length + BLOCK_MINIMUM[Enum_BlockType.Interface_Description_Block], + linktype=linktype_val, + snaplen=snaplen, + options=options_value, + length2=total_length + BLOCK_MINIMUM[Enum_BlockType.Interface_Description_Block], + ), block, 'reserved') + + def _make_block_packet(self, block: 'Optional[Data_PacketBlock]' = None, *, + interface_id: 'int' = 0, + drop_count: 'int' = 0, + timestamp: 'Optional[dt_type | int | float | Decimal]' = None, + captured_len: 'Optional[int]' = None, + original_len: 'Optional[int]' = None, + packet_data: 'bytes | ProtocolBase | Schema' = b'', + options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long + **kwargs: 'Any') -> 'Schema_PacketBlock': + """Make PCAP-NG packet block (obsolete). Args: - schema: Parsed option schema. - options: Parsed PCAP-NG options. + block: Block data model. + interface_id: Interface ID. + drop_count: Drops count, ``0xFFFF`` for "not available". + timestamp: Packet timestamp. + captured_len: Captured length. + original_len: Original length. + packet_data: Payload of the block. + options: Block options. + **kwargs: Arbitrary keyword arguments. Returns: - Parsed option data. - - """ - if self._type != Enum_BlockType.Packet_Block: - raise ProtocolError(f'PCAP-NG: [pack_flags] option must be in Packet Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[schema.type] > 0: - raise ProtocolError(f'PCAP-NG: [pack_flags] option must be only one, ' - f'not {self._opt[schema.type] + 1}') - if schema.length != 4: - raise ProtocolError(f'PCAP-NG: [pack_flags] invalid length (expected 4, got {schema.length})') - - option = Data_PACK_FlagsOption( - type=schema.type, - length=schema.length, - direction=PacketDirection(schema.flags['direction']), - reception=PacketReception(schema.flags['reception']), - fcs_len=schema.flags['fcs_len'], - checksum_not_ready=bool(schema.flags['checksum_not_ready']), - checksum_valid=bool(schema.flags['checksum_valid']), - tcp_segmentation_offloaded=bool(schema.flags['tcp_segmentation_offloaded']), - reserved=schema.flags['reserved'], - crc_error=bool(schema.flags['crc_error']), - too_long=bool(schema.flags['too_long']), - too_short=bool(schema.flags['too_short']), - gap_error=bool(schema.flags['gap_error']), - unaligned_error=bool(schema.flags['unaligned_error']), - delimiter_error=bool(schema.flags['delimiter_error']), - preamble_error=bool(schema.flags['preamble_error']), - symbol_error=bool(schema.flags['symbol_error']), - ) - return option + Constructed block schema. - def _read_option_pack_hash(self, schema: 'Schema_PACK_HashOption', *, - options: 'Option') -> 'Data_PACK_HashOption': - """Read PCAP-NG ``pack_hash`` option. + Raises: + ProtocolError: If ``drop_count`` is not an :obj:`int` (a + :obj:`bool` included), or is outside ``0`` to ``0xFFFF``. - Args: - schema: Parsed option schema. - options: Parsed PCAP-NG options. + .. versionchanged:: 1.6.0 - Returns: - Parsed option data. + ``drop_count`` is checked by + :meth:`~pcapkit.protocols.protocol.Protocol._make_uint`, so a value + the 16-bit field cannot hold, ``-1`` included, or a :obj:`bool` + raises :exc:`~pcapkit.utilities.exceptions.ProtocolError` rather + than being written masked (:issue:`2614`). """ - if self._type != Enum_BlockType.Packet_Block: - raise ProtocolError(f'PCAP-NG: [pack_hash] option must be in Packet Block, ' - f'not {_block_type_name(self._type)}') + warn('PCAP-NG: Packet Block has been obsolete! Please use Enhanced Packet Block and/or ' + 'Simple Packet Block instead.', DeprecatedFormatWarning, stacklevel=stacklevel()) - option = Data_PACK_HashOption( - type=schema.type, - length=schema.length, - algorithm=schema.func, - hash=schema.data, - ) - return option + if block is not None and hasattr(block, 'block_raw'): + return self._make_block_short(block) # type: ignore[return-value] + if block is not None: + interface_id = block.interface_id + drop_count = block.drop_count + timestamp = block.timestamp_epoch + captured_len = block.captured_len + original_len = block.original_len + packet_data = self._make_packet_data(block) + options = self._block_area(block, 'options') + drop_count = self._make_uint(drop_count, bits=16, name='[PB] drop_count', alias='PCAP-NG') - def _read_nrb_records(self, records_schema: 'list[Schema_NameResolutionRecord]') -> 'Record': - """Read PCAP-NG name resolution records. + timestamp_high, timestamp_low = self._make_timestamp(timestamp, interface_id=interface_id) + if captured_len is None: + if self._ctx is None: + snaplen = 0xFFFF_FFFF_FFFF_FFFF + else: + # a snaplen of zero means no limit + snaplen = self._get_interface(interface_id).snaplen or 0xFFFF_FFFF_FFFF_FFFF + captured_len = min(len(packet_data), snaplen) + if original_len is None: + original_len = len(packet_data) + # NOTE: Packet Data holds exactly Captured Packet Length octets, so a + # packet longer than that -- e.g. than the interface's snaplen -- is cut + # to it rather than written out whole behind a clipped length. + if len(packet_data) > captured_len: + packet_data = bytes(packet_data)[:captured_len] + packet_len = len(packet_data) + pad_length(len(packet_data), 4) - Args: - records_schema: Parsed name resolution records. + if options is not None: + options_value, total_length = self._make_pcapng_options(options, namespace='pack') + else: + options_value, total_length = [], 0 + total_length += self._make_options_trailer(block, options_value) - Returns: - Parsed PCAP-NG name resolution records data. + return self._make_padding(Schema_PacketBlock( + length=total_length + BLOCK_MINIMUM[Enum_BlockType.Packet_Block] + packet_len, + interface_id=interface_id, + drop_count=drop_count, + timestamp_high=timestamp_high, + timestamp_low=timestamp_low, + captured_length=captured_len, + original_length=original_len, + packet_data=packet_data, + options=options_value, + length2=total_length + BLOCK_MINIMUM[Enum_BlockType.Packet_Block] + packet_len, + ), block, 'padding_data') - """ - records = OrderedMultiDict() # type: Record + def _make_block_spb(self, block: 'Optional[Data_SimplePacketBlock]' = None, *, + original_len: 'Optional[int]' = None, + packet_data: 'bytes | ProtocolBase | Schema' = b'', + **kwargs: 'Any') -> 'Schema_SimplePacketBlock': + """Make PCAP-NG simple packet block (SPB). - # an area kept as captured holds no records to read - if any(isinstance(schema, bytes) for schema in records_schema): - return records + Args: + block: Block data model. + original_len: Original length. + packet_data: Payload of the block. + **kwargs: Arbitrary keyword arguments. - for schema in records_schema: - type = schema.type - name = self._lookup_registry(self.__record__, type) + Returns: + Constructed block schema. - if isinstance(name, str): - meth_name = f'_read_record_{name}' - meth = cast('RecordParser', - getattr(self, meth_name, self._read_record_unknown)) + """ + if block is not None: + original_len = block.original_len + packet_data = self._make_packet_data(block) + # NOTE: The data model's ``captured_len`` already bounds the data, + # including a block that kept octets past the snaplen (#1384). + captured_len = block.captured_len + else: + if original_len is None: + original_len = len(packet_data) + # NOTE: An SPB always belongs to interface 0, and its Packet Data is + # ``min(original_len, snaplen)`` octets long -- the length the parser + # reads back -- so a longer packet is cut to it. + if self._ctx is None or not self._ctx.interfaces: + snaplen = 0xFFFF_FFFF_FFFF_FFFF else: - meth = name[0] - data = meth(schema, records=records) - # NOTE: The padding to 32 bits, if it is not all zeros (#1437). - self._read_padding(schema, data) - - # record name resolution record data - records.add(type, data) - - # break when ``nrb_record_end`` is reached - if type == Enum_RecordType.nrb_record_end: - break + snaplen = self._ctx.interfaces[0].snaplen or 0xFFFF_FFFF_FFFF_FFFF + captured_len = min(original_len, snaplen) + if len(packet_data) > captured_len: + packet_data = bytes(packet_data)[:captured_len] + packet_len = len(packet_data) + pad_length(len(packet_data), 4) - return records + return self._make_padding(Schema_SimplePacketBlock( + length=packet_len + BLOCK_MINIMUM[Enum_BlockType.Simple_Packet_Block], + original_len=original_len, + packet_data=packet_data, + length2=packet_len + BLOCK_MINIMUM[Enum_BlockType.Simple_Packet_Block], + ), block, 'padding', 'padding_data') - def _read_record_unknown(self, schema: 'Schema_UnknownRecord', *, - records: 'Record') -> 'Data_UnknownRecord': - """Read PCAP-NG unknown name resolution records. + def _make_block_nrb(self, block: 'Optional[Data_NameResolutionBlock]' = None, *, + records: 'Optional[Record | list[Schema_NameResolutionRecord | tuple[Enum_RecordType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long + options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long + **kwargs: 'Any') -> 'Schema_NameResolutionBlock': + """Make PCAP-NG name resolution block (NRB). Args: - schema: Parsed name resolution record schema. - records: Parsed PCAP-NG records. + block: Block data model. + records: Name resolution records. + options: Block options. + **kwargs: Arbitrary keyword arguments. Returns: - Parsed name resolution record data. + Constructed block schema. """ - record = Data_UnknownRecord( - type=schema.type, - length=schema.length, - data=schema.data, + if block is not None: + records = self._block_area(block, 'records') + options = self._block_area(block, 'options') + + if records is not None: + records_value, records_length = self._make_nrb_records(records) + else: + records_value, records_length = [], 0 + + if options is not None: + options_value, options_length = self._make_pcapng_options(options, namespace='ns') + else: + options_value, options_length = [], 0 + options_length += self._make_options_trailer(block, options_value) + + return Schema_NameResolutionBlock( + length=options_length + records_length + GENERIC_BLOCK_MINIMUM, + records=records_value, + options=options_value, + length2=options_length + records_length + GENERIC_BLOCK_MINIMUM, ) - return record - def _read_record_end(self, schema: 'Schema_EndRecord', *, - records: 'Record') -> 'Data_EndRecord': - """Read PCAP-NG ``nrb_record_end`` name resolution records. + def _make_block_isb(self, block: 'Optional[Data_InterfaceStatisticsBlock]' = None, *, + interface_id: 'int' = 0, + timestamp: 'Optional[dt_type | int | float | Decimal]' = None, + options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long + **kwargs: 'Any') -> 'Schema_InterfaceStatisticsBlock': + """Make PCAP-NG interface statistics block (ISB). Args: - schema: Parsed name resolution record schema. - records: Parsed PCAP-NG records. + block: Block data model. + interface_id: Interface ID. + timestamp: Block timestamp. + options: Block options. + **kwargs: Arbitrary keyword arguments. Returns: - Parsed name resolution record data. + Constructed block schema. """ - if schema.length != 0: - raise ProtocolError(f'PCAP-NG: [nrb_record_end] invalid length (expected 0, got {schema.length})') + if block is not None: + interface_id = block.interface_id + timestamp = block.timestamp_epoch + options = self._block_area(block, 'options') - record = Data_EndRecord( - type=schema.type, - length=schema.length, + timestamp_high, timestamp_low = self._make_timestamp(timestamp, interface_id=interface_id) + # NOTE: ``isb_starttime`` and ``isb_endtime`` are scaled by the + # resolution of this block's interface, as on the parse path. + self._isb_interface_id = interface_id # pylint: disable=attribute-defined-outside-init + + if options is not None: + options_value, total_length = self._make_pcapng_options(options, namespace='isb') + else: + options_value, total_length = [], 0 + total_length += self._make_options_trailer(block, options_value) + + return Schema_InterfaceStatisticsBlock( + length=total_length + BLOCK_MINIMUM[Enum_BlockType.Interface_Statistics_Block], + interface_id=interface_id, + timestamp_high=timestamp_high, + timestamp_low=timestamp_low, + options=options_value, + length2=total_length + BLOCK_MINIMUM[Enum_BlockType.Interface_Statistics_Block], ) - return record - def _read_record_ipv4(self, schema: 'Schema_IPv4Record', *, - records: 'Record') -> 'Data_IPv4Record': - """Read PCAP-NG ``nrb_record_ipv4`` name resolution records. + def _make_block_epb(self, block: 'Optional[Data_EnhancedPacketBlock]' = None, *, + interface_id: 'int' = 0, + timestamp: 'Optional[dt_type | int | float | Decimal]' = None, + captured_len: 'Optional[int]' = None, + original_len: 'Optional[int]' = None, + packet_data: 'bytes | ProtocolBase | Schema' = b'', + options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long + **kwargs: 'Any') -> 'Schema_EnhancedPacketBlock': + """Make PCAP-NG enhanced packet block (EPB). Args: - schema: Parsed name resolution record schema. - records: Parsed PCAP-NG records. + block: Block data model. + interface_id: Interface ID. + timestamp: Packet timestamp. + captured_len: Captured length. + original_len: Original length. + packet_data: Payload of the block. + options: Block options. + **kwargs: Arbitrary keyword arguments. Returns: - Parsed name resolution record data. + Constructed block schema. """ - record = Data_IPv4Record( - type=schema.type, - length=schema.length, - ip=schema.ip, - records=tuple(schema.names), - ) - return record - - def _read_record_ipv6(self, schema: 'Schema_IPv6Record', *, - records: 'Record') -> 'Data_IPv6Record': - """Read PCAP-NG ``nrb_record_ipv6`` name resolution records. + if block is not None and hasattr(block, 'block_raw'): + return self._make_block_short(block) # type: ignore[return-value] + if block is not None: + interface_id = block.interface_id + timestamp = block.timestamp_epoch + captured_len = block.captured_len + original_len = block.original_len + packet_data = self._make_packet_data(block) + options = self._block_area(block, 'options') - Args: - schema: Parsed name resolution record schema. - records: Parsed PCAP-NG records. + timestamp_high, timestamp_low = self._make_timestamp(timestamp, interface_id=interface_id) + if captured_len is None: + if self._ctx is None: + snaplen = 0xFFFF_FFFF_FFFF_FFFF + else: + # a snaplen of zero means no limit + snaplen = self._get_interface(interface_id).snaplen or 0xFFFF_FFFF_FFFF_FFFF + captured_len = min(len(packet_data), snaplen) + if original_len is None: + original_len = len(packet_data) + # NOTE: Packet Data holds exactly Captured Packet Length octets, so a + # packet longer than that -- e.g. than the interface's snaplen -- is cut + # to it rather than written out whole behind a clipped length. + if len(packet_data) > captured_len: + packet_data = bytes(packet_data)[:captured_len] + packet_len = len(packet_data) + pad_length(len(packet_data), 4) - Returns: - Parsed name resolution record data. + if options is not None: + options_value, total_length = self._make_pcapng_options(options, namespace='epb') + else: + options_value, total_length = [], 0 + total_length += self._make_options_trailer(block, options_value) - """ - record = Data_IPv6Record( - type=schema.type, - length=schema.length, - ip=schema.ip, - records=tuple(schema.names), - ) - return record + return self._make_padding(Schema_EnhancedPacketBlock( + length=total_length + BLOCK_MINIMUM[Enum_BlockType.Enhanced_Packet_Block] + packet_len, + interface_id=interface_id, + timestamp_high=timestamp_high, + timestamp_low=timestamp_low, + captured_len=captured_len, + original_len=original_len, + packet_data=packet_data, + options=options_value, + length2=total_length + BLOCK_MINIMUM[Enum_BlockType.Enhanced_Packet_Block] + packet_len, + ), block, 'padding_data') - def _read_secrets_unknown(self, schema: 'Schema_UnknownSecrets', *, - block: 'Schema_DecryptionSecretsBlock') -> 'Data_UnknownSecrets': - """Read PCAP-NG unknown secrets. + def _make_block_systemd(self, block: 'Optional[Data_SystemdJournalExportBlock]' = None, *, + entries: 'Optional[list[OrderedMultiDict[str, str | bytes]] | bytes]' = None, + **kwargs: 'Any') -> 'Schema_SystemdJournalExportBlock': + """Make PCAP-NG :manpage:`systemd(1)` journal export block. Args: - schema: Parsed secret schema. - block: Parsed PCAP-NG decryption secrets block. + block: Block data model. + entries: :manpage:`systemd(1)` journal export entries. + **kwargs: Arbitrary keyword arguments. Returns: - Parsed decryption secrets data. + Constructed block schema. """ - secrets = Data_UnknownSecrets( - data=schema.data, - ) - return secrets - - def _read_secrets_tls(self, schema: 'Schema_TLSKeyLog', *, - block: 'Schema_DecryptionSecretsBlock') -> 'Data_TLSKeyLog': - """Read PCAP-NG TLS key log secrets. + self._byte = sys.byteorder - Args: - schema: Parsed secret schema. - block: Parsed PCAP-NG decryption secrets block. + if block is not None: + entries = cast('list[OrderedMultiDict[str, str | bytes]] | bytes', + getattr(block, 'entry_raw', block.data)) - Returns: - Parsed decryption secrets data. + if entries is None: + data = b'' + elif isinstance(entries, bytes): + data = entries + else: + data = Schema_SystemdJournalExportBlock.dump_entries(entries) + # NOTE: The entry MUST be zero-padded to 32 bits, and the Block Total + # Length counts that padding, as the ``entry`` field reads it back. + data += bytes(pad_length(len(data), 4)) - """ - secrets = Data_TLSKeyLog( - # NOTE: each label's entries become an OrderedMultiInfo as the model - # stores them (#1484, #2726). - entries=Data_TLSKeyLogEntries(schema.entries), + return Schema_SystemdJournalExportBlock( + length=len(data) + GENERIC_BLOCK_MINIMUM, + entry=data, + length2=len(data) + GENERIC_BLOCK_MINIMUM, ) - # NOTE: the log text is kept as well, since :attr:`entries` holds - # neither its comments nor the order of its lines, and a rebuild writes - # it back verbatim. - secrets.__update__(data=schema.data) - return secrets - def _read_secrets_wireguard(self, schema: 'Schema_WireGuardKeyLog', *, - block: 'Schema_DecryptionSecretsBlock') -> 'Data_WireGuardKeyLog': - """Read PCAP-NG WireGuard key log secrets. + def _make_block_dsb(self, block: 'Optional[Data_DecryptionSecretsBlock]' = None, *, + secrets_type: 'Enum_SecretsType | int' = Enum_SecretsType.TLS_Key_Log, + secrets_data: 'Schema_DSBSecrets | Data_DSBSecrets | bytes | dict[str, Any]' = b'', + options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long + **kwargs: 'Any') -> 'Schema_DecryptionSecretsBlock': + """Make PCAP-NG decryption secrets block (DSB). Args: - schema: Parsed secret schema. - block: Parsed PCAP-NG decryption secrets block. + block: Block data model. + secrets_type: Decryption secrets type. + secrets_data: Decryption secrets data. Raw :obj:`bytes` are read as + the Secrets Data of a parsed block are, through the schema + registered for ``secrets_type`` (:issue:`2273`). + options: Block options. + **kwargs: Arbitrary keyword arguments. Returns: - Parsed decryption secrets data. - - """ - secrets = Data_WireGuardKeyLog( - entries=schema.entries, - ) - # NOTE: the log text is kept as well, since :attr:`entries` holds - # neither its comments nor the order of its lines, and a rebuild writes - # it back verbatim. - secrets.__update__(data=schema.data) - return secrets - - def _read_secrets_zigbee_nwk(self, schema: 'Schema_ZigBeeNWKKey', *, - block: 'Schema_DecryptionSecretsBlock') -> 'Data_ZigBeeNWKKey': - """Read PCAP-NG ZigBee NWK Key secrets. - - Structure of ZigBee NWK Key secrets: - - .. code-block:: text - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +---------------------------------------------------------------+ - 0 | Block Type = 0x0000000A | - +---------------------------------------------------------------+ - 4 | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 8 | Secrets Type = 0x5a4e574b | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 12 | Secrets Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 16 | AES-128 | - | NWK Key | - | (16 octets) | - | (128 bits) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 32 | PAN ID | padding (0) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 36 / / - / Options (variable) / - / / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - / Block Total Length / - +---------------------------------------------------------------+ - - Args: - schema: Parsed secret schema. - block: Parsed PCAP-NG decryption secrets block. + Constructed block schema. - Returns: - Parsed decryption secrets data. + Raises: + ProtocolError: If ``secrets_type`` is a name, a :obj:`bool`, or a + member or code the 32-bit Secrets Type field cannot carry; or + if ``secrets_data`` is none of the forms this maker reads -- + raw :obj:`bytes`, a :obj:`dict` of secrets arguments, a + :class:`~pcapkit.protocols.misc.pcapng.data.DSBSecrets` data + model or a :class:`~pcapkit.protocols.schema.Schema`. """ - secrets = Data_ZigBeeNWKKey( - nwk_key=schema.key, - pan_id=schema.panid, - ) - # NOTE: the octets as the block holds them are kept as well, since a - # Secrets Length may declare fewer or more than the key's fixed width, - # and a rebuild writes them back verbatim (#1986). - secrets.__update__(data=bytes(schema)) - return secrets - - def _read_secrets_zigbee_aps(self, schema: 'Schema_ZigBeeAPSKey', *, - block: 'Schema_DecryptionSecretsBlock') -> 'Data_ZigBeeAPSKey': - """Read PCAP-NG ZigBee APS Key secrets. + declared = None # type: Optional[int] + if block is not None: + secrets_type_val = block.secrets_type + secrets_data = block.secrets_data + declared = block.secrets_length + options = self._block_area(block, 'options') + else: + secrets_type_val = self._make_index(secrets_type, Enum_SecretsType, bits=32, + name='[DSB] secrets_type', alias='PCAP-NG') - Structure of ZigBee APS Key secrets: + if isinstance(secrets_data, bytes): + from pcapkit.protocols.misc.pcapng.schema import dsb_secrets_selector - .. code-block:: text + # NOTE: Read through the schema the parse side selects, as the + # block's Secrets Data is read, since :meth:`read` consumes a + # secrets schema rather than raw octets; the schema packs back to + # exactly ``secrets_data`` (#2273). + secrets_data_val = cast('Schema_DSBSecrets', dsb_secrets_selector({ + 'secrets_type': secrets_type_val, + 'secrets_length': len(secrets_data), + 'length': len(secrets_data) + BLOCK_MINIMUM[Enum_BlockType.Decryption_Secrets_Block], + }).unpack(secrets_data, {})) + elif isinstance(secrets_data, (dict, Data_DSBSecrets)): + name = self._lookup_registry(self.__secrets__, secrets_type_val) + if isinstance(name, str): + meth_name = f'_make_secrets_{name}' + meth = cast('SecretsConstructor', + getattr(self, meth_name, self._make_secrets_unknown)) + else: + meth = name[1] - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +---------------------------------------------------------------+ - 0 | Block Type = 0x0000000A | - +---------------------------------------------------------------+ - 4 | Block Total Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 8 | Secrets Type = 0x5a415053 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 12 | Secrets Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 16 | AES-128 | - | APS Key | - | (16 octets) | - | (128 bits) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 32 | PAN ID | Low Node Short Address | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 36 | High Node Short Address | padding (0) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - 40 / / - / Options (variable) / - / / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - / Block Total Length / - +---------------------------------------------------------------+ + if isinstance(secrets_data, dict): + secrets_data_val = meth(secrets_type_val, **secrets_data) + else: + secrets_data_val = meth(secrets_type_val, secrets_data) + elif isinstance(secrets_data, Schema): + secrets_data_val = secrets_data + else: + raise ProtocolError(f'PCAP-NG: [DSB] invalid secrets_data: must be bytes, a dict, ' + f'a DSBSecrets data model or a Schema, not {secrets_data!r}') + # NOTE: Secrets Length excludes the padding to the next 32-bit boundary, + # which ``padding_data`` writes and the block length has to include. + if block is not None and isinstance(secrets_data_val, Schema): + # NOTE: Packed in the context of the block it was parsed from, so + # that a key log the block cut is still read as cut (#1980). + secrets_held = len(secrets_data_val.pack({'__packet__': { + 'length': block.length, 'secrets_length': block.secrets_length, + }})) + else: + secrets_held = len(secrets_data_val) + secrets_padded = secrets_held + pad_length(secrets_held, 4) + # NOTE: A Secrets Length running past the block is kept as declared, as + # an EPB keeps its captured length, so the block rebuilds as captured; + # the block length is still sized from the secrets written (#1980). + # Only a true overrun is kept -- the one the parse side bounds and + # warns on, measured by ``secrets_area`` -- so a length that fits the + # block, or one an edit has outdated, follows the secrets written. + from pcapkit.protocols.misc.pcapng.schema import secrets_area - Args: - schema: Parsed secret schema. - block: Parsed PCAP-NG decryption secrets block. + if block is not None and declared is not None and declared > secrets_area({ + 'length': block.length, 'secrets_length': declared, + }): + secrets_length = declared + else: + secrets_length = secrets_held - Returns: - Parsed decryption secrets data. + if options is not None: + options_value, total_length = self._make_pcapng_options(options, namespace='dsb') + else: + options_value, total_length = [], 0 + total_length += self._make_options_trailer(block, options_value) - """ - secrets = Data_ZigBeeAPSKey( - aps_key=schema.key, - pan_id=schema.panid, - short_address=schema.addr_high << 16 | schema.addr_low, - ) - # NOTE: kept as for ``_read_secrets_zigbee_nwk`` (#1986). - secrets.__update__(data=bytes(schema)) - return secrets + return self._make_padding(Schema_DecryptionSecretsBlock( + length=total_length + secrets_padded + BLOCK_MINIMUM[Enum_BlockType.Decryption_Secrets_Block], + secrets_type=secrets_type_val, + secrets_length=secrets_length, + secrets_data=secrets_data_val, + options=options_value, + length2=total_length + secrets_padded + BLOCK_MINIMUM[Enum_BlockType.Decryption_Secrets_Block], + ), block, 'padding_data') - def _make_block_unknown(self, block: 'Optional[Data_UnknownBlock]' = None, *, - data: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_UnknownBlock': - """Make unknown PCAP-NG block. + def _make_block_cb(self, block: 'Optional[Data_CustomBlock]' = None, *, + pen: 'int' = 0, + data: 'Schema | bytes' = b'', + options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long + **kwargs: 'Any') -> 'Schema_CustomBlock': + """Make PCAP-NG custom block (CB). Args: block: Block data model. - data: Unspecified block data. + pen: Private enterprise number. + data: Custom data. + options: Block options. **kwargs: Arbitrary keyword arguments. Returns: @@ -4505,17 +4374,36 @@ def _make_block_unknown(self, block: 'Optional[Data_UnknownBlock]' = None, *, """ if block is not None: - data = block.body + pen = block.pen + data = block.data + options = cast('Option', getattr(block, 'options', None)) - # NOTE: The body is padded to 32 bits, and Block Total Length counts it - # together with the 12 octets of block type and both length fields. - data += bytes(pad_length(len(data), 4)) - length = len(data) + GENERIC_BLOCK_MINIMUM + # NOTE: Unlike every other block that carries options, a custom block + # has no ``OptionField`` to name an option registry: + # :class:`~pcapkit.protocols.misc.pcapng.schema.CustomBlock` + # spans the custom data, its padding and the options as one opaque blob, + # since only the owner of the private enterprise number knows where the + # custom data ends. Its options are therefore resolved in the generic + # ``opt_*`` space, which is the only one valid in every block. + if options is not None: + options_value, _ = self._make_pcapng_options(options, namespace='opt') + else: + options_value, _ = [], 0 - return Schema_UnknownBlock( - length=length, - body=data, - length2=length, + cb_data = data.pack() if isinstance(data, Schema) else data + cb_data += bytes(pad_length(len(cb_data), 4)) + for option in options_value: + cb_data += option.pack() if isinstance(option, Schema) else option + cb_data += bytes(pad_length(len(cb_data), 4)) + + # NOTE: both custom block types span the same fixed fields, and this + # method makes either, so the one entry stands for both. + minimum = BLOCK_MINIMUM[Enum_BlockType.Custom_Block_that_rewriters_can_copy_into_new_files] + return Schema_CustomBlock( + length=len(cb_data) + minimum, + pen=pen, + data=cb_data, + length2=len(cb_data) + minimum, ) def _make_block_shb(self, block: 'Optional[Data_SectionHeaderBlock]' = None, *, @@ -4586,1343 +4474,1275 @@ def _make_block_shb(self, block: 'Optional[Data_SectionHeaderBlock]' = None, *, options_value, total_length = [], 0 total_length += self._make_options_trailer(block, options_value) - return Schema_SectionHeaderBlock( - length=total_length + BLOCK_MINIMUM[Enum_BlockType.Section_Header_Block], - magic=BYTE_ORDER_MAGIC, - major=major_version, - minor=minor_version, - section_length=section_length, - options=options_value, - length2=total_length + BLOCK_MINIMUM[Enum_BlockType.Section_Header_Block], + return Schema_SectionHeaderBlock( + length=total_length + BLOCK_MINIMUM[Enum_BlockType.Section_Header_Block], + magic=BYTE_ORDER_MAGIC, + major=major_version, + minor=minor_version, + section_length=section_length, + options=options_value, + length2=total_length + BLOCK_MINIMUM[Enum_BlockType.Section_Header_Block], + ) + + def _make_option_unknown(self, type: 'Enum_OptionType', option: 'Optional[Data_UnknownOption]' = None, *, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_UnknownOption': + """Make unknown PCAP-NG option. + + Args: + type: Option type. + option: Option data model. + data: Unspecified option data. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + data = option.data + + length = len(data) + + return Schema_UnknownOption( + type=type, + length=length, + data=data, + ) + + def _make_option_endofopt(self, type: 'Enum_OptionType', option: 'Optional[Data_EndOfOption]' = None, + **kwargs: 'Any') -> 'Schema_EndOfOption': + """Make PCAP-NG ``opt_endofopt`` option. + + Args: + type: Option type. + option: Option data model. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if self._opt[type] > 0: + raise ProtocolError(f'PCAP-NG: [opt_endofopt] option must be only one, ' + f'not {self._opt[type] + 1}') + + return Schema_EndOfOption( + type=type, + length=0, + ) + + def _make_option_comment(self, type: 'Enum_OptionType', option: 'Optional[Data_CommentOption]' = None, *, + comment: 'str' = '', + **kwargs: 'Any') -> 'Schema_CommentOption': + """Make PCAP-NG ``opt_comment`` option. + + Args: + type: Option type. + option: Option data model. + comment: Comment text. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed option schema. + + """ + if option is not None: + comment = option.comment + + return Schema_CommentOption( + type=type, + length=_utf8_length(comment), + comment=comment, ) - def _make_block_idb(self, block: 'Optional[Data_InterfaceDescriptionBlock]' = None, *, - linktype: 'Enum_LinkType | int' = Enum_LinkType.NULL, - snaplen: 'int' = 0, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long - **kwargs: 'Any') -> 'Schema_InterfaceDescriptionBlock': - """Make PCAP-NG interface description block (IDB). + def _make_option_custom(self, type: 'Enum_OptionType', option: 'Optional[Data_CustomOption]' = None, *, + pen: 'int' = 0xFFFFFFFF, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_CustomOption': + """Make PCAP-NG ``opt_custom`` option. Args: - block: Block data model. - linktype: Link layer protocol type. - snaplen: Snap length, an unsigned 32-bit value; 0, the default, - means no limit. - options: Block options. + type: Option type. + option: Option data model. + pen: Private enterprise number. + data: Custom data. **kwargs: Arbitrary keyword arguments. Returns: - Constructed block schema. + Constructed option schema. - Raises: - ProtocolError: If ``linktype`` is a name, a :obj:`bool`, or a - member or code the 16-bit LinkType field cannot carry; or if - ``snaplen`` is not an :obj:`int` (a :obj:`bool` included), or - is outside ``0`` to ``0xFFFFFFFF``, which the 32-bit SnapLen - field would truncate (:issue:`2278`). + """ + if option is not None: + pen = option.pen + data = option.data - .. versionchanged:: 1.5.0 + return Schema_CustomOption( + type=type, + length=len(data) + 4, + pen=pen, + data=data + ) - ``snaplen``'s default changed from ``0xFFFF_FFFF_FFFF_FFFF`` -- which - the 32-bit field silently truncated to ``0xFFFFFFFF`` -- to ``0``, - and a value outside the field's range now raises instead of being - truncated (:issue:`2278`). + def _make_option_if_name(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_NameOption]' = None, *, + name: 'str' = '', + **kwargs: 'Any') -> 'Schema_IF_NameOption': + """Make PCAP-NG ``if_name`` option. - .. versionchanged:: 1.6.0 + Args: + type: Option type. + option: Option data model. + name: Interface name. + **kwargs: Arbitrary keyword arguments. - ``snaplen`` is checked by - :meth:`~pcapkit.protocols.protocol.Protocol._make_uint`, so a - :obj:`bool` or a non-integer raises - :exc:`~pcapkit.utilities.exceptions.ProtocolError` too - (:issue:`2614`). + Returns: + Constructed option schema. """ - if block is not None and hasattr(block, 'block_raw'): - return self._make_block_short(block) # type: ignore[return-value] - if block is not None: - linktype_val = block.linktype - snaplen = block.snaplen - options = self._block_area(block, 'options') - else: - linktype_val = self._make_index(linktype, Enum_LinkType, bits=16, name='[IDB] linktype', - alias='PCAP-NG') - snaplen = self._make_uint(snaplen, bits=32, name='[IDB] snaplen', alias='PCAP-NG') + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_name] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[type] > 0: + raise ProtocolError(f'PCAP-NG: [if_name] option must be only one, ' + f'not {self._opt[type] + 1}') - if options is not None: - options_value, total_length = self._make_pcapng_options(options, namespace='if') - else: - options_value, total_length = [], 0 - total_length += self._make_options_trailer(block, options_value) + if option is not None: + name = option.name - return self._make_padding(Schema_InterfaceDescriptionBlock( - length=total_length + BLOCK_MINIMUM[Enum_BlockType.Interface_Description_Block], - linktype=linktype_val, - snaplen=snaplen, - options=options_value, - length2=total_length + BLOCK_MINIMUM[Enum_BlockType.Interface_Description_Block], - ), block, 'reserved') + return Schema_IF_NameOption( + type=type, + length=_utf8_length(name), + name=name, + ) - def _make_block_epb(self, block: 'Optional[Data_EnhancedPacketBlock]' = None, *, - interface_id: 'int' = 0, - timestamp: 'Optional[dt_type | int | float | Decimal]' = None, - captured_len: 'Optional[int]' = None, - original_len: 'Optional[int]' = None, - packet_data: 'bytes | ProtocolBase | Schema' = b'', - options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long - **kwargs: 'Any') -> 'Schema_EnhancedPacketBlock': - """Make PCAP-NG enhanced packet block (EPB). + def _make_option_if_description(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_DescriptionOption]' = None, *, + description: 'str' = '', + **kwargs: 'Any') -> 'Schema_IF_DescriptionOption': + """Make PCAP-NG ``if_description`` option. Args: - block: Block data model. - interface_id: Interface ID. - timestamp: Packet timestamp. - captured_len: Captured length. - original_len: Original length. - packet_data: Payload of the block. - options: Block options. + type: Option type. + option: Option data model. + description: Interface description. **kwargs: Arbitrary keyword arguments. Returns: - Constructed block schema. + Constructed option schema. """ - if block is not None and hasattr(block, 'block_raw'): - return self._make_block_short(block) # type: ignore[return-value] - if block is not None: - interface_id = block.interface_id - timestamp = block.timestamp_epoch - captured_len = block.captured_len - original_len = block.original_len - packet_data = self._make_packet_data(block) - options = self._block_area(block, 'options') - - timestamp_high, timestamp_low = self._make_timestamp(timestamp, interface_id=interface_id) - if captured_len is None: - if self._ctx is None: - snaplen = 0xFFFF_FFFF_FFFF_FFFF - else: - # a snaplen of zero means no limit - snaplen = self._get_interface(interface_id).snaplen or 0xFFFF_FFFF_FFFF_FFFF - captured_len = min(len(packet_data), snaplen) - if original_len is None: - original_len = len(packet_data) - # NOTE: Packet Data holds exactly Captured Packet Length octets, so a - # packet longer than that -- e.g. than the interface's snaplen -- is cut - # to it rather than written out whole behind a clipped length. - if len(packet_data) > captured_len: - packet_data = bytes(packet_data)[:captured_len] - packet_len = len(packet_data) + pad_length(len(packet_data), 4) + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_description] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[type] > 0: + raise ProtocolError(f'PCAP-NG: [if_description] option must be only one, ' + f'not {self._opt[type] + 1}') - if options is not None: - options_value, total_length = self._make_pcapng_options(options, namespace='epb') - else: - options_value, total_length = [], 0 - total_length += self._make_options_trailer(block, options_value) + if option is not None: + description = option.description - return self._make_padding(Schema_EnhancedPacketBlock( - length=total_length + BLOCK_MINIMUM[Enum_BlockType.Enhanced_Packet_Block] + packet_len, - interface_id=interface_id, - timestamp_high=timestamp_high, - timestamp_low=timestamp_low, - captured_len=captured_len, - original_len=original_len, - packet_data=packet_data, - options=options_value, - length2=total_length + BLOCK_MINIMUM[Enum_BlockType.Enhanced_Packet_Block] + packet_len, - ), block, 'padding_data') + return Schema_IF_DescriptionOption( + type=type, + length=_utf8_length(description), + description=description, + ) - def _make_block_spb(self, block: 'Optional[Data_SimplePacketBlock]' = None, *, - original_len: 'Optional[int]' = None, - packet_data: 'bytes | ProtocolBase | Schema' = b'', - **kwargs: 'Any') -> 'Schema_SimplePacketBlock': - """Make PCAP-NG simple packet block (SPB). + def _make_option_if_ipv4(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_IPv4AddrOption]' = None, *, + interface: 'IPv4Interface | str' = '192.168.1.1/255.255.255.0', + **kwargs: 'Any') -> 'Schema_IF_IPv4AddrOption': + """Make PCAP-NG ``if_IPv4addr`` option. Args: - block: Block data model. - original_len: Original length. - packet_data: Payload of the block. + type: Option type. + option: Option data model. + interface: IPv4 interface. **kwargs: Arbitrary keyword arguments. Returns: - Constructed block schema. + Constructed option schema. """ - if block is not None: - original_len = block.original_len - packet_data = self._make_packet_data(block) - # NOTE: The data model's ``captured_len`` already bounds the data, - # including a block that kept octets past the snaplen (#1384). - captured_len = block.captured_len - else: - if original_len is None: - original_len = len(packet_data) - # NOTE: An SPB always belongs to interface 0, and its Packet Data is - # ``min(original_len, snaplen)`` octets long -- the length the parser - # reads back -- so a longer packet is cut to it. - if self._ctx is None or not self._ctx.interfaces: - snaplen = 0xFFFF_FFFF_FFFF_FFFF - else: - snaplen = self._ctx.interfaces[0].snaplen or 0xFFFF_FFFF_FFFF_FFFF - captured_len = min(original_len, snaplen) - if len(packet_data) > captured_len: - packet_data = bytes(packet_data)[:captured_len] - packet_len = len(packet_data) + pad_length(len(packet_data), 4) + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_IPv4addr] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') - return self._make_padding(Schema_SimplePacketBlock( - length=packet_len + BLOCK_MINIMUM[Enum_BlockType.Simple_Packet_Block], - original_len=original_len, - packet_data=packet_data, - length2=packet_len + BLOCK_MINIMUM[Enum_BlockType.Simple_Packet_Block], - ), block, 'padding', 'padding_data') + if option is not None: + interface = option.interface - def _make_block_nrb(self, block: 'Optional[Data_NameResolutionBlock]' = None, *, - records: 'Optional[Record | list[Schema_NameResolutionRecord | tuple[Enum_RecordType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long - options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long - **kwargs: 'Any') -> 'Schema_NameResolutionBlock': - """Make PCAP-NG name resolution block (NRB). + return Schema_IF_IPv4AddrOption( + type=type, + length=8, + interface=interface, + ) + + def _make_option_if_ipv6(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_IPv6AddrOption]' = None, *, + interface: 'IPv6Interface | str' = '2001:0db8:85a3:08d3:1319:8a2e:0370:7344/64', + **kwargs: 'Any') -> 'Schema_IF_IPv6AddrOption': + """Make PCAP-NG ``if_IPv6addr`` option. Args: - block: Block data model. - records: Name resolution records. - options: Block options. + type: Option type. + option: Option data model. + interface: IPv6 interface. **kwargs: Arbitrary keyword arguments. Returns: - Constructed block schema. + Constructed option schema. """ - if block is not None: - records = self._block_area(block, 'records') - options = self._block_area(block, 'options') - - if records is not None: - records_value, records_length = self._make_nrb_records(records) - else: - records_value, records_length = [], 0 + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_IPv6addr] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') - if options is not None: - options_value, options_length = self._make_pcapng_options(options, namespace='ns') - else: - options_value, options_length = [], 0 - options_length += self._make_options_trailer(block, options_value) + if option is not None: + interface = option.interface - return Schema_NameResolutionBlock( - length=options_length + records_length + GENERIC_BLOCK_MINIMUM, - records=records_value, - options=options_value, - length2=options_length + records_length + GENERIC_BLOCK_MINIMUM, + return Schema_IF_IPv6AddrOption( + type=type, + length=17, + interface=interface, ) - def _make_block_isb(self, block: 'Optional[Data_InterfaceStatisticsBlock]' = None, *, - interface_id: 'int' = 0, - timestamp: 'Optional[dt_type | int | float | Decimal]' = None, - options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long - **kwargs: 'Any') -> 'Schema_InterfaceStatisticsBlock': - """Make PCAP-NG interface statistics block (ISB). + def _make_option_if_mac(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_MACAddrOption]' = None, *, + interface: 'str | bytes | bytearray' = '00:01:02:03:04:05', + **kwargs: 'Any') -> 'Schema_IF_MACAddrOption': + """Make PCAP-NG ``if_MACaddr`` option. Args: - block: Block data model. - interface_id: Interface ID. - timestamp: Block timestamp. - options: Block options. + type: Option type. + option: Option data model. + interface: MAC interface address. **kwargs: Arbitrary keyword arguments. Returns: - Constructed block schema. + Constructed option schema. """ - if block is not None: - interface_id = block.interface_id - timestamp = block.timestamp_epoch - options = self._block_area(block, 'options') - - timestamp_high, timestamp_low = self._make_timestamp(timestamp, interface_id=interface_id) - # NOTE: ``isb_starttime`` and ``isb_endtime`` are scaled by the - # resolution of this block's interface, as on the parse path. - self._isb_interface_id = interface_id # pylint: disable=attribute-defined-outside-init + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_MACaddr] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[type] > 0: + raise ProtocolError(f'PCAP-NG: [if_MACaddr] option must be only one, ' + f'not {self._opt[type] + 1}') - if options is not None: - options_value, total_length = self._make_pcapng_options(options, namespace='isb') - else: - options_value, total_length = [], 0 - total_length += self._make_options_trailer(block, options_value) + if option is not None: + interface = option.interface - return Schema_InterfaceStatisticsBlock( - length=total_length + BLOCK_MINIMUM[Enum_BlockType.Interface_Statistics_Block], - interface_id=interface_id, - timestamp_high=timestamp_high, - timestamp_low=timestamp_low, - options=options_value, - length2=total_length + BLOCK_MINIMUM[Enum_BlockType.Interface_Statistics_Block], + return Schema_IF_MACAddrOption( + type=type, + length=6, + interface=self._make_mac_addr(interface), ) - def _make_block_systemd(self, block: 'Optional[Data_SystemdJournalExportBlock]' = None, *, - entries: 'Optional[list[OrderedMultiDict[str, str | bytes]] | bytes]' = None, - **kwargs: 'Any') -> 'Schema_SystemdJournalExportBlock': - """Make PCAP-NG :manpage:`systemd(1)` journal export block. + def _make_option_if_eui(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_EUIAddrOption]' = None, *, + interface: 'str | bytes | bytearray' = '02:34:56:FF:FE:78:9A:BC', + **kwargs: 'Any') -> 'Schema_IF_EUIAddrOption': + """Make PCAP-NG ``if_EUIaddr`` option. Args: - block: Block data model. - entries: :manpage:`systemd(1)` journal export entries. + type: Option type. + option: Option data model. + interface: Hardware EUI address. **kwargs: Arbitrary keyword arguments. Returns: - Constructed block schema. + Constructed option schema. """ - self._byte = sys.byteorder - - if block is not None: - entries = cast('list[OrderedMultiDict[str, str | bytes]] | bytes', - getattr(block, 'entry_raw', block.data)) + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_EUIaddr] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[type] > 0: + raise ProtocolError(f'PCAP-NG: [if_EUIaddr] option must be only one, ' + f'not {self._opt[type] + 1}') - if entries is None: - data = b'' - elif isinstance(entries, bytes): - data = entries - else: - data = Schema_SystemdJournalExportBlock.dump_entries(entries) - # NOTE: The entry MUST be zero-padded to 32 bits, and the Block Total - # Length counts that padding, as the ``entry`` field reads it back. - data += bytes(pad_length(len(data), 4)) + if option is not None: + interface = option.interface - return Schema_SystemdJournalExportBlock( - length=len(data) + GENERIC_BLOCK_MINIMUM, - entry=data, - length2=len(data) + GENERIC_BLOCK_MINIMUM, + return Schema_IF_EUIAddrOption( + type=type, + length=8, + interface=self._make_mac_addr(interface, size=8, name='EUI address'), ) - def _make_block_dsb(self, block: 'Optional[Data_DecryptionSecretsBlock]' = None, *, - secrets_type: 'Enum_SecretsType | int' = Enum_SecretsType.TLS_Key_Log, - secrets_data: 'Schema_DSBSecrets | Data_DSBSecrets | bytes | dict[str, Any]' = b'', - options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long - **kwargs: 'Any') -> 'Schema_DecryptionSecretsBlock': - """Make PCAP-NG decryption secrets block (DSB). + def _make_option_if_speed(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_SpeedOption]' = None, *, + speed: 'int' = 100000000, + **kwargs: 'Any') -> 'Schema_IF_SpeedOption': + """Make PCAP-NG ``if_speed`` option. Args: - block: Block data model. - secrets_type: Decryption secrets type. - secrets_data: Decryption secrets data. Raw :obj:`bytes` are read as - the Secrets Data of a parsed block are, through the schema - registered for ``secrets_type`` (:issue:`2273`). - options: Block options. + type: Option type. + option: Option data model. + speed: Interface speed, in bits per second. **kwargs: Arbitrary keyword arguments. Returns: - Constructed block schema. - - Raises: - ProtocolError: If ``secrets_type`` is a name, a :obj:`bool`, or a - member or code the 32-bit Secrets Type field cannot carry; or - if ``secrets_data`` is none of the forms this maker reads -- - raw :obj:`bytes`, a :obj:`dict` of secrets arguments, a - :class:`~pcapkit.protocols.misc.pcapng.data.DSBSecrets` data - model or a :class:`~pcapkit.protocols.schema.Schema`. + Constructed option schema. """ - declared = None # type: Optional[int] - if block is not None: - secrets_type_val = block.secrets_type - secrets_data = block.secrets_data - declared = block.secrets_length - options = self._block_area(block, 'options') - else: - secrets_type_val = self._make_index(secrets_type, Enum_SecretsType, bits=32, - name='[DSB] secrets_type', alias='PCAP-NG') - - if isinstance(secrets_data, bytes): - from pcapkit.protocols.misc.pcapng.schema import dsb_secrets_selector - - # NOTE: Read through the schema the parse side selects, as the - # block's Secrets Data is read, since :meth:`read` consumes a - # secrets schema rather than raw octets; the schema packs back to - # exactly ``secrets_data`` (#2273). - secrets_data_val = cast('Schema_DSBSecrets', dsb_secrets_selector({ - 'secrets_type': secrets_type_val, - 'secrets_length': len(secrets_data), - 'length': len(secrets_data) + BLOCK_MINIMUM[Enum_BlockType.Decryption_Secrets_Block], - }).unpack(secrets_data, {})) - elif isinstance(secrets_data, (dict, Data_DSBSecrets)): - name = self._lookup_registry(self.__secrets__, secrets_type_val) - if isinstance(name, str): - meth_name = f'_make_secrets_{name}' - meth = cast('SecretsConstructor', - getattr(self, meth_name, self._make_secrets_unknown)) - else: - meth = name[1] - - if isinstance(secrets_data, dict): - secrets_data_val = meth(secrets_type_val, **secrets_data) - else: - secrets_data_val = meth(secrets_type_val, secrets_data) - elif isinstance(secrets_data, Schema): - secrets_data_val = secrets_data - else: - raise ProtocolError(f'PCAP-NG: [DSB] invalid secrets_data: must be bytes, a dict, ' - f'a DSBSecrets data model or a Schema, not {secrets_data!r}') - # NOTE: Secrets Length excludes the padding to the next 32-bit boundary, - # which ``padding_data`` writes and the block length has to include. - if block is not None and isinstance(secrets_data_val, Schema): - # NOTE: Packed in the context of the block it was parsed from, so - # that a key log the block cut is still read as cut (#1980). - secrets_held = len(secrets_data_val.pack({'__packet__': { - 'length': block.length, 'secrets_length': block.secrets_length, - }})) - else: - secrets_held = len(secrets_data_val) - secrets_padded = secrets_held + pad_length(secrets_held, 4) - # NOTE: A Secrets Length running past the block is kept as declared, as - # an EPB keeps its captured length, so the block rebuilds as captured; - # the block length is still sized from the secrets written (#1980). - # Only a true overrun is kept -- the one the parse side bounds and - # warns on, measured by ``secrets_area`` -- so a length that fits the - # block, or one an edit has outdated, follows the secrets written. - from pcapkit.protocols.misc.pcapng.schema import secrets_area - - if block is not None and declared is not None and declared > secrets_area({ - 'length': block.length, 'secrets_length': declared, - }): - secrets_length = declared - else: - secrets_length = secrets_held + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_speed] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[type] > 0: + raise ProtocolError(f'PCAP-NG: [if_speed] option must be only one, ' + f'not {self._opt[type] + 1}') - if options is not None: - options_value, total_length = self._make_pcapng_options(options, namespace='dsb') - else: - options_value, total_length = [], 0 - total_length += self._make_options_trailer(block, options_value) + if option is not None: + speed = option.speed - return self._make_padding(Schema_DecryptionSecretsBlock( - length=total_length + secrets_padded + BLOCK_MINIMUM[Enum_BlockType.Decryption_Secrets_Block], - secrets_type=secrets_type_val, - secrets_length=secrets_length, - secrets_data=secrets_data_val, - options=options_value, - length2=total_length + secrets_padded + BLOCK_MINIMUM[Enum_BlockType.Decryption_Secrets_Block], - ), block, 'padding_data') + return Schema_IF_SpeedOption( + type=type, + length=8, + speed=speed, + ) - def _make_block_cb(self, block: 'Optional[Data_CustomBlock]' = None, *, - pen: 'int' = 0, - data: 'Schema | bytes' = b'', - options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long - **kwargs: 'Any') -> 'Schema_CustomBlock': - """Make PCAP-NG custom block (CB). + def _make_option_if_tsresol(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_TSResolOption]' = None, *, + resolution: 'int' = DEFAULT_RESOLUTION, + **kwargs: 'Any') -> 'Schema_IF_TSResolOption': + """Make PCAP-NG ``if_tsresol`` option. Args: - block: Block data model. - pen: Private enterprise number. - data: Custom data. - options: Block options. + type: Option type. + option: Option data model. + resolution: Resolution of timestamps, in units per second. **kwargs: Arbitrary keyword arguments. Returns: - Constructed block schema. + Constructed option schema. """ - if block is not None: - pen = block.pen - data = block.data - options = cast('Option', getattr(block, 'options', None)) + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_tsresol] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[type] > 0: + raise ProtocolError(f'PCAP-NG: [if_tsresol] option must be only one, ' + f'not {self._opt[type] + 1}') - # NOTE: Unlike every other block that carries options, a custom block - # has no ``OptionField`` to name an option registry: - # :class:`~pcapkit.protocols.misc.pcapng.schema.CustomBlock` - # spans the custom data, its padding and the options as one opaque blob, - # since only the owner of the private enterprise number knows where the - # custom data ends. Its options are therefore resolved in the generic - # ``opt_*`` space, which is the only one valid in every block. - if options is not None: - options_value, _ = self._make_pcapng_options(options, namespace='opt') - else: - options_value, _ = [], 0 + if option is not None: + resolution = option.resolution - cb_data = data.pack() if isinstance(data, Schema) else data - cb_data += bytes(pad_length(len(cb_data), 4)) - for option in options_value: - cb_data += option.pack() if isinstance(option, Schema) else option - cb_data += bytes(pad_length(len(cb_data), 4)) + test = math.log10(resolution).as_integer_ratio()[1] + if test == 1: + flag = 0 + resl = int(math.log10(resolution)) + else: + test = math.log2(resolution).as_integer_ratio()[1] + if test == 1: + flag = 1 + resl = int(math.log2(resolution)) + else: + raise ProtocolError(f'PCAP-NG: [if_tsresol] option resolution must be power of 10 or 2, ' + f'not {resolution}') - # NOTE: both custom block types span the same fixed fields, and this - # method makes either, so the one entry stands for both. - minimum = BLOCK_MINIMUM[Enum_BlockType.Custom_Block_that_rewriters_can_copy_into_new_files] - return Schema_CustomBlock( - length=len(cb_data) + minimum, - pen=pen, - data=cb_data, - length2=len(cb_data) + minimum, + return Schema_IF_TSResolOption( + type=type, + length=1, + tsresol={ + 'flag': flag, + 'resolution': resl, + }, ) - def _make_block_packet(self, block: 'Optional[Data_PacketBlock]' = None, *, - interface_id: 'int' = 0, - drop_count: 'int' = 0, - timestamp: 'Optional[dt_type | int | float | Decimal]' = None, - captured_len: 'Optional[int]' = None, - original_len: 'Optional[int]' = None, - packet_data: 'bytes | ProtocolBase | Schema' = b'', - options: 'Optional[Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes]]' = None, # pylint: disable=line-too-long - **kwargs: 'Any') -> 'Schema_PacketBlock': - """Make PCAP-NG packet block (obsolete). + def _make_option_if_tzone(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_TZoneOption]' = None, *, + tzone: 'timezone | timedelta | int' = 0, + **kwargs: 'Any') -> 'Schema_IF_TZoneOption': + """Make PCAP-NG ``if_tzone`` option. Args: - block: Block data model. - interface_id: Interface ID. - drop_count: Drops count, ``0xFFFF`` for "not available". - timestamp: Packet timestamp. - captured_len: Captured length. - original_len: Original length. - packet_data: Payload of the block. - options: Block options. + type: Option type. + option: Option data model. + tzone: Timezone offset from UTC, as a :class:`~datetime.timezone`, + a :class:`~datetime.timedelta` or an :obj:`int` of seconds. **kwargs: Arbitrary keyword arguments. Returns: - Constructed block schema. + Constructed option schema. Raises: - ProtocolError: If ``drop_count`` is not an :obj:`int` (a - :obj:`bool` included), or is outside ``0`` to ``0xFFFF``. + ProtocolError: If ``tzone`` is none of those types, or is a + :obj:`bool`, or its seconds do not fit the signed 32-bit field. .. versionchanged:: 1.6.0 - ``drop_count`` is checked by - :meth:`~pcapkit.protocols.protocol.Protocol._make_uint`, so a value - the 16-bit field cannot hold, ``-1`` included, or a :obj:`bool` + An offset the signed 32-bit field cannot hold, or a :obj:`bool`, raises :exc:`~pcapkit.utilities.exceptions.ProtocolError` rather - than being written masked (:issue:`2614`). + than being written truncated (:issue:`2614`). """ - warn('PCAP-NG: Packet Block has been obsolete! Please use Enhanced Packet Block and/or ' - 'Simple Packet Block instead.', DeprecatedFormatWarning, stacklevel=stacklevel()) - - if block is not None and hasattr(block, 'block_raw'): - return self._make_block_short(block) # type: ignore[return-value] - if block is not None: - interface_id = block.interface_id - drop_count = block.drop_count - timestamp = block.timestamp_epoch - captured_len = block.captured_len - original_len = block.original_len - packet_data = self._make_packet_data(block) - options = self._block_area(block, 'options') - drop_count = self._make_uint(drop_count, bits=16, name='[PB] drop_count', alias='PCAP-NG') + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_tzone] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[type] > 0: + raise ProtocolError(f'PCAP-NG: [if_tzone] option must be only one, ' + f'not {self._opt[type] + 1}') - timestamp_high, timestamp_low = self._make_timestamp(timestamp, interface_id=interface_id) - if captured_len is None: - if self._ctx is None: - snaplen = 0xFFFF_FFFF_FFFF_FFFF - else: - # a snaplen of zero means no limit - snaplen = self._get_interface(interface_id).snaplen or 0xFFFF_FFFF_FFFF_FFFF - captured_len = min(len(packet_data), snaplen) - if original_len is None: - original_len = len(packet_data) - # NOTE: Packet Data holds exactly Captured Packet Length octets, so a - # packet longer than that -- e.g. than the interface's snaplen -- is cut - # to it rather than written out whole behind a clipped length. - if len(packet_data) > captured_len: - packet_data = bytes(packet_data)[:captured_len] - packet_len = len(packet_data) + pad_length(len(packet_data), 4) + if option is not None: + tzone = option.timezone - if options is not None: - options_value, total_length = self._make_pcapng_options(options, namespace='pack') + if isinstance(tzone, int): + tzone_val = tzone + elif isinstance(tzone, datetime.timedelta): + tzone_val = int(tzone.total_seconds()) + elif isinstance(tzone, datetime.timezone): + tzone_val = int(tzone.utcoffset(None).total_seconds()) else: - options_value, total_length = [], 0 - total_length += self._make_options_trailer(block, options_value) + raise ProtocolError(f'PCAP-NG: [if_tzone] option timezone must be int, timedelta or timezone, ' + f'not {builtins.type(tzone).__name__}') + # NOTE: checked rather than masked, see ``IF_TZoneOption.tzone``; the + # signed value is written, as the field reads it back. + self._make_sint(tzone_val, bits=32, name='[if_tzone] tzone', alias='PCAP-NG') - return self._make_padding(Schema_PacketBlock( - length=total_length + BLOCK_MINIMUM[Enum_BlockType.Packet_Block] + packet_len, - interface_id=interface_id, - drop_count=drop_count, - timestamp_high=timestamp_high, - timestamp_low=timestamp_low, - captured_length=captured_len, - original_length=original_len, - packet_data=packet_data, - options=options_value, - length2=total_length + BLOCK_MINIMUM[Enum_BlockType.Packet_Block] + packet_len, - ), block, 'padding_data') + return Schema_IF_TZoneOption( + type=type, + length=4, + tzone=tzone_val, + ) - def _make_pcapng_options(self, options: 'Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes] | bytes', # pylint: disable=line-too-long - namespace: 'str') -> 'tuple[list[Schema_Option | bytes], int]': - """Make options for PCAP-NG. + def _make_option_if_filter(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_FilterOption]' = None, *, + filter: 'Enum_FilterType | int' = Enum_FilterType(0), + expression: 'bytes | str' = b'', + **kwargs: 'Any') -> 'Schema_IF_FilterOption': + """Make PCAP-NG ``if_filter`` option. Args: - options: PCAP-NG options. An option's code is a member, kept as - given, or an :class:`int`, resolved in ``namespace``. - namespace: Namespace of options. + type: Option type. + option: Option data model. + filter: Filter type. + expression: Filter expression. + **kwargs: Arbitrary keyword arguments. Returns: - Tuple of options and total length of options. + Constructed option schema. Raises: - ProtocolError: If an option's code is a name, a :obj:`bool`, or a - code the 16-bit option code field cannot carry - (:issue:`2594`), or if an option given as raw octets is - shorter than that field (:issue:`2723`). + ProtocolError: If ``filter`` is a name, a :obj:`bool`, or a member + or code the 8-bit filter type field cannot carry. """ - # an option area kept as captured is written back verbatim - if isinstance(options, bytes): - return [options], len(options) + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_filter] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[type] > 0: + raise ProtocolError(f'PCAP-NG: [if_filter] option must be only one, ' + f'not {self._opt[type] + 1}') - has_endofopt = False - total_length = 0 - if isinstance(options, list): - options_list = [] # type: list[Schema_Option | bytes] - for schema in options: - if isinstance(schema, bytes): - # NOTE: Octets short of the code field would read as code 0 - # and be taken for an opt_endofopt (#2723). - if len(schema) < 2: - raise ProtocolError(f'PCAP-NG: invalid option: {schema!r} holds no whole option code') - code = Enum_OptionType.get(int.from_bytes(schema[0:2], self._byte, signed=False), - namespace=namespace) - if code == Enum_OptionType.opt_endofopt: # moved to the end, written once - has_endofopt = True - continue + if option is not None: + filter_val = option.code + expr_val = option.expression + else: + filter_val = self._make_index(filter, Enum_FilterType, bits=8, name='[if_filter] filter', + alias='PCAP-NG') + expr_val = expression if isinstance(expression, bytes) else expression.encode() - data = schema # type: Schema_Option | bytes - data_len = len(data) - elif isinstance(schema, Schema): - code = schema.type - if code == Enum_OptionType.opt_endofopt: # moved to the end, written once - has_endofopt = True - continue + return Schema_IF_FilterOption( + type=type, + length=1 + len(expr_val), + code=filter_val, + filter=expr_val, + ) - data = schema - data_len = len(schema.pack()) - else: - kind, args = cast('tuple[Enum_OptionType | int, dict[str, Any]]', schema) - # NOTE: a member is a str, which ``_make_index`` refuses, and a code is - # resolved in the block's namespace, as a raw option's is. - code = kind if isinstance(kind, Enum_OptionType) else Enum_OptionType.get( - self._make_uint(kind, bits=16, name='option code', alias='PCAP-NG'), namespace=namespace) - if code == Enum_OptionType.opt_endofopt: # moved to the end, written once - has_endofopt = True - continue + def _make_option_if_os(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_OSOption]' = None, *, + os: 'str' = platform.platform(), + **kwargs: 'Any') -> 'Schema_IF_OSOption': + """Make PCAP-NG ``if_os`` option. - name = self._lookup_registry(self.__option__, _option_key(code)) - if isinstance(name, str): - meth_name = f'_make_option_{name}' - meth = cast('OptionConstructor', - getattr(self, meth_name, self._make_option_unknown)) - else: - meth = name[1] + Args: + type: Option type. + option: Option data model. + os: Operating system name. + **kwargs: Arbitrary keyword arguments. - data = self._make_padding(meth(code, **args), args) - data_len = len(data.pack()) + Returns: + Constructed option schema. - options_list.append(data) - total_length += data_len - self._opt[code] += 1 + """ + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_os] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[type] > 0: + raise ProtocolError(f'PCAP-NG: [if_os] option must be only one, ' + f'not {self._opt[type] + 1}') - if has_endofopt: - opt_endofopt = self._make_option_endofopt(Enum_OptionType.opt_endofopt) - total_length += len(opt_endofopt.pack()) - options_list.append(opt_endofopt) - return options_list, total_length + if option is not None: + os = option.os - options_list = [] - for key, option in options.items(multi=True): - code = key if isinstance(key, Enum_OptionType) else Enum_OptionType.get( - self._make_uint(key, bits=16, name='option code', alias='PCAP-NG'), namespace=namespace) - if code == Enum_OptionType.opt_endofopt: # moved to the end, written once - has_endofopt = True - continue + return Schema_IF_OSOption( + type=type, + length=_utf8_length(os), + os=os, + ) - name = self._lookup_registry(self.__option__, _option_key(code)) - if isinstance(name, str): - meth_name = f'_make_option_{name}' - meth = cast('OptionConstructor', - getattr(self, meth_name, self._make_option_unknown)) - else: - meth = name[1] + def _make_option_if_fcslen(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_FCSLenOption]' = None, *, + fcs_length: 'int' = 4, + **kwargs: 'Any') -> 'Schema_IF_FCSLenOption': + """Make PCAP-NG ``if_fcslen`` option. - data = self._make_padding(meth(code, option), option) - data_len = len(data.pack()) + Args: + type: Option type. + option: Option data model. + fcs_length: FCS length, in bytes. + **kwargs: Arbitrary keyword arguments. - options_list.append(data) - total_length += data_len - self._opt[code] += 1 + Returns: + Constructed option schema. - if has_endofopt: - opt_endofopt = self._make_option_endofopt(Enum_OptionType.opt_endofopt) - total_length += len(opt_endofopt.pack()) - options_list.append(opt_endofopt) - return options_list, total_length + """ + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_fcslen] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[type] > 0: + raise ProtocolError(f'PCAP-NG: [if_fcslen] option must be only one, ' + f'not {self._opt[type] + 1}') - def _make_option_unknown(self, type: 'Enum_OptionType', option: 'Optional[Data_UnknownOption]' = None, *, - data: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_UnknownOption': - """Make unknown PCAP-NG option. + if option is not None: + fcs_length = option.fcs_length + + return Schema_IF_FCSLenOption( + type=type, + length=1, + fcslen=fcs_length, + ) + + def _make_option_if_tsoffset(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_TSOffsetOption]' = None, *, + offset: 'int' = 0, + **kwargs: 'Any') -> 'Schema_IF_TSOffsetOption': + """Make PCAP-NG ``if_tsoffset`` option. Args: type: Option type. option: Option data model. - data: Unspecified option data. + offset: Timestamp offset in seconds. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. + Raises: + ProtocolError: If ``offset`` is not an :obj:`int`, is a + :obj:`bool`, or does not fit the signed 64-bit field. + + .. versionchanged:: 1.6.0 + + An offset the signed 64-bit field cannot hold, a :obj:`bool` or a + non-integer raises :exc:`~pcapkit.utilities.exceptions.ProtocolError` + rather than being written truncated, written as zero (``None``) or + failing to pack (:issue:`2614`). + """ - if option is not None: - data = option.data + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_tsoffset] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[type] > 0: + raise ProtocolError(f'PCAP-NG: [if_tsoffset] option must be only one, ' + f'not {self._opt[type] + 1}') - length = len(data) + if option is not None: + offset = option.offset + # NOTE: checked rather than masked, see ``IF_TSOffsetOption.tsoffset``. + self._make_sint(offset, bits=64, name='[if_tsoffset] offset', alias='PCAP-NG') - return Schema_UnknownOption( + return Schema_IF_TSOffsetOption( type=type, - length=length, - data=data, + length=8, + tsoffset=offset, ) - def _make_option_endofopt(self, type: 'Enum_OptionType', option: 'Optional[Data_EndOfOption]' = None, - **kwargs: 'Any') -> 'Schema_EndOfOption': - """Make PCAP-NG ``opt_endofopt`` option. + def _make_option_if_hardware(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_HardwareOption]' = None, *, + hardware: 'str' = platform.processor(), + **kwargs: 'Any') -> 'Schema_IF_HardwareOption': + """Make PCAP-NG ``if_hardware`` option. Args: type: Option type. option: Option data model. + hardware: Hardware information. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. """ + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_hardware] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [opt_endofopt] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [if_hardware] option must be only one, ' f'not {self._opt[type] + 1}') - return Schema_EndOfOption( + if option is not None: + hardware = option.hardware + + return Schema_IF_HardwareOption( type=type, - length=0, + length=_utf8_length(hardware), + hardware=hardware, ) - def _make_option_comment(self, type: 'Enum_OptionType', option: 'Optional[Data_CommentOption]' = None, *, - comment: 'str' = '', - **kwargs: 'Any') -> 'Schema_CommentOption': - """Make PCAP-NG ``opt_comment`` option. + def _make_option_if_txspeed(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_TxSpeedOption]' = None, *, + speed: 'int' = 100000000, + **kwargs: 'Any') -> 'Schema_IF_TxSpeedOption': + """Make PCAP-NG ``if_txspeed`` option. Args: type: Option type. option: Option data model. - comment: Comment text. + speed: Interface transmit speed, in bits per second. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. """ + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_txspeed] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[type] > 0: + raise ProtocolError(f'PCAP-NG: [if_txspeed] option must be only one, ' + f'not {self._opt[type] + 1}') + if option is not None: - comment = option.comment + speed = option.speed - return Schema_CommentOption( + return Schema_IF_TxSpeedOption( type=type, - length=_utf8_length(comment), - comment=comment, + length=8, + tx_speed=speed, ) - def _make_option_custom(self, type: 'Enum_OptionType', option: 'Optional[Data_CustomOption]' = None, *, - pen: 'int' = 0xFFFFFFFF, - data: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_CustomOption': - """Make PCAP-NG ``opt_custom`` option. + def _make_option_if_rxspeed(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_RxSpeedOption]' = None, *, + speed: 'int' = 100000000, + **kwargs: 'Any') -> 'Schema_IF_RxSpeedOption': + """Make PCAP-NG ``if_rxspeed`` option. Args: type: Option type. option: Option data model. - pen: Private enterprise number. - data: Custom data. + speed: Interface receive speed, in bits per second. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. """ + if self._type != Enum_BlockType.Interface_Description_Block: + raise ProtocolError(f'PCAP-NG: [if_rxspeed] option must be in Interface Description Block, ' + f'not {_block_type_name(self._type)}') + if self._opt[type] > 0: + raise ProtocolError(f'PCAP-NG: [if_rxspeed] option must be only one, ' + f'not {self._opt[type] + 1}') + if option is not None: - pen = option.pen - data = option.data + speed = option.speed - return Schema_CustomOption( + return Schema_IF_RxSpeedOption( type=type, - length=len(data) + 4, - pen=pen, - data=data + length=8, + rx_speed=speed, ) - def _make_option_if_name(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_NameOption]' = None, *, - name: 'str' = '', - **kwargs: 'Any') -> 'Schema_IF_NameOption': - """Make PCAP-NG ``if_name`` option. + def _make_option_epb_flags(self, type: 'Enum_OptionType', option: 'Optional[Data_EPB_FlagsOption]' = None, *, + direction: 'PacketDirection | int' = PacketDirection.UNKNOWN, + reception: 'PacketReception | int' = PacketReception.UNKNOWN, + fcs_len: 'int' = 0, + checksum_not_ready: 'bool' = False, + checksum_valid: 'bool' = False, + tcp_segmentation_offloaded: 'bool' = False, + reserved: 'int' = 0, + crc_error: 'bool' = False, + too_long: 'bool' = False, + too_short: 'bool' = False, + gap_error: 'bool' = False, + unaligned_error: 'bool' = False, + delimiter_error: 'bool' = False, + preamble_error: 'bool' = False, + symbol_error: 'bool' = False, + **kwargs: 'Any') -> 'Schema_EPB_FlagsOption': + """Make PCAP-NG ``epb_flags`` option. Args: type: Option type. option: Option data model. - name: Interface name. + direction: Packet direction. + reception: Packet reception. + fcs_len: Length of FCS field, in bytes. + checksum_not_ready: Whether the checksum is not ready. + checksum_valid: Whether the checksum is valid. + tcp_segmentation_offloaded: Whether TCP segmentation was offloaded. + reserved: Bits 12 to 23 of the flags word, verbatim. + crc_error: Whether CRC error occurred. + too_long: Whether packet is too long. + too_short: Whether packet is too short. + gap_error: Whether gap error occurred. + unaligned_error: Whether unaligned error occurred. + delimiter_error: Whether delimiter error occurred. + preamble_error: Whether preamble error occurred. + symbol_error: Whether symbol error occurred. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. + Raises: + ProtocolError: If ``direction`` or ``reception`` is a name, a + :obj:`bool`, or a member or code its 2-bit or 3-bit sub-field + cannot carry. + """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_name] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Enhanced_Packet_Block: + raise ProtocolError(f'PCAP-NG: [epb_flags] option must be in Enhanced Packet Block, ' f'not {_block_type_name(self._type)}') if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [if_name] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [epb_flags] option must be only one, ' f'not {self._opt[type] + 1}') if option is not None: - name = option.name + direction_val = option.direction + reception_val = option.reception + fcs_len = option.fcs_len + checksum_not_ready = option.checksum_not_ready + checksum_valid = option.checksum_valid + tcp_segmentation_offloaded = option.tcp_segmentation_offloaded + reserved = option.reserved + crc_error = option.crc_error + too_long = option.too_long + too_short = option.too_short + gap_error = option.gap_error + unaligned_error = option.unaligned_error + delimiter_error = option.delimiter_error + preamble_error = option.preamble_error + symbol_error = option.symbol_error + else: + direction_val = self._make_index(direction, PacketDirection, bits=2, + name='[epb_flags] direction', alias='PCAP-NG') + reception_val = self._make_index(reception, PacketReception, bits=3, + name='[epb_flags] reception', alias='PCAP-NG') - return Schema_IF_NameOption( + return Schema_EPB_FlagsOption( type=type, - length=_utf8_length(name), - name=name, + length=4, + flags={ + 'direction': getattr(direction_val, 'value', direction_val), + 'reception': getattr(reception_val, 'value', reception_val), + 'fcs_len': fcs_len, + 'checksum_not_ready': int(checksum_not_ready), + 'checksum_valid': int(checksum_valid), + 'tcp_segmentation_offloaded': int(tcp_segmentation_offloaded), + 'reserved': reserved, + 'crc_error': int(crc_error), + 'too_long': int(too_long), + 'too_short': int(too_short), + 'gap_error': int(gap_error), + 'unaligned_error': int(unaligned_error), + 'delimiter_error': int(delimiter_error), + 'preamble_error': int(preamble_error), + 'symbol_error': int(symbol_error), + }, ) - def _make_option_if_description(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_DescriptionOption]' = None, *, - description: 'str' = '', - **kwargs: 'Any') -> 'Schema_IF_DescriptionOption': - """Make PCAP-NG ``if_description`` option. + def _make_option_epb_hash(self, type: 'Enum_OptionType', option: 'Optional[Data_EPB_HashOption]' = None, *, + algorithm: 'Enum_HashAlgorithm | int' = Enum_HashAlgorithm.two_s_complement, + hash: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_EPB_HashOption': + """Make PCAP-NG ``epb_hash`` option. Args: type: Option type. option: Option data model. - description: Interface description. + algorithm: Hash algorithm. + hash: Hash value. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. + Raises: + ProtocolError: If ``algorithm`` is a name, a :obj:`bool`, or a + member or code the 8-bit hash algorithm field cannot carry. + """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_description] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Enhanced_Packet_Block: + raise ProtocolError(f'PCAP-NG: [epb_hash] option must be in Enhanced Packet Block, ' f'not {_block_type_name(self._type)}') - if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [if_description] option must be only one, ' - f'not {self._opt[type] + 1}') if option is not None: - description = option.description + algo_val = option.algorithm + hash = option.hash + else: + algo_val = self._make_index(algorithm, Enum_HashAlgorithm, bits=8, + name='[epb_hash] algorithm', alias='PCAP-NG') - return Schema_IF_DescriptionOption( + return Schema_EPB_HashOption( type=type, - length=_utf8_length(description), - description=description, + length=1 + len(hash), + func=algo_val, + data=hash, ) - def _make_option_if_ipv4(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_IPv4AddrOption]' = None, *, - interface: 'IPv4Interface | str' = '192.168.1.1/255.255.255.0', - **kwargs: 'Any') -> 'Schema_IF_IPv4AddrOption': - """Make PCAP-NG ``if_IPv4addr`` option. + def _make_option_epb_dropcount(self, type: 'Enum_OptionType', option: 'Optional[Data_EPB_DropCountOption]' = None, *, + drop_count: 'int' = 0, + **kwargs: 'Any') -> 'Schema_EPB_DropCountOption': + """Make PCAP-NG ``epb_dropcount`` option. Args: type: Option type. option: Option data model. - interface: IPv4 interface. + drop_count: Number of dropped packets. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_IPv4addr] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Enhanced_Packet_Block: + raise ProtocolError(f'PCAP-NG: [epb_dropcount] option must be in Enhanced Packet Block, ' f'not {_block_type_name(self._type)}') + if self._opt[type] > 0: + raise ProtocolError(f'PCAP-NG: [epb_dropcount] option must be only one, ' + f'not {self._opt[type] + 1}') if option is not None: - interface = option.interface + drop_count = option.drop_count - return Schema_IF_IPv4AddrOption( + return Schema_EPB_DropCountOption( type=type, length=8, - interface=interface, + drop_count=drop_count, ) - def _make_option_if_ipv6(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_IPv6AddrOption]' = None, *, - interface: 'IPv6Interface | str' = '2001:0db8:85a3:08d3:1319:8a2e:0370:7344/64', - **kwargs: 'Any') -> 'Schema_IF_IPv6AddrOption': - """Make PCAP-NG ``if_IPv6addr`` option. + def _make_option_epb_packetid(self, type: 'Enum_OptionType', option: 'Optional[Data_EPB_PacketIDOption]' = None, *, + packet_id: 'int' = 0, + **kwargs: 'Any') -> 'Schema_EPB_PacketIDOption': + """Make PCAP-NG ``epb_packetid`` option. Args: type: Option type. option: Option data model. - interface: IPv6 interface. + packet_id: Packet ID. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_IPv6addr] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Enhanced_Packet_Block: + raise ProtocolError(f'PCAP-NG: [epb_packetid] option must be in Enhanced Packet Block, ' f'not {_block_type_name(self._type)}') + if self._opt[type] > 0: + raise ProtocolError(f'PCAP-NG: [epb_packetid] option must be only one, ' + f'not {self._opt[type] + 1}') if option is not None: - interface = option.interface + packet_id = option.packet_id - return Schema_IF_IPv6AddrOption( + return Schema_EPB_PacketIDOption( type=type, - length=17, - interface=interface, + length=8, + packet_id=packet_id, ) - def _make_option_if_mac(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_MACAddrOption]' = None, *, - interface: 'str | bytes | bytearray' = '00:01:02:03:04:05', - **kwargs: 'Any') -> 'Schema_IF_MACAddrOption': - """Make PCAP-NG ``if_MACaddr`` option. + def _make_option_epb_queue(self, type: 'Enum_OptionType', option: 'Optional[Data_EPB_QueueOption]' = None, *, + queue_id: 'int' = 0, + **kwargs: 'Any') -> 'Schema_EPB_QueueOption': + """Make PCAP-NG ``epb_queue`` option. Args: type: Option type. option: Option data model. - interface: MAC interface address. + queue_id: Queue ID. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_MACaddr] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Enhanced_Packet_Block: + raise ProtocolError(f'PCAP-NG: [epb_queue] option must be in Enhanced Packet Block, ' f'not {_block_type_name(self._type)}') if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [if_MACaddr] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [epb_queue] option must be only one, ' f'not {self._opt[type] + 1}') if option is not None: - interface = option.interface + queue_id = option.queue_id - return Schema_IF_MACAddrOption( + return Schema_EPB_QueueOption( type=type, - length=6, - interface=self._make_mac_addr(interface), + length=4, + queue_id=queue_id, ) - def _make_option_if_eui(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_EUIAddrOption]' = None, *, - interface: 'str | bytes | bytearray' = '02:34:56:FF:FE:78:9A:BC', - **kwargs: 'Any') -> 'Schema_IF_EUIAddrOption': - """Make PCAP-NG ``if_EUIaddr`` option. + def _make_option_epb_verdict(self, type: 'Enum_OptionType', option: 'Optional[Data_EPB_VerdictOption]' = None, *, + verdict: 'Enum_VerdictType | int' = Enum_VerdictType.Hardware, + value: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_EPB_VerdictOption': + """Make PCAP-NG ``epb_verdict`` option. Args: type: Option type. option: Option data model. - interface: Hardware EUI address. + verdict: Verdict type. + value: Verdict value. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. + Raises: + ProtocolError: If ``verdict`` is a name, a :obj:`bool`, or a + member or code the 8-bit verdict type field cannot carry. + """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_EUIaddr] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Enhanced_Packet_Block: + raise ProtocolError(f'PCAP-NG: [epb_verdict] option must be in Enhanced Packet Block, ' f'not {_block_type_name(self._type)}') - if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [if_EUIaddr] option must be only one, ' - f'not {self._opt[type] + 1}') + # NOTE: No only-one guard: the specification allows ``epb_verdict`` to + # appear multiple times, and the parser accepts every occurrence. if option is not None: - interface = option.interface + verdict_val = option.verdict + value = option.value + else: + verdict_val = self._make_index(verdict, Enum_VerdictType, bits=8, + name='[epb_verdict] verdict', alias='PCAP-NG') - return Schema_IF_EUIAddrOption( + return Schema_EPB_VerdictOption( type=type, - length=8, - interface=self._make_mac_addr(interface, size=8, name='EUI address'), + length=1 + len(value), + verdict=verdict_val, + value=value, ) - def _make_option_if_speed(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_SpeedOption]' = None, *, - speed: 'int' = 100000000, - **kwargs: 'Any') -> 'Schema_IF_SpeedOption': - """Make PCAP-NG ``if_speed`` option. + def _make_option_ns_dnsname(self, type: 'Enum_OptionType', option: 'Optional[Data_NS_DNSNameOption]' = None, *, + name: 'str' = '', + **kwargs: 'Any') -> 'Schema_NS_DNSNameOption': + """Make PCAP-NG ``ns_dnsname`` option. Args: type: Option type. option: Option data model. - speed: Interface speed, in bits per second. + name: DNS server name. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_speed] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Name_Resolution_Block: + raise ProtocolError(f'PCAP-NG: [ns_dnsname] option must be in Name Resolution Block, ' f'not {_block_type_name(self._type)}') if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [if_speed] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [ns_dnsname] option must be only one, ' f'not {self._opt[type] + 1}') if option is not None: - speed = option.speed + name = option.name - return Schema_IF_SpeedOption( + return Schema_NS_DNSNameOption( type=type, - length=8, - speed=speed, + length=_utf8_length(name), + name=name, ) - def _make_option_if_tsresol(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_TSResolOption]' = None, *, - resolution: 'int' = DEFAULT_RESOLUTION, - **kwargs: 'Any') -> 'Schema_IF_TSResolOption': - """Make PCAP-NG ``if_tsresol`` option. + def _make_option_ns_dnsipv4(self, type: 'Enum_OptionType', option: 'Optional[Data_NS_DNSIP4AddrOption]' = None, *, + ip: 'str | bytes | IPv4Address | int' = '8.8.8.8', + **kwargs: 'Any') -> 'Schema_NS_DNSIP4AddrOption': + """Make PCAP-NG ``ns_dnsIP4addr`` option. Args: type: Option type. option: Option data model. - resolution: Resolution of timestamps, in units per second. + ip: DNS server IPv4 address. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_tsresol] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Name_Resolution_Block: + raise ProtocolError(f'PCAP-NG: [ns_dnsIP4addr] option must be in Name Resolution Block, ' f'not {_block_type_name(self._type)}') if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [if_tsresol] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [ns_dnsIP4addr] option must be only one, ' f'not {self._opt[type] + 1}') if option is not None: - resolution = option.resolution - - test = math.log10(resolution).as_integer_ratio()[1] - if test == 1: - flag = 0 - resl = int(math.log10(resolution)) - else: - test = math.log2(resolution).as_integer_ratio()[1] - if test == 1: - flag = 1 - resl = int(math.log2(resolution)) - else: - raise ProtocolError(f'PCAP-NG: [if_tsresol] option resolution must be power of 10 or 2, ' - f'not {resolution}') + ip = option.ip - return Schema_IF_TSResolOption( + return Schema_NS_DNSIP4AddrOption( type=type, - length=1, - tsresol={ - 'flag': flag, - 'resolution': resl, - }, + length=4, + ip=ip, ) - def _make_option_if_tzone(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_TZoneOption]' = None, *, - tzone: 'timezone | timedelta | int' = 0, - **kwargs: 'Any') -> 'Schema_IF_TZoneOption': - """Make PCAP-NG ``if_tzone`` option. - + def _make_option_ns_dnsipv6(self, type: 'Enum_OptionType', option: 'Optional[Data_NS_DNSIP6AddrOption]' = None, *, + ip: 'str | bytes | IPv6Address | int' = '2001:4860:4860::8888', + **kwargs: 'Any') -> 'Schema_NS_DNSIP6AddrOption': + """Make PCAP-NG ``ns_dnsIP6addr`` option. + Args: type: Option type. option: Option data model. - tzone: Timezone offset from UTC, as a :class:`~datetime.timezone`, - a :class:`~datetime.timedelta` or an :obj:`int` of seconds. + ip: DNS server IPv6 address. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. - Raises: - ProtocolError: If ``tzone`` is none of those types, or is a - :obj:`bool`, or its seconds do not fit the signed 32-bit field. - - .. versionchanged:: 1.6.0 - - An offset the signed 32-bit field cannot hold, or a :obj:`bool`, - raises :exc:`~pcapkit.utilities.exceptions.ProtocolError` rather - than being written truncated (:issue:`2614`). - """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_tzone] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Name_Resolution_Block: + raise ProtocolError(f'PCAP-NG: [ns_dnsIP6addr] option must be in Name Resolution Block, ' f'not {_block_type_name(self._type)}') if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [if_tzone] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [ns_dnsIP6addr] option must be only one, ' f'not {self._opt[type] + 1}') if option is not None: - tzone = option.timezone - - if isinstance(tzone, int): - tzone_val = tzone - elif isinstance(tzone, datetime.timedelta): - tzone_val = int(tzone.total_seconds()) - elif isinstance(tzone, datetime.timezone): - tzone_val = int(tzone.utcoffset(None).total_seconds()) - else: - raise ProtocolError(f'PCAP-NG: [if_tzone] option timezone must be int, timedelta or timezone, ' - f'not {builtins.type(tzone).__name__}') - # NOTE: checked rather than masked, see ``IF_TZoneOption.tzone``; the - # signed value is written, as the field reads it back. - self._make_sint(tzone_val, bits=32, name='[if_tzone] tzone', alias='PCAP-NG') + ip = option.ip - return Schema_IF_TZoneOption( + return Schema_NS_DNSIP6AddrOption( type=type, - length=4, - tzone=tzone_val, + length=16, + ip=ip, ) - def _make_option_if_filter(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_FilterOption]' = None, *, - filter: 'Enum_FilterType | int' = Enum_FilterType(0), - expression: 'bytes | str' = b'', - **kwargs: 'Any') -> 'Schema_IF_FilterOption': - """Make PCAP-NG ``if_filter`` option. + def _make_option_isb_starttime(self, type: 'Enum_OptionType', option: 'Optional[Data_ISB_StartTimeOption]' = None, *, + timestamp: 'Optional[int | float | dt_type | Decimal]' = None, + **kwargs: 'Any') -> 'Schema_ISB_StartTimeOption': + """Make PCAP-NG ``isb_starttime`` option. Args: type: Option type. option: Option data model. - filter: Filter type. - expression: Filter expression. + timestamp: Start time. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. - Raises: - ProtocolError: If ``filter`` is a name, a :obj:`bool`, or a member - or code the 8-bit filter type field cannot carry. - """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_filter] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Interface_Statistics_Block: + raise ProtocolError(f'PCAP-NG: [isb_starttime] option must be in Interface Statistics Block, ' f'not {_block_type_name(self._type)}') if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [if_filter] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [isb_starttime] option must be only one, ' f'not {self._opt[type] + 1}') if option is not None: - filter_val = option.code - expr_val = option.expression - else: - filter_val = self._make_index(filter, Enum_FilterType, bits=8, name='[if_filter] filter', - alias='PCAP-NG') - expr_val = expression if isinstance(expression, bytes) else expression.encode() + timestamp = option.timestamp_epoch + ts_high, ts_low = self._make_timestamp(timestamp, interface_id=self._isb_interface_id) - return Schema_IF_FilterOption( + return Schema_ISB_StartTimeOption( type=type, - length=1 + len(expr_val), - code=filter_val, - filter=expr_val, + length=8, + timestamp_high=ts_high, + timestamp_low=ts_low, ) - def _make_option_if_os(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_OSOption]' = None, *, - os: 'str' = platform.platform(), - **kwargs: 'Any') -> 'Schema_IF_OSOption': - """Make PCAP-NG ``if_os`` option. + def _make_option_isb_endtime(self, type: 'Enum_OptionType', option: 'Optional[Data_ISB_EndTimeOption]' = None, *, + timestamp: 'Optional[int | float | dt_type | Decimal]' = None, + **kwargs: 'Any') -> 'Schema_ISB_EndTimeOption': + """Make PCAP-NG ``isb_endtime`` option. Args: type: Option type. option: Option data model. - os: Operating system name. + timestamp: End time. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_os] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Interface_Statistics_Block: + raise ProtocolError(f'PCAP-NG: [isb_endtime] option must be in Interface Statistics Block, ' f'not {_block_type_name(self._type)}') if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [if_os] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [isb_endtime] option must be only one, ' f'not {self._opt[type] + 1}') if option is not None: - os = option.os + timestamp = option.timestamp_epoch + ts_high, ts_low = self._make_timestamp(timestamp, interface_id=self._isb_interface_id) - return Schema_IF_OSOption( + return Schema_ISB_EndTimeOption( type=type, - length=_utf8_length(os), - os=os, + length=8, + timestamp_high=ts_high, + timestamp_low=ts_low, ) - def _make_option_if_fcslen(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_FCSLenOption]' = None, *, - fcs_length: 'int' = 4, - **kwargs: 'Any') -> 'Schema_IF_FCSLenOption': - """Make PCAP-NG ``if_fcslen`` option. + def _make_option_isb_ifrecv(self, type: 'Enum_OptionType', option: 'Optional[Data_ISB_IFRecvOption]' = None, *, + packets: 'int' = 0, + **kwargs: 'Any') -> 'Schema_ISB_IFRecvOption': + """Make PCAP-NG ``isb_ifrecv`` option. Args: type: Option type. option: Option data model. - fcs_length: FCS length, in bytes. + packets: Number of received packets. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_fcslen] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Interface_Statistics_Block: + raise ProtocolError(f'PCAP-NG: [isb_ifrecv] option must be in Interface Statistics Block, ' f'not {_block_type_name(self._type)}') if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [if_fcslen] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [isb_ifrecv] option must be only one, ' f'not {self._opt[type] + 1}') if option is not None: - fcs_length = option.fcs_length + packets = option.packets - return Schema_IF_FCSLenOption( + return Schema_ISB_IFRecvOption( type=type, - length=1, - fcslen=fcs_length, + length=8, + packets=packets, ) - def _make_option_if_tsoffset(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_TSOffsetOption]' = None, *, - offset: 'int' = 0, - **kwargs: 'Any') -> 'Schema_IF_TSOffsetOption': - """Make PCAP-NG ``if_tsoffset`` option. + def _make_option_isb_ifdrop(self, type: 'Enum_OptionType', option: 'Optional[Data_ISB_IFDropOption]' = None, *, + packets: 'int' = 0, + **kwargs: 'Any') -> 'Schema_ISB_IFDropOption': + """Make PCAP-NG ``isb_ifdrop`` option. Args: type: Option type. option: Option data model. - offset: Timestamp offset in seconds. + packets: Number of dropped packets. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. - Raises: - ProtocolError: If ``offset`` is not an :obj:`int`, is a - :obj:`bool`, or does not fit the signed 64-bit field. - - .. versionchanged:: 1.6.0 - - An offset the signed 64-bit field cannot hold, a :obj:`bool` or a - non-integer raises :exc:`~pcapkit.utilities.exceptions.ProtocolError` - rather than being written truncated, written as zero (``None``) or - failing to pack (:issue:`2614`). - """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_tsoffset] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Interface_Statistics_Block: + raise ProtocolError(f'PCAP-NG: [isb_ifdrop] option must be in Interface Statistics Block, ' f'not {_block_type_name(self._type)}') if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [if_tsoffset] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [isb_ifdrop] option must be only one, ' f'not {self._opt[type] + 1}') if option is not None: - offset = option.offset - # NOTE: checked rather than masked, see ``IF_TSOffsetOption.tsoffset``. - self._make_sint(offset, bits=64, name='[if_tsoffset] offset', alias='PCAP-NG') + packets = option.packets - return Schema_IF_TSOffsetOption( + return Schema_ISB_IFDropOption( type=type, length=8, - tsoffset=offset, + packets=packets, ) - def _make_option_if_hardware(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_HardwareOption]' = None, *, - hardware: 'str' = platform.processor(), - **kwargs: 'Any') -> 'Schema_IF_HardwareOption': - """Make PCAP-NG ``if_hardware`` option. + def _make_option_isb_filteraccept(self, type: 'Enum_OptionType', option: 'Optional[Data_ISB_FilterAcceptOption]' = None, *, + packets: 'int' = 0, + **kwargs: 'Any') -> 'Schema_ISB_FilterAcceptOption': + """Make PCAP-NG ``isb_filteraccept`` option. Args: type: Option type. option: Option data model. - hardware: Hardware information. + packets: Number of packets accepted by the filter. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_hardware] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Interface_Statistics_Block: + raise ProtocolError(f'PCAP-NG: [isb_filteraccept] option must be in Interface Statistics Block, ' f'not {_block_type_name(self._type)}') if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [if_hardware] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [isb_filteraccept] option must be only one, ' f'not {self._opt[type] + 1}') if option is not None: - hardware = option.hardware + packets = option.packets - return Schema_IF_HardwareOption( + return Schema_ISB_FilterAcceptOption( type=type, - length=_utf8_length(hardware), - hardware=hardware, + length=8, + packets=packets, ) - def _make_option_if_txspeed(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_TxSpeedOption]' = None, *, - speed: 'int' = 100000000, - **kwargs: 'Any') -> 'Schema_IF_TxSpeedOption': - """Make PCAP-NG ``if_txspeed`` option. + def _make_option_isb_osdrop(self, type: 'Enum_OptionType', option: 'Optional[Data_ISB_OSDropOption]' = None, *, + packets: 'int' = 0, + **kwargs: 'Any') -> 'Schema_ISB_OSDropOption': + """Make PCAP-NG ``isb_osdrop`` option. Args: type: Option type. option: Option data model. - speed: Interface transmit speed, in bits per second. + packets: Number of packets dropped by the OS. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_txspeed] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Interface_Statistics_Block: + raise ProtocolError(f'PCAP-NG: [isb_osdrop] option must be in Interface Statistics Block, ' f'not {_block_type_name(self._type)}') if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [if_txspeed] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [isb_osdrop] option must be only one, ' f'not {self._opt[type] + 1}') if option is not None: - speed = option.speed + packets = option.packets - return Schema_IF_TxSpeedOption( + return Schema_ISB_OSDropOption( type=type, length=8, - tx_speed=speed, + packets=packets, ) - def _make_option_if_rxspeed(self, type: 'Enum_OptionType', option: 'Optional[Data_IF_RxSpeedOption]' = None, *, - speed: 'int' = 100000000, - **kwargs: 'Any') -> 'Schema_IF_RxSpeedOption': - """Make PCAP-NG ``if_rxspeed`` option. + def _make_option_isb_usrdeliv(self, type: 'Enum_OptionType', option: 'Optional[Data_ISB_UsrDelivOption]' = None, *, + packets: 'int' = 0, + **kwargs: 'Any') -> 'Schema_ISB_UsrDelivOption': + """Make PCAP-NG ``isb_usrdeliv`` option. Args: type: Option type. option: Option data model. - speed: Interface receive speed, in bits per second. + packets: Number of packets delivered to the user. **kwargs: Arbitrary keyword arguments. Returns: Constructed option schema. """ - if self._type != Enum_BlockType.Interface_Description_Block: - raise ProtocolError(f'PCAP-NG: [if_rxspeed] option must be in Interface Description Block, ' + if self._type != Enum_BlockType.Interface_Statistics_Block: + raise ProtocolError(f'PCAP-NG: [isb_usrdeliv] option must be in Interface Statistics Block, ' f'not {_block_type_name(self._type)}') if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [if_rxspeed] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [isb_usrdeliv] option must be only one, ' f'not {self._opt[type] + 1}') if option is not None: - speed = option.speed - - return Schema_IF_RxSpeedOption( - type=type, - length=8, - rx_speed=speed, - ) - - def _make_option_epb_flags(self, type: 'Enum_OptionType', option: 'Optional[Data_EPB_FlagsOption]' = None, *, - direction: 'PacketDirection | int' = PacketDirection.UNKNOWN, - reception: 'PacketReception | int' = PacketReception.UNKNOWN, - fcs_len: 'int' = 0, - checksum_not_ready: 'bool' = False, - checksum_valid: 'bool' = False, - tcp_segmentation_offloaded: 'bool' = False, - reserved: 'int' = 0, - crc_error: 'bool' = False, - too_long: 'bool' = False, - too_short: 'bool' = False, - gap_error: 'bool' = False, - unaligned_error: 'bool' = False, - delimiter_error: 'bool' = False, - preamble_error: 'bool' = False, - symbol_error: 'bool' = False, - **kwargs: 'Any') -> 'Schema_EPB_FlagsOption': - """Make PCAP-NG ``epb_flags`` option. + packets = option.packets + + return Schema_ISB_UsrDelivOption( + type=type, + length=8, + packets=packets, + ) + + def _make_option_pack_flags(self, type: 'Enum_OptionType', option: 'Optional[Data_PACK_FlagsOption]' = None, *, + direction: 'PacketDirection | int' = PacketDirection.UNKNOWN, + reception: 'PacketReception | int' = PacketReception.UNKNOWN, + fcs_len: 'int' = 0, + checksum_not_ready: 'bool' = False, + checksum_valid: 'bool' = False, + tcp_segmentation_offloaded: 'bool' = False, + reserved: 'int' = 0, + crc_error: 'bool' = False, + too_long: 'bool' = False, + too_short: 'bool' = False, + gap_error: 'bool' = False, + unaligned_error: 'bool' = False, + delimiter_error: 'bool' = False, + preamble_error: 'bool' = False, + symbol_error: 'bool' = False, + **kwargs: 'Any') -> 'Schema_PACK_FlagsOption': + """Make PCAP-NG ``pack_flags`` option. Args: type: Option type. @@ -5953,11 +5773,11 @@ def _make_option_epb_flags(self, type: 'Enum_OptionType', option: 'Optional[Data cannot carry. """ - if self._type != Enum_BlockType.Enhanced_Packet_Block: - raise ProtocolError(f'PCAP-NG: [epb_flags] option must be in Enhanced Packet Block, ' + if self._type != Enum_BlockType.Packet_Block: + raise ProtocolError(f'PCAP-NG: [pack_flags] option must be in Packet Block, ' f'not {_block_type_name(self._type)}') if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [epb_flags] option must be only one, ' + raise ProtocolError(f'PCAP-NG: [pack_flags] option must be only one, ' f'not {self._opt[type] + 1}') if option is not None: @@ -5978,11 +5798,11 @@ def _make_option_epb_flags(self, type: 'Enum_OptionType', option: 'Optional[Data symbol_error = option.symbol_error else: direction_val = self._make_index(direction, PacketDirection, bits=2, - name='[epb_flags] direction', alias='PCAP-NG') + name='[pack_flags] direction', alias='PCAP-NG') reception_val = self._make_index(reception, PacketReception, bits=3, - name='[epb_flags] reception', alias='PCAP-NG') + name='[pack_flags] reception', alias='PCAP-NG') - return Schema_EPB_FlagsOption( + return Schema_PACK_FlagsOption( type=type, length=4, flags={ @@ -6004,11 +5824,11 @@ def _make_option_epb_flags(self, type: 'Enum_OptionType', option: 'Optional[Data }, ) - def _make_option_epb_hash(self, type: 'Enum_OptionType', option: 'Optional[Data_EPB_HashOption]' = None, *, - algorithm: 'Enum_HashAlgorithm | int' = Enum_HashAlgorithm.two_s_complement, - hash: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_EPB_HashOption': - """Make PCAP-NG ``epb_hash`` option. + def _make_option_pack_hash(self, type: 'Enum_OptionType', option: 'Optional[Data_PACK_HashOption]' = None, *, + algorithm: 'Enum_HashAlgorithm | int' = Enum_HashAlgorithm.two_s_complement, + hash: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_PACK_HashOption': + """Make PCAP-NG ``pack_hash`` option. Args: type: Option type. @@ -6025,8 +5845,8 @@ def _make_option_epb_hash(self, type: 'Enum_OptionType', option: 'Optional[Data_ member or code the 8-bit hash algorithm field cannot carry. """ - if self._type != Enum_BlockType.Enhanced_Packet_Block: - raise ProtocolError(f'PCAP-NG: [epb_hash] option must be in Enhanced Packet Block, ' + if self._type != Enum_BlockType.Packet_Block: + raise ProtocolError(f'PCAP-NG: [pack_hash] option must be in Packet Block, ' f'not {_block_type_name(self._type)}') if option is not None: @@ -6034,1027 +5854,1207 @@ def _make_option_epb_hash(self, type: 'Enum_OptionType', option: 'Optional[Data_ hash = option.hash else: algo_val = self._make_index(algorithm, Enum_HashAlgorithm, bits=8, - name='[epb_hash] algorithm', alias='PCAP-NG') + name='[pack_hash] algorithm', alias='PCAP-NG') - return Schema_EPB_HashOption( + return Schema_PACK_HashOption( type=type, length=1 + len(hash), func=algo_val, data=hash, ) - def _make_option_epb_dropcount(self, type: 'Enum_OptionType', option: 'Optional[Data_EPB_DropCountOption]' = None, *, - drop_count: 'int' = 0, - **kwargs: 'Any') -> 'Schema_EPB_DropCountOption': - """Make PCAP-NG ``epb_dropcount`` option. + def _make_record_unknown(self, type: 'Enum_RecordType', record: 'Optional[Data_UnknownRecord]' = None, *, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_UnknownRecord': + """Make PCAP-NG unknown name resolution record. Args: - type: Option type. - option: Option data model. - drop_count: Number of dropped packets. + type: Record type. + record: Record data model. + data: Record data. **kwargs: Arbitrary keyword arguments. Returns: - Constructed option schema. + Constructed record schema. """ - if self._type != Enum_BlockType.Enhanced_Packet_Block: - raise ProtocolError(f'PCAP-NG: [epb_dropcount] option must be in Enhanced Packet Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [epb_dropcount] option must be only one, ' - f'not {self._opt[type] + 1}') + if record is not None: + data = record.data - if option is not None: - drop_count = option.drop_count + return Schema_UnknownRecord( + type=type, + length=len(data), + data=data, + ) - return Schema_EPB_DropCountOption( + def _make_record_end(self, type: 'Enum_RecordType', record: 'Optional[Data_EndRecord]' = None, + **kwargs: 'Any') -> 'Schema_EndRecord': + """Make PCAP-NG ``nrb_record_end`` name resolution record. + + Args: + type: Record type. + record: Record data model. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed record schema. + + """ + return Schema_EndRecord( type=type, - length=8, - drop_count=drop_count, + length=0, ) - def _make_option_epb_packetid(self, type: 'Enum_OptionType', option: 'Optional[Data_EPB_PacketIDOption]' = None, *, - packet_id: 'int' = 0, - **kwargs: 'Any') -> 'Schema_EPB_PacketIDOption': - """Make PCAP-NG ``epb_packetid`` option. + def _make_record_ipv4(self, type: 'Enum_RecordType', record: 'Optional[Data_IPv4Record]' = None, *, + ip: 'IPv4Address | str | bytes | int' = '127.0.0.1', + names: 'Optional[list[str]]' = None, + **kwargs: 'Any') -> 'Schema_IPv4Record': + """Make PCAP-NG ``nrb_record_ipv4`` name resolution record. Args: - type: Option type. - option: Option data model. - packet_id: Packet ID. + type: Record type. + record: Record data model. + ip: IPv4 address. + names: Host names. **kwargs: Arbitrary keyword arguments. Returns: - Constructed option schema. + Constructed record schema. """ - if self._type != Enum_BlockType.Enhanced_Packet_Block: - raise ProtocolError(f'PCAP-NG: [epb_packetid] option must be in Enhanced Packet Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [epb_packetid] option must be only one, ' - f'not {self._opt[type] + 1}') + if record is not None: + ip = record.ip + names = cast('list[str]', record.records) - if option is not None: - packet_id = option.packet_id + if names is None: + names = [] + records = _join_names(names) - return Schema_EPB_PacketIDOption( + return Schema_IPv4Record( type=type, - length=8, - packet_id=packet_id, + length=4 + _utf8_length(records), + ip=ip, + resol=records, ) - def _make_option_epb_queue(self, type: 'Enum_OptionType', option: 'Optional[Data_EPB_QueueOption]' = None, *, - queue_id: 'int' = 0, - **kwargs: 'Any') -> 'Schema_EPB_QueueOption': - """Make PCAP-NG ``epb_queue`` option. + def _make_record_ipv6(self, type: 'Enum_RecordType', record: 'Optional[Data_IPv6Record]' = None, *, + ip: 'IPv6Address | str | bytes | int' = '::1', + names: 'Optional[list[str]]' = None, + **kwargs: 'Any') -> 'Schema_IPv6Record': + """Make PCAP-NG ``nrb_record_ipv6`` name resolution record. + + Args: + type: Record type. + record: Record data model. + ip: IPv6 address. + names: Host names. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed record schema. + + """ + if record is not None: + ip = record.ip + names = cast('list[str]', record.records) + + if names is None: + names = [] + records = _join_names(names) + + return Schema_IPv6Record( + type=type, + length=16 + _utf8_length(records), + ip=ip, + resol=records, + ) + + def _make_secrets_unknown(self, type: 'Enum_SecretsType', secrets: 'Optional[Data_UnknownSecrets]' = None, *, + data: 'bytes' = b'', + **kwargs: 'Any') -> 'Schema_UnknownSecrets': + """Make PCAP-NG unknown secrets. + + Args: + type: Secrets type. + secrets: Secrets data model. + data: Secrets data. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed secrets schema. + + """ + if secrets is not None: + data = secrets.data + + return Schema_UnknownSecrets( + data=data, + ) + + def _make_secrets_tls(self, type: 'Enum_SecretsType', secrets: 'Optional[Data_TLSKeyLog]' = None, *, + entries: 'Optional[Mapping[Enum_TLSKeyLabel, OrderedMultiDict[bytes, bytes]]]' = None, + data: 'Optional[str]' = None, + **kwargs: 'Any') -> 'Schema_TLSKeyLog': + """Make PCAP-NG TLS Key Log secrets. + + Args: + type: Secrets type. + secrets: Secrets data model. The key log text it was parsed from, + if any, is written back verbatim; otherwise its entries are. + On a parsed model the text wins, so an edit to its + ``entries`` alone is silently ignored. + entries: TLS Key Log entries. + data: TLS key log text, written back verbatim in place of + ``entries``, as the text of ``secrets`` is. + **kwargs: Arbitrary keyword arguments. + + Returns: + Constructed secrets schema. + + .. versionchanged:: 1.6.0 + + Takes the key log text as ``data``, so that the :obj:`dict` of a + parsed model rebuilds the log as captured, as the model itself does, + rather than as its entries under a generated comment + (:issue:`2698`). + + """ + from pcapkit import __version__ + + if secrets is not None: + data = secrets.get('data') + if isinstance(data, str): + return Schema_TLSKeyLog( + data=data, + ) + if secrets is not None: + # NOTE: each an OrderedMultiInfo, which is an OrderedMultiDict. + entries = cast('Mapping[Enum_TLSKeyLabel, OrderedMultiDict[bytes, bytes]]', secrets.entries) + + if entries is None: + entries = {} + + lines = [f'# generated by PyPCAPKit v{__version__}\n'] # type: list[str] + for label, entry in entries.items(): + for k, v in entry.items(multi=True): + lines.append(f'{label.name} {k.hex()} {v.hex()}\n') + + return Schema_TLSKeyLog( + data=''.join(lines), + ) + + def _make_secrets_wireguard(self, type: 'Enum_SecretsType', secrets: 'Optional[Data_WireGuardKeyLog]' = None, *, + entries: 'Optional[OrderedMultiDict[WireGuardKeyLabel, bytes]]' = None, + data: 'Optional[str]' = None, + **kwargs: 'Any') -> 'Schema_WireGuardKeyLog': + """Make PCAP-NG WireGuard secrets. Args: - type: Option type. - option: Option data model. - queue_id: Queue ID. + type: Secrets type. + secrets: Secrets data model. The key log text it was parsed from, + if any, is written back verbatim; otherwise its entries are. + On a parsed model the text wins, so an edit to its + ``entries`` alone is silently ignored. + entries: WireGuard Key Log entries. + data: WireGuard key log text, written back verbatim in place of + ``entries``, as the text of ``secrets`` is. **kwargs: Arbitrary keyword arguments. Returns: - Constructed option schema. + Constructed secrets schema. + + .. versionchanged:: 1.6.0 + + Takes the key log text as ``data``, as + :meth:`_make_secrets_tls` does (:issue:`2698`). """ - if self._type != Enum_BlockType.Enhanced_Packet_Block: - raise ProtocolError(f'PCAP-NG: [epb_queue] option must be in Enhanced Packet Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [epb_queue] option must be only one, ' - f'not {self._opt[type] + 1}') + from pcapkit import __version__ - if option is not None: - queue_id = option.queue_id + if secrets is not None: + data = secrets.get('data') + if isinstance(data, str): + return Schema_WireGuardKeyLog( + data=data, + ) + if secrets is not None: + entries = secrets.entries - return Schema_EPB_QueueOption( - type=type, - length=4, - queue_id=queue_id, + if entries is None: + entries = OrderedMultiDict() + + lines = [f'# generated by PyPCAPKit v{__version__}\n'] # type: list[str] + for label, value in entries.items(multi=True): + lines.append(f'{label.name} = {base64.b64encode(value).decode()}\n') + + return Schema_WireGuardKeyLog( + data=''.join(lines), ) - def _make_option_epb_verdict(self, type: 'Enum_OptionType', option: 'Optional[Data_EPB_VerdictOption]' = None, *, - verdict: 'Enum_VerdictType | int' = Enum_VerdictType.Hardware, - value: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_EPB_VerdictOption': - """Make PCAP-NG ``epb_verdict`` option. + def _make_secrets_zigbee_aps(self, type: 'Enum_SecretsType', secrets: 'Optional[Data_ZigBeeAPSKey]' = None, *, + aps_key: 'bytes' = bytes(16), + pan_id: 'int' = 0, + short_address: 'int' = 0, + data: 'Optional[bytes]' = None, + **kwargs: 'Any') -> 'Schema_ZigBeeAPSKey': + """Make PCAP-NG ZigBee APS Key secrets. Args: - type: Option type. - option: Option data model. - verdict: Verdict type. - value: Verdict value. + type: Secrets type. + secrets: Secrets data model. The Secrets Data octets it was parsed + from, if any, are written back verbatim, as for + :meth:`_make_secrets_zigbee_nwk`; otherwise its fields are. + aps_key: APS key, 16 octets. + pan_id: PAN ID. + short_address: Short address. + data: Secrets Data octets, written back verbatim in place of the + fields, as the octets of ``secrets`` are. **kwargs: Arbitrary keyword arguments. Returns: - Constructed option schema. + Constructed secrets schema. Raises: - ProtocolError: If ``verdict`` is a name, a :obj:`bool`, or a - member or code the 8-bit verdict type field cannot carry. + ProtocolError: If ``aps_key`` is not 16 octets. - """ - if self._type != Enum_BlockType.Enhanced_Packet_Block: - raise ProtocolError(f'PCAP-NG: [epb_verdict] option must be in Enhanced Packet Block, ' - f'not {_block_type_name(self._type)}') - # NOTE: No only-one guard: the specification allows ``epb_verdict`` to - # appear multiple times, and the parser accepts every occurrence. + .. versionchanged:: 1.6.0 - if option is not None: - verdict_val = option.verdict - value = option.value - else: - verdict_val = self._make_index(verdict, Enum_VerdictType, bits=8, - name='[epb_verdict] verdict', alias='PCAP-NG') + * Takes the Secrets Data octets as ``data``, as + :meth:`_make_secrets_zigbee_nwk` does (:issue:`2698`). + * ``aps_key`` of any other width, empty included, raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError` rather than + being padded or cut to the 16-octet key, and defaults to 16 zero + octets where it defaulted to empty (:issue:`2676`). - return Schema_EPB_VerdictOption( - type=type, - length=1 + len(value), - verdict=verdict_val, - value=value, + """ + if secrets is not None: + data = secrets.get('data') + if isinstance(data, bytes): + return zigbee_key_held(Schema_ZigBeeAPSKey, data) + if secrets is not None: + aps_key = secrets.aps_key + pan_id = secrets.pan_id + short_address = secrets.short_address + + return Schema_ZigBeeAPSKey( + key=self._make_octets(aps_key, size=16, name='[ZigBee APS Key] key', alias='PCAP-NG'), + panid=pan_id, + addr_high=short_address >> 16, + addr_low=short_address & 0xFFFF, ) - def _make_option_ns_dnsname(self, type: 'Enum_OptionType', option: 'Optional[Data_NS_DNSNameOption]' = None, *, - name: 'str' = '', - **kwargs: 'Any') -> 'Schema_NS_DNSNameOption': - """Make PCAP-NG ``ns_dnsname`` option. + def _make_secrets_zigbee_nwk(self, type: 'Enum_SecretsType', secrets: 'Optional[Data_ZigBeeNWKKey]' = None, *, + nwk_key: 'bytes' = bytes(16), + pan_id: 'int' = 0, + data: 'Optional[bytes]' = None, + **kwargs: 'Any') -> 'Schema_ZigBeeNWKKey': + """Make PCAP-NG ZigBee NWK Key secrets. Args: - type: Option type. - option: Option data model. - name: DNS server name. + type: Secrets type. + secrets: Secrets data model. The Secrets Data octets it was parsed + from, if any, are written back verbatim, so that a key shorter + or longer than its fixed width rebuilds as captured + (:issue:`1986`); otherwise its fields are. On a parsed model + the octets win, so an edit to its fields alone is silently + ignored. + nwk_key: NWK key, 16 octets. + pan_id: PAN ID. + data: Secrets Data octets, written back verbatim in place of + ``nwk_key`` and ``pan_id``, as the octets of ``secrets`` are. **kwargs: Arbitrary keyword arguments. Returns: - Constructed option schema. + Constructed secrets schema. - """ - if self._type != Enum_BlockType.Name_Resolution_Block: - raise ProtocolError(f'PCAP-NG: [ns_dnsname] option must be in Name Resolution Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [ns_dnsname] option must be only one, ' - f'not {self._opt[type] + 1}') + Raises: + ProtocolError: If ``nwk_key`` is not 16 octets. - if option is not None: - name = option.name + .. versionchanged:: 1.6.0 - return Schema_NS_DNSNameOption( - type=type, - length=_utf8_length(name), - name=name, + * Takes the Secrets Data octets as ``data``, so that the :obj:`dict` + of a parsed model rebuilds the key as captured, as the model itself + does, rather than as its zero-filled fields (:issue:`2698`). + * ``nwk_key`` of any other width, empty included, raises + :exc:`~pcapkit.utilities.exceptions.ProtocolError` rather than + being padded or cut to the 16-octet key, and defaults to 16 zero + octets where it defaulted to empty (:issue:`2676`). + + """ + if secrets is not None: + data = secrets.get('data') + if isinstance(data, bytes): + return zigbee_key_held(Schema_ZigBeeNWKKey, data) + if secrets is not None: + nwk_key = secrets.nwk_key + pan_id = secrets.pan_id + + return Schema_ZigBeeNWKKey( + key=self._make_octets(nwk_key, size=16, name='[ZigBee NWK Key] key', alias='PCAP-NG'), + panid=pan_id, ) - def _make_option_ns_dnsipv4(self, type: 'Enum_OptionType', option: 'Optional[Data_NS_DNSIP4AddrOption]' = None, *, - ip: 'str | bytes | IPv4Address | int' = '8.8.8.8', - **kwargs: 'Any') -> 'Schema_NS_DNSIP4AddrOption': - """Make PCAP-NG ``ns_dnsIP4addr`` option. + def _check_block_floor(self, length: 'Optional[int]') -> 'None': + """Reject a tail too short to hold any block at all. Args: - type: Option type. - option: Option data model. - ip: DNS server IPv4 address. - **kwargs: Arbitrary keyword arguments. + length: Length of packet data, as the caller declared it, or + :obj:`None` to measure what is left in + :attr:`self._file `. - Returns: - Constructed option schema. + Raises: + StreamEOFError: If fewer than twelve octets are left to read. + + A PCAP-NG block is twelve octets at its smallest -- Block Type, Block + Total Length and the trailing copy of it, over an empty body -- which is + what :meth:`__length_hint__` reports. A tail shorter than that is not a + block, so parsing one out of it can only invent fields from + :meth:`FieldBase.unpack `'s + zero padding, and once the four octets of :attr:`PCAPNG.type + ` have been padded out + of nothing ``__length__`` is negative -- which + :func:`~pcapkit.protocols.misc.pcapng.schema.pcapng_block_selector` hands + to :class:`~pcapkit.corekit.fields.misc.SchemaField`, where + :meth:`io.RawIOBase.read` raises the bare ``ValueError`` of :issue:`678`. + + Reporting the end of the stream instead is what keeps the :issue:`431` accommodation: + :exc:`~pcapkit.utilities.exceptions.StreamEOFError` is an + :exc:`EOFError`, which :meth:`Extractor.record_frames + ` already catches + to stop with the frames read so far, so -- unlike a refusal inside + :meth:`FieldBase.unpack `, + which has no catch point above it and would abort the whole extraction -- + this costs no frame that was actually in the file. It is the same signal + :func:`~pcapkit.corekit.decorators.prepare` raises for a *derived* + remainder of exactly zero; a remainder of one, two or three octets + passes that check and is caught here instead. + + Note: + This runs on both of :meth:`unpack`'s paths. On the parsing path the + tail is what the file has left to read; on the construction path it is + the octets of a block handed to :meth:`make` as raw :obj:`bytes`, which + :meth:`unpack` parses off the buffer :meth:`pack` has just filled + (:issue:`2354`) -- a caller's octets are no more trustworthy than a + file's. Octets too short for this floor are therefore refused here + rather than by :meth:`read`'s own Block Total Length check, which sees + only what gets past the floor and so names an impossible *declared* + length, never a tail too short to declare one. + + Measuring an undeclared length needs the stream to seek, and nothing + guards that here because :meth:`__post_init__` has already called + :meth:`~io.IOBase.tell` on it to record ``_seek_set``: a stream that + could not seek never reaches this method. """ - if self._type != Enum_BlockType.Name_Resolution_Block: - raise ProtocolError(f'PCAP-NG: [ns_dnsIP4addr] option must be in Name Resolution Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [ns_dnsIP4addr] option must be only one, ' - f'not {self._opt[type] + 1}') + if length is None: + current = self._file.tell() + length = self._file.seek(0, io.SEEK_END) - current + self._file.seek(current, io.SEEK_SET) + + if length < GENERIC_BLOCK_MINIMUM: + raise StreamEOFError(f'PCAP-NG: block truncated: {length} octet(s) left, ' + f'fewer than the {GENERIC_BLOCK_MINIMUM} a block needs', quiet=True) + + def _get_payload(self) -> 'bytes': + """Get payload of :attr:`self.__header__ `. - if option is not None: - ip = option.ip + Returns: + Payload of :attr:`self.__header__ ` as :obj:`bytes`. - return Schema_NS_DNSIP4AddrOption( - type=type, - length=4, - ip=ip, - ) + See Also: + This is a wrapper function for :meth:`pcapkit.protocols.schema.Schema.get_payload`. - def _make_option_ns_dnsipv6(self, type: 'Enum_OptionType', option: 'Optional[Data_NS_DNSIP6AddrOption]' = None, *, - ip: 'str | bytes | IPv6Address | int' = '2001:4860:4860::8888', - **kwargs: 'Any') -> 'Schema_NS_DNSIP6AddrOption': - """Make PCAP-NG ``ns_dnsIP6addr`` option. + Note: + A packet block kept as captured has no payload (see + :meth:`_read_block_short`). + + """ + if isinstance(self.__header__.block, Schema_UnknownBlock): + return b'' + return self.__header__.block.get_payload() + + @staticmethod + def _get_payload_name(block: 'Schema_BlockType | bytes') -> 'Optional[str]': + """Get the name of the field carrying a block's captured packet octets. Args: - type: Option type. - option: Option data model. - ip: DNS server IPv6 address. - **kwargs: Arbitrary keyword arguments. + block: Parsed block schema. Returns: - Constructed option schema. + The name of the block schema's payload field, or :obj:`None` when the + block declares none -- which is every block type outside + :attr:`PACKET_TYPES `, and also a block whose + body was handed over as raw :obj:`bytes` at construction time rather + than as a schema. """ - if self._type != Enum_BlockType.Name_Resolution_Block: - raise ProtocolError(f'PCAP-NG: [ns_dnsIP6addr] option must be in Name Resolution Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [ns_dnsIP6addr] option must be only one, ' - f'not {self._opt[type] + 1}') + name = getattr(block, '__payload__', None) + if name is None or name not in getattr(block, '__fields__', {}): + return None + return name - if option is not None: - ip = option.ip + @staticmethod + def _get_local_timezone() -> 'timezone': + """Get local timezone. - return Schema_NS_DNSIP6AddrOption( - type=type, - length=16, - ip=ip, - ) + Warning: + Not used when reconstructing a block timestamp, and must not be: + a PCAP-NG timestamp is an offset from the UNIX epoch, so mixing the + *reading* host's zone into it makes one file parse to different + instants on different machines. + :meth:`self._get_timezone <_get_timezone>` returns + :attr:`datetime.timezone.utc` instead when the capture names no + ``if_tzone``. - def _make_option_isb_starttime(self, type: 'Enum_OptionType', option: 'Optional[Data_ISB_StartTimeOption]' = None, *, - timestamp: 'Optional[int | float | dt_type | Decimal]' = None, - **kwargs: 'Any') -> 'Schema_ISB_StartTimeOption': - """Make PCAP-NG ``isb_starttime`` option. + It also answers for *today* rather than for the capture's own + instant -- ``America/New_York`` reports ``-04:00`` all year, so a + December timestamp would get EDT's offset and not EST's -- which is + a second reason it cannot stand in for a capture's timezone. + + """ + tzinfo = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo + if tzinfo is None: + return datetime.timezone.utc + return cast('timezone', tzinfo) + + def _get_interface(self, interface_id: 'int') -> 'Data_InterfaceDescriptionBlock': + """Interface description that ``interface_id`` names. Args: - type: Option type. - option: Option data model. - timestamp: Start time. - **kwargs: Arbitrary keyword arguments. + interface_id: Interface ID that the current block associates with. Returns: - Constructed option schema. + Interface Description Block (IDB) of the current section that + ``interface_id`` identifies. - """ - if self._type != Enum_BlockType.Interface_Statistics_Block: - raise ProtocolError(f'PCAP-NG: [isb_starttime] option must be in Interface Statistics Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [isb_starttime] option must be only one, ' - f'not {self._opt[type] + 1}') + Raises: + FormatError: If the current section describes no such interface. - if option is not None: - timestamp = option.timestamp_epoch - ts_high, ts_low = self._make_timestamp(timestamp, interface_id=self._isb_interface_id) + Note: + The PCAP-NG specification requires a block's interface ID to name an + Interface Description Block of the block's own section, so an ID that + names none is a malformed file rather than a programming error -- + hence :exc:`~pcapkit.utilities.exceptions.FormatError` rather than + the bare :exc:`IndexError` that indexing the list would raise. - return Schema_ISB_StartTimeOption( - type=type, - length=8, - timestamp_high=ts_high, - timestamp_low=ts_low, - ) + """ + interfaces = self._ctx.interfaces + if not 0 <= interface_id < len(interfaces): + tag = self.INTERFACE_ID_BLOCK_TAGS.get(self._type, f'Block {self._type}') + raise FormatError(f'PCAP-NG: [{tag}] invalid interface ID: {interface_id}') + return interfaces[interface_id] - def _make_option_isb_endtime(self, type: 'Enum_OptionType', option: 'Optional[Data_ISB_EndTimeOption]' = None, *, - timestamp: 'Optional[int | float | dt_type | Decimal]' = None, - **kwargs: 'Any') -> 'Schema_ISB_EndTimeOption': - """Make PCAP-NG ``isb_endtime`` option. + def _get_resolution(self, interface_id: 'int' = 0) -> 'int': + """Timestamp resolution of the current block, in units per second. Args: - type: Option type. - option: Option data model. - timestamp: End time. - **kwargs: Arbitrary keyword arguments. + interface_id: Interface ID that the current block associates with. Returns: - Constructed option schema. + Timestamp resolution of the current block, in units per second. """ - if self._type != Enum_BlockType.Interface_Statistics_Block: - raise ProtocolError(f'PCAP-NG: [isb_endtime] option must be in Interface Statistics Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [isb_endtime] option must be only one, ' - f'not {self._opt[type] + 1}') - - if option is not None: - timestamp = option.timestamp_epoch - ts_high, ts_low = self._make_timestamp(timestamp, interface_id=self._isb_interface_id) + if self._ctx is None: + warn(f"'{self.__class__.__name__}' object has no attribute '_get_resolution'", + AttributeWarning, stacklevel=stacklevel()) + return DEFAULT_RESOLUTION - return Schema_ISB_EndTimeOption( - type=type, - length=8, - timestamp_high=ts_high, - timestamp_low=ts_low, - ) + options = self._get_interface(interface_id).options + tsresol = cast('Optional[Data_IF_TSResolOption]', + options.get(Enum_OptionType.if_tsresol)) + if tsresol is None: + return DEFAULT_RESOLUTION + return tsresol.resolution - def _make_option_isb_ifrecv(self, type: 'Enum_OptionType', option: 'Optional[Data_ISB_IFRecvOption]' = None, *, - packets: 'int' = 0, - **kwargs: 'Any') -> 'Schema_ISB_IFRecvOption': - """Make PCAP-NG ``isb_ifrecv`` option. + def _get_offset(self, interface_id: 'int' = 0) -> 'int': + """Timestamp offset of the current block, in seconds. Args: - type: Option type. - option: Option data model. - packets: Number of received packets. - **kwargs: Arbitrary keyword arguments. + interface_id: Interface ID that the current block associates with. Returns: - Constructed option schema. + Timestamp offset of the current block, in seconds. """ - if self._type != Enum_BlockType.Interface_Statistics_Block: - raise ProtocolError(f'PCAP-NG: [isb_ifrecv] option must be in Interface Statistics Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [isb_ifrecv] option must be only one, ' - f'not {self._opt[type] + 1}') - - if option is not None: - packets = option.packets + if self._ctx is None: + warn(f"'{self.__class__.__name__}' object has no attribute '_get_offset'", + AttributeWarning, stacklevel=stacklevel()) + return 0 - return Schema_ISB_IFRecvOption( - type=type, - length=8, - packets=packets, - ) + options = self._get_interface(interface_id).options + tsoffset = cast('Optional[Data_IF_TSOffsetOption]', + options.get(Enum_OptionType.if_tsoffset)) + if tsoffset is None: + return 0 + return tsoffset.offset - def _make_option_isb_ifdrop(self, type: 'Enum_OptionType', option: 'Optional[Data_ISB_IFDropOption]' = None, *, - packets: 'int' = 0, - **kwargs: 'Any') -> 'Schema_ISB_IFDropOption': - """Make PCAP-NG ``isb_ifdrop`` option. + def _get_timezone(self, interface_id: 'int' = 0) -> 'timezone': + """Timezone of the current block. Args: - type: Option type. - option: Option data model. - packets: Number of dropped packets. - **kwargs: Arbitrary keyword arguments. + interface_id: Interface ID that the current block associates with. Returns: - Constructed option schema. + Timezone of the current block. """ - if self._type != Enum_BlockType.Interface_Statistics_Block: - raise ProtocolError(f'PCAP-NG: [isb_ifdrop] option must be in Interface Statistics Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [isb_ifdrop] option must be only one, ' - f'not {self._opt[type] + 1}') - - if option is not None: - packets = option.packets - - return Schema_ISB_IFDropOption( - type=type, - length=8, - packets=packets, - ) + # NOTE: UTC, not the host's timezone, is the default in both branches + # below. A capture carries no timezone of its own unless it says so, and + # draft-ietf-opsawg-pcapng-02 section 4.3, Enhanced Packet Block, + # defines the block timestamp as an offset from 1970-01-01 00:00:00 UTC + # with no timezone term at all, so substituting the *reading* machine's + # zone would make the same file parse to different instants on different + # hosts. ``_get_resolution`` + # and ``_get_offset`` fall back to the format's own defaults in this + # situation; this is the matching one. + if self._ctx is None: + warn(f"'{self.__class__.__name__}' object has no attribute '_get_timezone'", + AttributeWarning, stacklevel=stacklevel()) + return datetime.timezone.utc - def _make_option_isb_filteraccept(self, type: 'Enum_OptionType', option: 'Optional[Data_ISB_FilterAcceptOption]' = None, *, - packets: 'int' = 0, - **kwargs: 'Any') -> 'Schema_ISB_FilterAcceptOption': - """Make PCAP-NG ``isb_filteraccept`` option. + options = self._get_interface(interface_id).options + tzone = cast('Optional[Data_IF_TZoneOption]', + options.get(Enum_OptionType.if_tzone)) + # an ``if_tzone`` kept as captured (a :class:`~datetime.timedelta` of 24 + # hours or more) names no zone a datetime can be rendered in + if tzone is None or not isinstance(tzone.timezone, datetime.timezone): + return datetime.timezone.utc + return tzone.timezone - Args: - type: Option type. - option: Option data model. - packets: Number of packets accepted by the filter. - **kwargs: Arbitrary keyword arguments. + def _get_linktype(self, interface_id: 'int' = 0) -> 'Enum_LinkType': + """Data link layer protocol type. - Returns: - Constructed option schema. + Args: + interface_id: Interface ID that the current block associates with. - """ - if self._type != Enum_BlockType.Interface_Statistics_Block: - raise ProtocolError(f'PCAP-NG: [isb_filteraccept] option must be in Interface Statistics Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [isb_filteraccept] option must be only one, ' - f'not {self._opt[type] + 1}') + Returns: + Data link layer protocol type. - if option is not None: - packets = option.packets + Raises: + UnsupportedCall: If current block is not a valid packet block, i.e., + EPB, SPB or obsolete Packet Block. - return Schema_ISB_FilterAcceptOption( - type=type, - length=8, - packets=packets, - ) + """ + if self._ctx is None or self._type not in self.PACKET_TYPES: + raise UnsupportedCall(f"'{self.__class__.__name__}' object has no attribute '_get_linktype'") + return self._get_interface(interface_id).linktype - def _make_option_isb_osdrop(self, type: 'Enum_OptionType', option: 'Optional[Data_ISB_OSDropOption]' = None, *, - packets: 'int' = 0, - **kwargs: 'Any') -> 'Schema_ISB_OSDropOption': - """Make PCAP-NG ``isb_osdrop`` option. + def _read_timestamp(self, timestamp_high: 'int', timestamp_low: 'int', *, + interface_id: 'int' = 0) -> 'tuple[dt_type, Decimal]': + """Read timestamp. Args: - type: Option type. - option: Option data model. - packets: Number of packets dropped by the OS. - **kwargs: Arbitrary keyword arguments. + timestamp_high: Higher 32-bit integer value of timestamp. + timestamp_low: Lower 32-bit integer value of timestamp. + interface_id: Interface ID that the current block associates with. Returns: - Constructed option schema. + Tuple of timestamp in :class:`~datetime.datetime` object with + timezone information and :class:`decimal.Decimal` object since + UNIX-Epoch in UTC timezone. """ - if self._type != Enum_BlockType.Interface_Statistics_Block: - raise ProtocolError(f'PCAP-NG: [isb_osdrop] option must be in Interface Statistics Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [isb_osdrop] option must be only one, ' - f'not {self._opt[type] + 1}') + tzone = self._get_timezone(interface_id) - if option is not None: - packets = option.packets + timestamp_raw = (timestamp_high << 32) | timestamp_low + with localcontext(prec=64): + # NOTE: This is the UTC epoch the docstring above promises, with no + # ``tzone.utcoffset(None)`` added. The block + # timestamp is an offset from the UNIX epoch, so it is + # timezone-independent by definition (draft-ietf-opsawg-pcapng-02 + # section 4.3, Enhanced Packet Block, spells out the arithmetic and + # has no timezone term), and the same draft's section 4.2, Interface + # Description Block, says of ``if_tzone`` that it "SHOULD NOT be used" + # and of ``if_tsoffset`` that it is "not intended to be used as an + # offset between local time and UTC". A shifted epoch would also + # contradict ``ts_datetime`` below, which is built from the + # unshifted value. + timestamp_epoch = decimal.Decimal(timestamp_raw) / self._get_resolution(interface_id) + \ + self._get_offset(interface_id) - return Schema_ISB_OSDropOption( - type=type, - length=8, - packets=packets, - ) + ts_ratio = timestamp_epoch.as_integer_ratio() + try: + # NOTE: ``tzone`` survives only here, as the zone the instant is + # *rendered* in -- which is all a ``tzinfo`` can affect on an aware + # datetime. It never moves the instant itself. Not the stdlib's + # ``fromtimestamp``, whose range is the C library's: Windows' takes + # nothing before 1970 or after 3000 (#2099). + ts_datetime = fromtimestamp(ts_ratio[0] / ts_ratio[1], tzone) + # an epoch past what ``datetime`` or a 64-bit ``time_t`` holds, e.g. + # from an extreme ``if_tsoffset``, raises one of these + except (ValueError, OverflowError, OSError): + warn(f'PCAP-NG: [Block {self._type}] invalid timestamp: {timestamp_epoch}', + ProtocolWarning, stacklevel=stacklevel()) + ts_datetime = datetime.datetime.fromtimestamp(0, datetime.timezone.utc) - def _make_option_isb_usrdeliv(self, type: 'Enum_OptionType', option: 'Optional[Data_ISB_UsrDelivOption]' = None, *, - packets: 'int' = 0, - **kwargs: 'Any') -> 'Schema_ISB_UsrDelivOption': - """Make PCAP-NG ``isb_usrdeliv`` option. + return (ts_datetime, timestamp_epoch) + + @staticmethod + def _block_area(block: 'Data_PCAPNG', name: 'str') -> 'Any': + """Option or record area of a parsed block, for its ``make``. Args: - type: Option type. - option: Option data model. - packets: Number of packets delivered to the user. - **kwargs: Arbitrary keyword arguments. + block: Block data model. + name: ``'options'`` or ``'records'``. Returns: - Constructed option schema. + The area's octets if :meth:`read` kept it as captured, as + ``_raw``; otherwise the parsed ````. """ - if self._type != Enum_BlockType.Interface_Statistics_Block: - raise ProtocolError(f'PCAP-NG: [isb_usrdeliv] option must be in Interface Statistics Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [isb_usrdeliv] option must be only one, ' - f'not {self._opt[type] + 1}') + return getattr(block, f'{name}_raw', getattr(block, name)) - if option is not None: - packets = option.packets + @staticmethod + def _read_padding(schema: 'Schema', data: 'Any', name: 'str' = 'padding', + key: 'Optional[str]' = None, *, nonzero: 'bool' = True) -> 'None': + """Keep a padding field of a parsed schema on its data model. - return Schema_ISB_UsrDelivOption( - type=type, - length=8, - packets=packets, - ) + Args: + schema: Parsed block, option or record schema. + data: Its data model, updated in place. + name: Name of the padding field on ``schema``. + key: Name to keep it as on ``data``; defaults to ``name``. + nonzero: Keep the octets only when they are not all zeros -- the + form a fresh build writes -- rather than whenever there are any. - def _make_option_pack_flags(self, type: 'Enum_OptionType', option: 'Optional[Data_PACK_FlagsOption]' = None, *, - direction: 'PacketDirection | int' = PacketDirection.UNKNOWN, - reception: 'PacketReception | int' = PacketReception.UNKNOWN, - fcs_len: 'int' = 0, - checksum_not_ready: 'bool' = False, - checksum_valid: 'bool' = False, - tcp_segmentation_offloaded: 'bool' = False, - reserved: 'int' = 0, - crc_error: 'bool' = False, - too_long: 'bool' = False, - too_short: 'bool' = False, - gap_error: 'bool' = False, - unaligned_error: 'bool' = False, - delimiter_error: 'bool' = False, - preamble_error: 'bool' = False, - symbol_error: 'bool' = False, - **kwargs: 'Any') -> 'Schema_PACK_FlagsOption': - """Make PCAP-NG ``pack_flags`` option. + The octets are kept as ``key`` only when there is something a rebuild + would otherwise lose, so the data model of well-formed input gains no + key (:issue:`1437`). :meth:`_make_padding` writes them back. + + """ + padding = schema.__dict__.get(name) + if isinstance(padding, bytes) and (any(padding) if nonzero else padding): + data.__update__([(key or name, padding)]) + + @staticmethod + def _make_padding(schema: '_S', source: 'Any', name: 'str' = 'padding', + key: 'Optional[str]' = None) -> '_S': + """Write the padding octets :meth:`_read_padding` kept back onto a schema. Args: - type: Option type. - option: Option data model. - direction: Packet direction. - reception: Packet reception. - fcs_len: Length of FCS field, in bytes. - checksum_not_ready: Whether the checksum is not ready. - checksum_valid: Whether the checksum is valid. - tcp_segmentation_offloaded: Whether TCP segmentation was offloaded. - reserved: Bits 12 to 23 of the flags word, verbatim. - crc_error: Whether CRC error occurred. - too_long: Whether packet is too long. - too_short: Whether packet is too short. - gap_error: Whether gap error occurred. - unaligned_error: Whether unaligned error occurred. - delimiter_error: Whether delimiter error occurred. - preamble_error: Whether preamble error occurred. - symbol_error: Whether symbol error occurred. - **kwargs: Arbitrary keyword arguments. + schema: Constructed block, option or record schema, updated in place. + source: Data model or keyword arguments it was made from, if any. + name: Name of the padding field on ``schema``. + key: Name the octets are kept as on ``source``; defaults to ``name``. Returns: - Constructed option schema. - - Raises: - ProtocolError: If ``direction`` or ``reception`` is a name, a - :obj:`bool`, or a member or code its 2-bit or 3-bit sub-field - cannot carry. + The ``schema`` given. Without kept octets its padding stays zeros. """ - if self._type != Enum_BlockType.Packet_Block: - raise ProtocolError(f'PCAP-NG: [pack_flags] option must be in Packet Block, ' - f'not {_block_type_name(self._type)}') - if self._opt[type] > 0: - raise ProtocolError(f'PCAP-NG: [pack_flags] option must be only one, ' - f'not {self._opt[type] + 1}') - - if option is not None: - direction_val = option.direction - reception_val = option.reception - fcs_len = option.fcs_len - checksum_not_ready = option.checksum_not_ready - checksum_valid = option.checksum_valid - tcp_segmentation_offloaded = option.tcp_segmentation_offloaded - reserved = option.reserved - crc_error = option.crc_error - too_long = option.too_long - too_short = option.too_short - gap_error = option.gap_error - unaligned_error = option.unaligned_error - delimiter_error = option.delimiter_error - preamble_error = option.preamble_error - symbol_error = option.symbol_error - else: - direction_val = self._make_index(direction, PacketDirection, bits=2, - name='[pack_flags] direction', alias='PCAP-NG') - reception_val = self._make_index(reception, PacketReception, bits=3, - name='[pack_flags] reception', alias='PCAP-NG') - - return Schema_PACK_FlagsOption( - type=type, - length=4, - flags={ - 'direction': getattr(direction_val, 'value', direction_val), - 'reception': getattr(reception_val, 'value', reception_val), - 'fcs_len': fcs_len, - 'checksum_not_ready': int(checksum_not_ready), - 'checksum_valid': int(checksum_valid), - 'tcp_segmentation_offloaded': int(tcp_segmentation_offloaded), - 'reserved': reserved, - 'crc_error': int(crc_error), - 'too_long': int(too_long), - 'too_short': int(too_short), - 'gap_error': int(gap_error), - 'unaligned_error': int(unaligned_error), - 'delimiter_error': int(delimiter_error), - 'preamble_error': int(preamble_error), - 'symbol_error': int(symbol_error), - }, - ) + if source is None or isinstance(schema, bytes): + return schema + padding = source.get(key or name) + if isinstance(padding, (bytes, bytearray)) and name in schema.__fields__: + setattr(schema, name, bytes(padding)) + return schema - def _make_option_pack_hash(self, type: 'Enum_OptionType', option: 'Optional[Data_PACK_HashOption]' = None, *, - algorithm: 'Enum_HashAlgorithm | int' = Enum_HashAlgorithm.two_s_complement, - hash: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_PACK_HashOption': - """Make PCAP-NG ``pack_hash`` option. + def _make_options_trailer(self, block: 'Optional[Data_PCAPNG]', + options: 'list[Schema_Option | bytes]') -> 'int': + """Append the octets a parsed block held past its ``opt_endofopt``. Args: - type: Option type. - option: Option data model. - algorithm: Hash algorithm. - hash: Hash value. - **kwargs: Arbitrary keyword arguments. + block: Block data model, if any. + options: Constructed options, extended in place. Returns: - Constructed option schema. + Number of octets appended. - Raises: - ProtocolError: If ``algorithm`` is a name, a :obj:`bool`, or a - member or code the 8-bit hash algorithm field cannot carry. + An option area ends at its ``opt_endofopt`` and the octets after it are + read as padding. :meth:`_read_padding` keeps them as ``padding_opts``, + and they are written back after the options, verbatim (:issue:`1437`). """ - if self._type != Enum_BlockType.Packet_Block: - raise ProtocolError(f'PCAP-NG: [pack_hash] option must be in Packet Block, ' - f'not {_block_type_name(self._type)}') + trailer = block.get('padding_opts') if block is not None else None + if not isinstance(trailer, bytes) or not trailer: + return 0 + options.append(trailer) + return len(trailer) - if option is not None: - algo_val = option.algorithm - hash = option.hash - else: - algo_val = self._make_index(algorithm, Enum_HashAlgorithm, bits=8, - name='[pack_hash] algorithm', alias='PCAP-NG') + @classmethod + def _make_packet_data(cls, block: 'Data_PCAPNG') -> 'bytes | ProtocolBase': + """Create the packet data of an EPB, SPB or PB from its data model. - return Schema_PACK_HashOption( - type=type, - length=1 + len(hash), - func=algo_val, - data=hash, - ) + Args: + block: Block data model. - def _make_nrb_records(self, records: 'Record | list[Schema_NameResolutionRecord | tuple[Enum_RecordType | int, dict[str, Any]] | bytes] | bytes') -> 'tuple[list[Schema_NameResolutionRecord | bytes], int]': # pylint: disable=line-too-long - """Make name resolution records for PCAP-NG. + Returns: + The captured packet octets the block was parsed from, as + :attr:`~pcapkit.protocols.data.Protocol.packet` holds them, + cut to the block's ``captured_len``; or, for a data model that + carries none, the payload rebuilt from its decoded next layer. + + Note: + The cut is what keeps a block with ``captured_len`` of zero intact: + its ``packet`` can hold octets past the payload (:issue:`1275`), and + writing those back would grow the block. + + The fallback is lossy. A next layer rebuilt from its own data model + drops anything it did not decode, such as Ethernet pad octets, so + the block it yields can be shorter than its ``captured_len`` says. + + """ + packet = block.get('packet') + if isinstance(packet, bytes): + return packet[:block.captured_len] + return cls._make_payload(block) + + def _make_timestamp(self, timestamp: 'Optional[float | Decimal | dt_type | int]' = None, *, + interface_id: 'int' = 0) -> 'tuple[int, int]': + """Make timestamp. Args: - records: PCAP-NG name resolution records. + timestamp: Timestamp in seconds since UNIX-Epoch, in UTC, i.e. as + :meth:`self._read_timestamp <_read_timestamp>` returns it. + interface_id: Interface ID that the current block associates with. Returns: - Tuple of name resolution records and total length of the records. + Tuple of timestamp in higher and lower 32-bit integer value, + scaled by the interface's ``if_tsresol`` and shifted by its + ``if_tsoffset``. - Raises: - ProtocolError: If a record's type is a name, a :obj:`bool`, or a - code the 16-bit record type field cannot carry - (:issue:`2594`), or if a record given as raw octets is shorter - than that field (:issue:`2723`). + Notes: + No timezone conversion happens here, and none should: the field this + builds is defined as an offset from the UNIX epoch, and the read + side applies no timezone shift for it to mirror. - """ - # a record area kept as captured is written back verbatim - if isinstance(records, bytes): - return [records], len(records) + Which clock a defaulted ``timestamp`` is read to follows the + resolution of the interface ``interface_id`` names, not + :attr:`self.nanosecond `. That property resolves its + interface through the parsed data model, which a block being made + has not got, so reading it here would make a ``make`` that names + no ``timestamp`` -- of an Enhanced Packet Block, an obsolete + Packet Block or an Interface Statistics Block -- raise + :exc:`AttributeError` on ``_info`` (:issue:`2362`). - has_record_end = False - total_length = 0 - if isinstance(records, list): - records_list = [] # type: list[Schema_NameResolutionRecord | bytes] - for schema in records: - if isinstance(schema, bytes): - # NOTE: Octets short of the type field would read as type 0 - # and be taken for an nrb_record_end (#2723). - if len(schema) < 2: - raise ProtocolError(f'PCAP-NG: invalid record: {schema!r} holds no whole record type') - code = Enum_RecordType.get(int.from_bytes(schema[0:2], self._byte, signed=False)) - if code == Enum_RecordType.nrb_record_end: # moved to the end, written once - has_record_end = True - continue + .. versionchanged:: 1.5.0 - data = schema # type: Schema_NameResolutionRecord | bytes - data_len = len(data) - elif isinstance(schema, Schema): - code = schema.type - if code == Enum_RecordType.nrb_record_end: # moved to the end, written once - has_record_end = True - continue + A defaulted ``timestamp`` no longer reads + :attr:`self.nanosecond `, so it no longer requires a + parsed data model (:issue:`2362`). - data = schema - data_len = len(schema.pack()) + """ + with localcontext(prec=64): + resolution = self._get_resolution(interface_id) + if timestamp is None: + if resolution > RESOLUTION[False]: + timestamp = decimal.Decimal(time.time_ns()) / 1_000_000_000 else: - kind, args = cast('tuple[Enum_RecordType | int, dict[str, Any]]', schema) - code = self._make_index(kind, Enum_RecordType, bits=16, name='record type', alias='PCAP-NG') - if code == Enum_RecordType.nrb_record_end: # moved to the end, written once - has_record_end = True - continue + timestamp = decimal.Decimal(time.time()) + else: + if isinstance(timestamp, datetime.datetime): + timestamp = totimestamp(timestamp) + timestamp = decimal.Decimal(timestamp) - name = self._lookup_registry(self.__record__, code) - if isinstance(name, str): - meth_name = f'_make_record_{name}' - meth = cast('RecordConstructor', - getattr(self, meth_name, self._make_record_unknown)) - else: - meth = name[1] + ts_info = int( + (timestamp - self._get_offset(interface_id)) * resolution + ) + return (ts_info >> 32) & 0xFFFF_FFFF, ts_info & 0xFFFF_FFFF - data = self._make_padding(meth(code, **args), args) - data_len = len(data.pack()) + def _make_mac_addr(self, addr: 'str | bytes | bytearray', *, size: 'Optional[int]' = 6, + name: 'str' = 'MAC address', alias: 'Optional[str]' = None) -> 'bytes': + """Make a MAC address, or an EUI-64 one with ``size=8``. - records_list.append(data) - total_length += data_len + Args: + addr: MAC or EUI address. + size: Octets in the address, as for :meth:`Protocol._make_mac_addr + `. + name: Name of the address, for the error message. + alias: Prefix of the error message, ``'PCAP-NG'`` by default. - if has_record_end: - nrb_record_end = self._make_record_end(Enum_RecordType.nrb_record_end) # type: ignore[arg-type] - total_length += len(nrb_record_end.pack()) - records_list.append(nrb_record_end) - return records_list, total_length + Returns: + The address packed as ``size`` octets, 6 by default. - records_list = [] - for key, record in records.items(multi=True): - code = self._make_index(key, Enum_RecordType, bits=16, name='record type', alias='PCAP-NG') - if code == Enum_RecordType.nrb_record_end: # moved to the end, written once - has_record_end = True - continue + Raises: + ProtocolError: If ``addr`` is not a :class:`str`, :class:`bytes` + or :class:`bytearray` (a :obj:`bool` included), or is not a + ``:``- or ``-``-separated *hex* encoded address of ``size`` + octets. - name = self._lookup_registry(self.__record__, code) - if isinstance(name, str): - meth_name = f'_make_record_{name}' - meth = cast('RecordConstructor', - getattr(self, meth_name, self._make_record_unknown)) - else: - meth = name[1] + """ + prefix = 'PCAP-NG' if alias is None else alias + return super()._make_mac_addr(addr, size=size, name=name, alias=prefix) - data = self._make_padding(meth(code, record), record) - data_len = len(data.pack()) + def _decode_next_layer(self, dict_: 'Data_PCAPNG', proto: 'Optional[int]' = None, + length: 'Optional[int]' = None, *, packet: 'Optional[dict[str, Any]]' = None) -> 'Data_PCAPNG': # pylint: disable=arguments-differ + r"""Decode next layer protocol. - records_list.append(data) - total_length += data_len + Arguments: + dict\_: info buffer + proto: next layer protocol index + length: valid (*non-padding*) length + packet: packet info (passed from :meth:`self.unpack `) - if has_record_end: - nrb_record_end = self._make_record_end(Enum_RecordType.nrb_record_end) # type: ignore[arg-type] - total_length += len(nrb_record_end.pack()) - records_list.append(nrb_record_end) - return records_list, total_length + Returns: + Current protocol with packet extracted. - def _make_record_unknown(self, type: 'Enum_RecordType', record: 'Optional[Data_UnknownRecord]' = None, *, - data: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_UnknownRecord': - """Make PCAP-NG unknown name resolution record. + Notes: + We added a new key ``__next_type__`` to ``dict_`` to store the + next layer protocol type, and a new key ``__next_name__`` to + store the next layer protocol name. :meth:`Info.to_dict + ` carries both, and the + dumpers leave them out. - Args: - type: Record type. - record: Record data model. - data: Record data. - **kwargs: Arbitrary keyword arguments. + We also added a new key ``protocols`` to ``dict_`` to store the + protocol chain of the current packet (frame). + + ``proto`` is dispatched as :meth:`_dispatch_linktype` says, so + ``LINKTYPE_RAW`` data is read as IPv4 or IPv6 (:issue:`1502`). + + """ + proto = self._dispatch_linktype(proto, length) + next_ = cast('ProtocolBase', self._import_next_layer(proto, length, packet=packet)) # type: ignore[misc,call-arg,redundant-cast] + info, chain = next_.info, next_.protochain + + # make next layer protocol name + layer = next_.info_name + # proto = next_.__class__.__name__ + + # write info and protocol chain into dict + dict_.__update__({ + layer: info, + 'protocols': chain.chain if chain else '', + '__next_type__': type(next_), + '__next_name__': layer, + }) + self._next = next_ # pylint: disable=attribute-defined-outside-init + self._protos = chain # pylint: disable=attribute-defined-outside-init + return dict_ + + def _dispatch_linktype(self, proto: 'Optional[int]', length: 'Optional[int]' = None) -> 'Optional[int]': + """Link type a packet block's data is dispatched under. + + Arguments: + proto: link type of the interface the block was captured on + length: valid (*non-padding*) length Returns: - Constructed record schema. + For :attr:`LinkType.RAW ` + data whose first octet carries IP version 4 or 6, + :attr:`LinkType.IPV4 ` or + :attr:`LinkType.IPV6 `; + otherwise ``proto``. + + Note: + As :meth:`Frame._dispatch_linktype + ` does + for PCAP: ``LINKTYPE_RAW`` (101) data is IPv4 or IPv6 by its version + nibble, an entry registered under ``RAW`` itself is used as it + stands, and data of any other version is kept as + :class:`~pcapkit.protocols.misc.raw.protocol.Raw` (:issue:`1502`). """ - if record is not None: - data = record.data + if proto != Enum_LinkType.RAW or Enum_LinkType.RAW in self.__proto__: + return proto + data = self._get_payload()[:length] + version = data[0] >> 4 if data else None + if version == 4: + return Enum_LinkType.IPV4 + if version == 6: + return Enum_LinkType.IPV6 + return proto - return Schema_UnknownRecord( - type=type, - length=len(data), - data=data, - ) + def _read_pcapng_options(self, options_schema: 'list[Schema_Option]') -> 'Option': + """Read PCAP-NG options. - def _make_record_end(self, type: 'Enum_RecordType', record: 'Optional[Data_EndRecord]' = None, - **kwargs: 'Any') -> 'Schema_EndRecord': - """Make PCAP-NG ``nrb_record_end`` name resolution record. + Structure of PCAP-NG option: + + .. code-block:: text + + 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Code | Option Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / Option Value / + / variable length, padded to 32 bits / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / / + / . . . other options . . . / + / / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Option Code == opt_endofopt | Option Length == 0 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Args: - type: Record type. - record: Record data model. - **kwargs: Arbitrary keyword arguments. + options_schema: Parsed PCAP-NG options. Returns: - Constructed record schema. + Parsed PCAP-NG options data. """ - return Schema_EndRecord( - type=type, - length=0, - ) + options = OrderedMultiDict() # type: Option - def _make_record_ipv4(self, type: 'Enum_RecordType', record: 'Optional[Data_IPv4Record]' = None, *, - ip: 'IPv4Address | str | bytes | int' = '127.0.0.1', - names: 'Optional[list[str]]' = None, - **kwargs: 'Any') -> 'Schema_IPv4Record': - """Make PCAP-NG ``nrb_record_ipv4`` name resolution record. + # an area kept as captured holds no options to read + if any(isinstance(schema, bytes) for schema in options_schema): + return options - Args: - type: Record type. - record: Record data model. - ip: IPv4 address. - names: Host names. - **kwargs: Arbitrary keyword arguments. + for schema in options_schema: + type = schema.type + name = self._lookup_registry(self.__option__, _option_key(type)) - Returns: - Constructed record schema. + if isinstance(name, str): + meth_name = f'_read_option_{name}' + meth = cast('OptionParser', + getattr(self, meth_name, self._read_option_unknown)) + else: + meth = name[0] + data = meth(schema, options=options) + # NOTE: The padding to 32 bits, if it is not all zeros (#1437). + self._read_padding(schema, data) - """ - if record is not None: - ip = record.ip - names = cast('list[str]', record.records) + # record option data + options.add(type, data) + self._opt[type] += 1 - if names is None: - names = [] - records = _join_names(names) + # break when ``opt_endofopt`` is reached + if type == Enum_OptionType.opt_endofopt: + break - return Schema_IPv4Record( - type=type, - length=4 + _utf8_length(records), - ip=ip, - resol=records, - ) + return options - def _make_record_ipv6(self, type: 'Enum_RecordType', record: 'Optional[Data_IPv6Record]' = None, *, - ip: 'IPv6Address | str | bytes | int' = '::1', - names: 'Optional[list[str]]' = None, - **kwargs: 'Any') -> 'Schema_IPv6Record': - """Make PCAP-NG ``nrb_record_ipv6`` name resolution record. + def _read_nrb_records(self, records_schema: 'list[Schema_NameResolutionRecord]') -> 'Record': + """Read PCAP-NG name resolution records. Args: - type: Record type. - record: Record data model. - ip: IPv6 address. - names: Host names. - **kwargs: Arbitrary keyword arguments. + records_schema: Parsed name resolution records. Returns: - Constructed record schema. + Parsed PCAP-NG name resolution records data. """ - if record is not None: - ip = record.ip - names = cast('list[str]', record.records) - - if names is None: - names = [] - records = _join_names(names) + records = OrderedMultiDict() # type: Record - return Schema_IPv6Record( - type=type, - length=16 + _utf8_length(records), - ip=ip, - resol=records, - ) + # an area kept as captured holds no records to read + if any(isinstance(schema, bytes) for schema in records_schema): + return records - def _make_secrets_unknown(self, type: 'Enum_SecretsType', secrets: 'Optional[Data_UnknownSecrets]' = None, *, - data: 'bytes' = b'', - **kwargs: 'Any') -> 'Schema_UnknownSecrets': - """Make PCAP-NG unknown secrets. + for schema in records_schema: + type = schema.type + name = self._lookup_registry(self.__record__, type) - Args: - type: Secrets type. - secrets: Secrets data model. - data: Secrets data. - **kwargs: Arbitrary keyword arguments. + if isinstance(name, str): + meth_name = f'_read_record_{name}' + meth = cast('RecordParser', + getattr(self, meth_name, self._read_record_unknown)) + else: + meth = name[0] + data = meth(schema, records=records) + # NOTE: The padding to 32 bits, if it is not all zeros (#1437). + self._read_padding(schema, data) - Returns: - Constructed secrets schema. + # record name resolution record data + records.add(type, data) - """ - if secrets is not None: - data = secrets.data + # break when ``nrb_record_end`` is reached + if type == Enum_RecordType.nrb_record_end: + break - return Schema_UnknownSecrets( - data=data, - ) + return records - def _make_secrets_tls(self, type: 'Enum_SecretsType', secrets: 'Optional[Data_TLSKeyLog]' = None, *, - entries: 'Optional[Mapping[Enum_TLSKeyLabel, OrderedMultiDict[bytes, bytes]]]' = None, - data: 'Optional[str]' = None, - **kwargs: 'Any') -> 'Schema_TLSKeyLog': - """Make PCAP-NG TLS Key Log secrets. + def _make_pcapng_options(self, options: 'Option | list[Schema_Option | tuple[Enum_OptionType | int, dict[str, Any]] | bytes] | bytes', # pylint: disable=line-too-long + namespace: 'str') -> 'tuple[list[Schema_Option | bytes], int]': + """Make options for PCAP-NG. Args: - type: Secrets type. - secrets: Secrets data model. The key log text it was parsed from, - if any, is written back verbatim; otherwise its entries are. - On a parsed model the text wins, so an edit to its - ``entries`` alone is silently ignored. - entries: TLS Key Log entries. - data: TLS key log text, written back verbatim in place of - ``entries``, as the text of ``secrets`` is. - **kwargs: Arbitrary keyword arguments. + options: PCAP-NG options. An option's code is a member, kept as + given, or an :class:`int`, resolved in ``namespace``. + namespace: Namespace of options. Returns: - Constructed secrets schema. - - .. versionchanged:: 1.6.0 + Tuple of options and total length of options. - Takes the key log text as ``data``, so that the :obj:`dict` of a - parsed model rebuilds the log as captured, as the model itself does, - rather than as its entries under a generated comment - (:issue:`2698`). + Raises: + ProtocolError: If an option's code is a name, a :obj:`bool`, or a + code the 16-bit option code field cannot carry + (:issue:`2594`), or if an option given as raw octets is + shorter than that field (:issue:`2723`). """ - from pcapkit import __version__ - - if secrets is not None: - data = secrets.get('data') - if isinstance(data, str): - return Schema_TLSKeyLog( - data=data, - ) - if secrets is not None: - # NOTE: each an OrderedMultiInfo, which is an OrderedMultiDict. - entries = cast('Mapping[Enum_TLSKeyLabel, OrderedMultiDict[bytes, bytes]]', secrets.entries) - - if entries is None: - entries = {} + # an option area kept as captured is written back verbatim + if isinstance(options, bytes): + return [options], len(options) - lines = [f'# generated by PyPCAPKit v{__version__}\n'] # type: list[str] - for label, entry in entries.items(): - for k, v in entry.items(multi=True): - lines.append(f'{label.name} {k.hex()} {v.hex()}\n') + has_endofopt = False + total_length = 0 + if isinstance(options, list): + options_list = [] # type: list[Schema_Option | bytes] + for schema in options: + if isinstance(schema, bytes): + # NOTE: Octets short of the code field would read as code 0 + # and be taken for an opt_endofopt (#2723). + if len(schema) < 2: + raise ProtocolError(f'PCAP-NG: invalid option: {schema!r} holds no whole option code') + code = Enum_OptionType.get(int.from_bytes(schema[0:2], self._byte, signed=False), + namespace=namespace) + if code == Enum_OptionType.opt_endofopt: # moved to the end, written once + has_endofopt = True + continue - return Schema_TLSKeyLog( - data=''.join(lines), - ) + data = schema # type: Schema_Option | bytes + data_len = len(data) + elif isinstance(schema, Schema): + code = schema.type + if code == Enum_OptionType.opt_endofopt: # moved to the end, written once + has_endofopt = True + continue - def _make_secrets_wireguard(self, type: 'Enum_SecretsType', secrets: 'Optional[Data_WireGuardKeyLog]' = None, *, - entries: 'Optional[OrderedMultiDict[WireGuardKeyLabel, bytes]]' = None, - data: 'Optional[str]' = None, - **kwargs: 'Any') -> 'Schema_WireGuardKeyLog': - """Make PCAP-NG WireGuard secrets. + data = schema + data_len = len(schema.pack()) + else: + kind, args = cast('tuple[Enum_OptionType | int, dict[str, Any]]', schema) + # NOTE: a member is a str, which ``_make_index`` refuses, and a code is + # resolved in the block's namespace, as a raw option's is. + code = kind if isinstance(kind, Enum_OptionType) else Enum_OptionType.get( + self._make_uint(kind, bits=16, name='option code', alias='PCAP-NG'), namespace=namespace) + if code == Enum_OptionType.opt_endofopt: # moved to the end, written once + has_endofopt = True + continue - Args: - type: Secrets type. - secrets: Secrets data model. The key log text it was parsed from, - if any, is written back verbatim; otherwise its entries are. - On a parsed model the text wins, so an edit to its - ``entries`` alone is silently ignored. - entries: WireGuard Key Log entries. - data: WireGuard key log text, written back verbatim in place of - ``entries``, as the text of ``secrets`` is. - **kwargs: Arbitrary keyword arguments. + name = self._lookup_registry(self.__option__, _option_key(code)) + if isinstance(name, str): + meth_name = f'_make_option_{name}' + meth = cast('OptionConstructor', + getattr(self, meth_name, self._make_option_unknown)) + else: + meth = name[1] - Returns: - Constructed secrets schema. + data = self._make_padding(meth(code, **args), args) + data_len = len(data.pack()) - .. versionchanged:: 1.6.0 + options_list.append(data) + total_length += data_len + self._opt[code] += 1 - Takes the key log text as ``data``, as - :meth:`_make_secrets_tls` does (:issue:`2698`). + if has_endofopt: + opt_endofopt = self._make_option_endofopt(Enum_OptionType.opt_endofopt) + total_length += len(opt_endofopt.pack()) + options_list.append(opt_endofopt) + return options_list, total_length - """ - from pcapkit import __version__ + options_list = [] + for key, option in options.items(multi=True): + code = key if isinstance(key, Enum_OptionType) else Enum_OptionType.get( + self._make_uint(key, bits=16, name='option code', alias='PCAP-NG'), namespace=namespace) + if code == Enum_OptionType.opt_endofopt: # moved to the end, written once + has_endofopt = True + continue - if secrets is not None: - data = secrets.get('data') - if isinstance(data, str): - return Schema_WireGuardKeyLog( - data=data, - ) - if secrets is not None: - entries = secrets.entries + name = self._lookup_registry(self.__option__, _option_key(code)) + if isinstance(name, str): + meth_name = f'_make_option_{name}' + meth = cast('OptionConstructor', + getattr(self, meth_name, self._make_option_unknown)) + else: + meth = name[1] - if entries is None: - entries = OrderedMultiDict() + data = self._make_padding(meth(code, option), option) + data_len = len(data.pack()) - lines = [f'# generated by PyPCAPKit v{__version__}\n'] # type: list[str] - for label, value in entries.items(multi=True): - lines.append(f'{label.name} = {base64.b64encode(value).decode()}\n') + options_list.append(data) + total_length += data_len + self._opt[code] += 1 - return Schema_WireGuardKeyLog( - data=''.join(lines), - ) + if has_endofopt: + opt_endofopt = self._make_option_endofopt(Enum_OptionType.opt_endofopt) + total_length += len(opt_endofopt.pack()) + options_list.append(opt_endofopt) + return options_list, total_length - def _make_secrets_zigbee_nwk(self, type: 'Enum_SecretsType', secrets: 'Optional[Data_ZigBeeNWKKey]' = None, *, - nwk_key: 'bytes' = bytes(16), - pan_id: 'int' = 0, - data: 'Optional[bytes]' = None, - **kwargs: 'Any') -> 'Schema_ZigBeeNWKKey': - """Make PCAP-NG ZigBee NWK Key secrets. + def _make_nrb_records(self, records: 'Record | list[Schema_NameResolutionRecord | tuple[Enum_RecordType | int, dict[str, Any]] | bytes] | bytes') -> 'tuple[list[Schema_NameResolutionRecord | bytes], int]': # pylint: disable=line-too-long + """Make name resolution records for PCAP-NG. Args: - type: Secrets type. - secrets: Secrets data model. The Secrets Data octets it was parsed - from, if any, are written back verbatim, so that a key shorter - or longer than its fixed width rebuilds as captured - (:issue:`1986`); otherwise its fields are. On a parsed model - the octets win, so an edit to its fields alone is silently - ignored. - nwk_key: NWK key, 16 octets. - pan_id: PAN ID. - data: Secrets Data octets, written back verbatim in place of - ``nwk_key`` and ``pan_id``, as the octets of ``secrets`` are. - **kwargs: Arbitrary keyword arguments. + records: PCAP-NG name resolution records. Returns: - Constructed secrets schema. + Tuple of name resolution records and total length of the records. Raises: - ProtocolError: If ``nwk_key`` is not 16 octets. + ProtocolError: If a record's type is a name, a :obj:`bool`, or a + code the 16-bit record type field cannot carry + (:issue:`2594`), or if a record given as raw octets is shorter + than that field (:issue:`2723`). - .. versionchanged:: 1.6.0 + """ + # a record area kept as captured is written back verbatim + if isinstance(records, bytes): + return [records], len(records) - * Takes the Secrets Data octets as ``data``, so that the :obj:`dict` - of a parsed model rebuilds the key as captured, as the model itself - does, rather than as its zero-filled fields (:issue:`2698`). - * ``nwk_key`` of any other width, empty included, raises - :exc:`~pcapkit.utilities.exceptions.ProtocolError` rather than - being padded or cut to the 16-octet key, and defaults to 16 zero - octets where it defaulted to empty (:issue:`2676`). + has_record_end = False + total_length = 0 + if isinstance(records, list): + records_list = [] # type: list[Schema_NameResolutionRecord | bytes] + for schema in records: + if isinstance(schema, bytes): + # NOTE: Octets short of the type field would read as type 0 + # and be taken for an nrb_record_end (#2723). + if len(schema) < 2: + raise ProtocolError(f'PCAP-NG: invalid record: {schema!r} holds no whole record type') + code = Enum_RecordType.get(int.from_bytes(schema[0:2], self._byte, signed=False)) + if code == Enum_RecordType.nrb_record_end: # moved to the end, written once + has_record_end = True + continue - """ - if secrets is not None: - data = secrets.get('data') - if isinstance(data, bytes): - return zigbee_key_held(Schema_ZigBeeNWKKey, data) - if secrets is not None: - nwk_key = secrets.nwk_key - pan_id = secrets.pan_id + data = schema # type: Schema_NameResolutionRecord | bytes + data_len = len(data) + elif isinstance(schema, Schema): + code = schema.type + if code == Enum_RecordType.nrb_record_end: # moved to the end, written once + has_record_end = True + continue - return Schema_ZigBeeNWKKey( - key=self._make_octets(nwk_key, size=16, name='[ZigBee NWK Key] key', alias='PCAP-NG'), - panid=pan_id, - ) + data = schema + data_len = len(schema.pack()) + else: + kind, args = cast('tuple[Enum_RecordType | int, dict[str, Any]]', schema) + code = self._make_index(kind, Enum_RecordType, bits=16, name='record type', alias='PCAP-NG') + if code == Enum_RecordType.nrb_record_end: # moved to the end, written once + has_record_end = True + continue - def _make_secrets_zigbee_aps(self, type: 'Enum_SecretsType', secrets: 'Optional[Data_ZigBeeAPSKey]' = None, *, - aps_key: 'bytes' = bytes(16), - pan_id: 'int' = 0, - short_address: 'int' = 0, - data: 'Optional[bytes]' = None, - **kwargs: 'Any') -> 'Schema_ZigBeeAPSKey': - """Make PCAP-NG ZigBee APS Key secrets. + name = self._lookup_registry(self.__record__, code) + if isinstance(name, str): + meth_name = f'_make_record_{name}' + meth = cast('RecordConstructor', + getattr(self, meth_name, self._make_record_unknown)) + else: + meth = name[1] - Args: - type: Secrets type. - secrets: Secrets data model. The Secrets Data octets it was parsed - from, if any, are written back verbatim, as for - :meth:`_make_secrets_zigbee_nwk`; otherwise its fields are. - aps_key: APS key, 16 octets. - pan_id: PAN ID. - short_address: Short address. - data: Secrets Data octets, written back verbatim in place of the - fields, as the octets of ``secrets`` are. - **kwargs: Arbitrary keyword arguments. + data = self._make_padding(meth(code, **args), args) + data_len = len(data.pack()) - Returns: - Constructed secrets schema. + records_list.append(data) + total_length += data_len - Raises: - ProtocolError: If ``aps_key`` is not 16 octets. + if has_record_end: + nrb_record_end = self._make_record_end(Enum_RecordType.nrb_record_end) # type: ignore[arg-type] + total_length += len(nrb_record_end.pack()) + records_list.append(nrb_record_end) + return records_list, total_length - .. versionchanged:: 1.6.0 + records_list = [] + for key, record in records.items(multi=True): + code = self._make_index(key, Enum_RecordType, bits=16, name='record type', alias='PCAP-NG') + if code == Enum_RecordType.nrb_record_end: # moved to the end, written once + has_record_end = True + continue - * Takes the Secrets Data octets as ``data``, as - :meth:`_make_secrets_zigbee_nwk` does (:issue:`2698`). - * ``aps_key`` of any other width, empty included, raises - :exc:`~pcapkit.utilities.exceptions.ProtocolError` rather than - being padded or cut to the 16-octet key, and defaults to 16 zero - octets where it defaulted to empty (:issue:`2676`). + name = self._lookup_registry(self.__record__, code) + if isinstance(name, str): + meth_name = f'_make_record_{name}' + meth = cast('RecordConstructor', + getattr(self, meth_name, self._make_record_unknown)) + else: + meth = name[1] - """ - if secrets is not None: - data = secrets.get('data') - if isinstance(data, bytes): - return zigbee_key_held(Schema_ZigBeeAPSKey, data) - if secrets is not None: - aps_key = secrets.aps_key - pan_id = secrets.pan_id - short_address = secrets.short_address + data = self._make_padding(meth(code, record), record) + data_len = len(data.pack()) - return Schema_ZigBeeAPSKey( - key=self._make_octets(aps_key, size=16, name='[ZigBee APS Key] key', alias='PCAP-NG'), - panid=pan_id, - addr_high=short_address >> 16, - addr_low=short_address & 0xFFFF, - ) + records_list.append(data) + total_length += data_len + + if has_record_end: + nrb_record_end = self._make_record_end(Enum_RecordType.nrb_record_end) # type: ignore[arg-type] + total_length += len(nrb_record_end.pack()) + records_list.append(nrb_record_end) + return records_list, total_length diff --git a/pcapkit/protocols/transport/sctp/protocol.py b/pcapkit/protocols/transport/sctp/protocol.py index af33ee17ed..2a50c48041 100644 --- a/pcapkit/protocols/transport/sctp/protocol.py +++ b/pcapkit/protocols/transport/sctp/protocol.py @@ -992,23 +992,6 @@ def _make_data(cls, data: 'Data_SCTP') -> 'dict[str, Any]': # type: ignore[over 'trailer': data.get('trailer', b''), } - def _get_payload(self) -> 'bytes': - """Get payload of the packet. - - SCTP has no payload field in its header schema -- user data travels - inside DATA chunks -- so this returns the user data of the *first* DATA - chunk found by :meth:`self.read `, which is also the chunk - whose payload protocol identifier selects the next layer. Should the - packet carry no DATA chunk, an empty :obj:`bytes` is returned and the - next layer resolves to - :class:`~pcapkit.protocols.misc.no_payload.protocol.NoPayload`. - - Returns: - Payload of the packet as :obj:`bytes`. - - """ - return self._payload - def _read_sctp_chunks(self) -> 'Chunks': """Read SCTP chunk list. @@ -1117,291 +1100,213 @@ def _decode_next_layer(self, dict_: 'Data_SCTP', proto: 'Optional[int]' = None, return ProtocolBase._decode_next_layer( # pylint: disable=protected-access self, dict_, proto, length, packet=packet) # type: ignore[arg-type] - def _make_sctp_chunk(self, code: 'Enum_Chunk', chunk: 'Optional[Data_Chunk]' = None, - **kwargs: 'Any') -> 'Schema_Chunk': - """Dispatch to the chunk constructor registered for ``code``. - - Args: - code: SCTP chunk type. - chunk: Chunk data, if constructing from a parsed data model. - **kwargs: Arbitrary keyword arguments for the constructor. - - Returns: - Constructed chunk schema. - - """ - name = self._lookup_registry(self.__chunk__, code) - if isinstance(name, str): - meth_name = f'_make_chunk_{name}' - meth = cast('ChunkConstructor', - getattr(self, meth_name, self._make_chunk_donone)) - else: - meth = name[1] - return _restore_padding(chunk, meth(code, chunk, **kwargs)) + def _read_chunk_donone(self, schema: 'Schema_UnknownChunk', *, chunks: 'Chunks') -> 'Data_UnknownChunk': # pylint: disable=unused-argument + """Read SCTP chunk of an unsupported type. - def _read_sctp_parameters(self, schemas: 'list[Schema_Parameter]') -> 'Parameters': - """Read SCTP chunk parameter list. + This is the fall-through for every chunk type :mod:`pcapkit` does not + implement -- unassigned, reserved, or defined by an SCTP extension -- + as well as for the SCTP-defined but reserved ECNE and CWR chunks. The + chunk's raw flags and value are recorded verbatim rather than raising, + so that a bundle containing an unknown chunk still parses. - Args: - schemas: Parsed parameter schemas. + Arguments: + schema: parsed chunk schema + chunks: extracted SCTP chunks Returns: - Extracted SCTP chunk parameters. + Parsed chunk data. """ - parameters = OrderedMultiDict() # type: Parameters + return Data_UnknownChunk( + type=schema.type, + length=schema.length, + flags=schema.flags, + value=schema.value, + ) - for schema in schemas: - code = schema.type - name = self._lookup_registry(self.__parameter__, code) + def _read_chunk_data(self, schema: 'Schema_DATAChunk', *, chunks: 'Chunks') -> 'Data_DATAChunk': # pylint: disable=unused-argument + """Read SCTP DATA chunk. - if isinstance(name, str): - meth_name = f'_read_param_{name}' - meth = cast('ParameterParser', - getattr(self, meth_name, self._read_param_donone)) - else: - meth = name[0] - parameters.add(code, _keep_padding(schema, meth(schema, parameters=parameters))) - return parameters + Structure of SCTP DATA chunk [:rfc:`9260#section-3.3.1`]: - def _make_sctp_parameters(self, parameters: 'list[Schema_Parameter | tuple[Enum_Parameter | int, dict[str, Any]] | bytes] | Parameters') -> 'list[Schema_Parameter | bytes]': # pylint: disable=line-too-long - """Make chunk parameters for SCTP. + .. code-block:: text - Args: - parameters: SCTP chunk parameters. + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type = 0 | Res |I|U|B|E| Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | TSN | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Stream Identifier S | Stream Sequence Number n | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Payload Protocol Identifier | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + \\ \\ + / User Data (seq n of Stream S) / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed chunk schema + chunks: extracted SCTP chunks Returns: - Constructed parameter schemas. + Parsed chunk data. Raises: - ProtocolError: If the type of a parameter, given in a ``(type, - kwargs)`` pair or as the key of a container, is a name, a - :obj:`bool` or a code its field cannot carry, or if a parameter - given as raw octets is empty (:issue:`2723`). + ProtocolError: If ``length`` is **NOT** greater than ``16``, since + :rfc:`9260#section-3.3.1` requires at least one byte of user + data. """ - parameters_list = [] # type: list[Schema_Parameter | bytes] - - if isinstance(parameters, list): - for schema in parameters: - if isinstance(schema, bytes) and not schema: - raise ProtocolError(f'{self.alias}: invalid parameter: {schema!r} holds no type field') - if isinstance(schema, (bytes, Schema)): - parameters_list.append(schema) - else: - kind, args = cast('tuple[Enum_Parameter | int, dict[str, Any]]', schema) - code = self._make_index(kind, Enum_Parameter, bits=16, name='parameter type') - parameters_list.append(self._make_sctp_parameter(code, None, **args)) - return _last_item_only(parameters_list) - - for key, parameter in parameters.items(multi=True): - code = self._make_index(key, Enum_Parameter, bits=16, name='parameter type') - parameters_list.append(self._make_sctp_parameter(code, parameter)) - return _last_item_only(parameters_list) + if schema.length <= 16: + raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') - def _make_sctp_parameter(self, code: 'Enum_Parameter', parameter: 'Optional[Data_Parameter]' = None, - **kwargs: 'Any') -> 'Schema_Parameter': - """Dispatch to the parameter constructor registered for ``code``. + return Data_DATAChunk( + type=schema.type, + length=schema.length, + flags=Data_DATAChunkFlags( + reserved=schema.flags['reserved'], + I=bool(schema.flags['I']), + U=bool(schema.flags['U']), + B=bool(schema.flags['B']), + E=bool(schema.flags['E']), + ), + tsn=schema.tsn, + stream_id=schema.stream_id, + stream_seq=schema.stream_seq, + ppid=schema.ppid, + data=schema.data, + ) - Args: - code: SCTP chunk parameter type. - parameter: Parameter data, if constructing from a parsed data model. - **kwargs: Arbitrary keyword arguments for the constructor. + def _read_chunk_init(self, schema: 'Schema_INITChunk', *, chunks: 'Chunks') -> 'Data_INITChunk': # pylint: disable=unused-argument + """Read SCTP INIT chunk. - Returns: - Constructed parameter schema. + Structure of SCTP INIT chunk [:rfc:`9260#section-3.3.2`]: - """ - name = self._lookup_registry(self.__parameter__, code) - if isinstance(name, str): - meth_name = f'_make_param_{name}' - meth = cast('ParameterConstructor', - getattr(self, meth_name, self._make_param_donone)) - else: - meth = name[1] - return _restore_padding(parameter, meth(code, parameter, **kwargs)) + .. code-block:: text - def _read_sctp_causes(self, schemas: 'list[Schema_ErrorCause]') -> 'Causes': - """Read SCTP error cause list. + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type = 1 | Chunk Flags | Chunk Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Initiate Tag | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Advertised Receiver Window Credit (a_rwnd) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Number of Outbound Streams | Number of Inbound Streams | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Initial TSN | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + \\ \\ + / Optional/Variable-Length Parameters / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - Args: - schemas: Parsed error cause schemas. + Arguments: + schema: parsed chunk schema + chunks: extracted SCTP chunks Returns: - Extracted SCTP error causes. + Parsed chunk data. + + Raises: + ProtocolError: If ``length`` is **NOT** at least ``20``. """ - causes = OrderedMultiDict() # type: Causes + if schema.length < 20: + raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') - for schema in schemas: - code = schema.code - name = self._lookup_registry(self.__cause__, code) + return Data_INITChunk( + type=schema.type, + length=schema.length, + flags=schema.flags, + init_tag=schema.init_tag, + a_rwnd=schema.a_rwnd, + outbound_streams=schema.outbound_streams, + inbound_streams=schema.inbound_streams, + init_tsn=schema.init_tsn, + parameters=self._read_sctp_parameters(schema.parameters), + ) - if isinstance(name, str): - meth_name = f'_read_cause_{name}' - meth = cast('CauseParser', - getattr(self, meth_name, self._read_cause_donone)) - else: - meth = name[0] - causes.add(code, _keep_padding(schema, meth(schema, causes=causes))) - return causes + def _read_chunk_init_ack(self, schema: 'Schema_INITACKChunk', *, chunks: 'Chunks') -> 'Data_INITACKChunk': # pylint: disable=unused-argument + """Read SCTP INIT ACK chunk. - def _make_sctp_causes(self, causes: 'list[Schema_ErrorCause | tuple[Enum_CauseCode | int, dict[str, Any]] | bytes] | Causes') -> 'list[Schema_ErrorCause | bytes]': # pylint: disable=line-too-long - """Make error causes for SCTP. + Structure of SCTP INIT ACK chunk [:rfc:`9260#section-3.3.3`]: - Args: - causes: SCTP error causes. + .. code-block:: text + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Type = 2 | Chunk Flags | Chunk Length | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Initiate Tag | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Advertised Receiver Window Credit | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Number of Outbound Streams | Number of Inbound Streams | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Initial TSN | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + \\ \\ + / Optional/Variable-Length Parameters / + \\ \\ + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Arguments: + schema: parsed chunk schema + chunks: extracted SCTP chunks Returns: - Constructed error cause schemas. + Parsed chunk data. Raises: - ProtocolError: If the code of an error cause, given in a ``(code, - kwargs)`` pair or as the key of a container, is a name, a - :obj:`bool` or a code its field cannot carry, or if an error - cause given as raw octets is empty (:issue:`2723`). - - """ - causes_list = [] # type: list[Schema_ErrorCause | bytes] - - if isinstance(causes, list): - for schema in causes: - if isinstance(schema, bytes) and not schema: - raise ProtocolError(f'{self.alias}: invalid error cause: {schema!r} holds no code field') - if isinstance(schema, (bytes, Schema)): - causes_list.append(schema) - else: - kind, args = cast('tuple[Enum_CauseCode | int, dict[str, Any]]', schema) - code = self._make_index(kind, Enum_CauseCode, bits=16, name='cause code') - causes_list.append(self._make_sctp_cause(code, None, **args)) - return _last_item_only(causes_list) - - for key, cause in causes.items(multi=True): - code = self._make_index(key, Enum_CauseCode, bits=16, name='cause code') - causes_list.append(self._make_sctp_cause(code, cause)) - return _last_item_only(causes_list) - - def _make_sctp_cause(self, code: 'Enum_CauseCode', cause: 'Optional[Data_ErrorCause]' = None, - **kwargs: 'Any') -> 'Schema_ErrorCause': - """Dispatch to the error cause constructor registered for ``code``. - - Args: - code: SCTP error cause code. - cause: Cause data, if constructing from a parsed data model. - **kwargs: Arbitrary keyword arguments for the constructor. - - Returns: - Constructed error cause schema. - - """ - name = self._lookup_registry(self.__cause__, code) - if isinstance(name, str): - meth_name = f'_make_cause_{name}' - meth = cast('CauseConstructor', - getattr(self, meth_name, self._make_cause_donone)) - else: - meth = name[1] - return _restore_padding(cause, meth(code, cause, **kwargs)) - - def _make_nested_length(self, base: 'int', items: 'Sequence[Schema_Parameter | Schema_ErrorCause | bytes]', # pylint: disable=line-too-long - declared: 'Optional[int]' = None) -> 'int': - """Compute the length of a chunk carrying a nested parameter or error - cause list. - - Per :rfc:`9260#section-3.2`, the chunk length counts the padding of - every nested item except the last. The RFC also asks receivers to - accept a length that counts the last item's padding too, so a parsed - chunk declaring that longer form keeps it. - - Args: - base: size of the chunk's fixed-length fields, including the - four-byte chunk header - items: constructed nested items; a raw :obj:`bytes` item is - counted as given - declared: chunk length declared by the parsed chunk being - rebuilt, if any - - Returns: - Chunk length. - - """ - sizes = [len(item) if isinstance(item, bytes) else len(item.pack()) for item in items] - padded = base + sum(sizes) - if not items or isinstance(items[-1], bytes) or declared == padded: - return padded - return padded - max(sizes[-1] - items[-1].length, 0) - - def _make_param_types(self, types: 'Optional[list[Enum_Parameter | int]]') -> 'list[Enum_Parameter]': - """Resolve a list of parameter types. - - Shared by :meth:`_make_param_addrtypes` and - :meth:`_make_cause_missing_param`, whose ``types`` both list - parameter types. - - Args: - types: parameter types, each given as a - :class:`~pcapkit.const.sctp.parameter.Parameter` member or an - :obj:`int` code - - Returns: - The :class:`~pcapkit.const.sctp.parameter.Parameter` member each - item resolves to with ``get``, or the code itself where no member - holds it. - - Raises: - ProtocolError: If an item is a :obj:`bool` (:issue:`2288`), a name, - any other non-:obj:`int`, or a code the 16-bit parameter type - field cannot carry (:issue:`2594`). + ProtocolError: If ``length`` is **NOT** at least ``20``. """ - return [self._make_index(item, Enum_Parameter, bits=16, name='parameter type') - for item in types or []] - - def _read_chunk_donone(self, schema: 'Schema_UnknownChunk', *, chunks: 'Chunks') -> 'Data_UnknownChunk': # pylint: disable=unused-argument - """Read SCTP chunk of an unsupported type. - - This is the fall-through for every chunk type :mod:`pcapkit` does not - implement -- unassigned, reserved, or defined by an SCTP extension -- - as well as for the SCTP-defined but reserved ECNE and CWR chunks. The - chunk's raw flags and value are recorded verbatim rather than raising, - so that a bundle containing an unknown chunk still parses. - - Arguments: - schema: parsed chunk schema - chunks: extracted SCTP chunks - - Returns: - Parsed chunk data. + if schema.length < 20: + raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') - """ - return Data_UnknownChunk( + return Data_INITACKChunk( type=schema.type, length=schema.length, flags=schema.flags, - value=schema.value, + init_tag=schema.init_tag, + a_rwnd=schema.a_rwnd, + outbound_streams=schema.outbound_streams, + inbound_streams=schema.inbound_streams, + init_tsn=schema.init_tsn, + parameters=self._read_sctp_parameters(schema.parameters), ) - def _read_chunk_data(self, schema: 'Schema_DATAChunk', *, chunks: 'Chunks') -> 'Data_DATAChunk': # pylint: disable=unused-argument - """Read SCTP DATA chunk. + def _read_chunk_sack(self, schema: 'Schema_SACKChunk', *, chunks: 'Chunks') -> 'Data_SACKChunk': # pylint: disable=unused-argument + """Read SCTP SACK chunk. - Structure of SCTP DATA chunk [:rfc:`9260#section-3.3.1`]: + Structure of SCTP SACK chunk [:rfc:`9260#section-3.3.4`]: .. code-block:: text 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type = 0 | Res |I|U|B|E| Length | + | Type = 3 | Chunk Flags | Chunk Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | TSN | + | Cumulative TSN Ack | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Stream Identifier S | Stream Sequence Number n | + | Advertised Receiver Window Credit (a_rwnd) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Payload Protocol Identifier | + | Number of Gap Ack Blocks = N | Number of Duplicate TSNs = M | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - \\ \\ - / User Data (seq n of Stream S) / - \\ \\ + | Gap Ack Block #1 Start | Gap Ack Block #1 End | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / ... / + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Duplicate TSN 1 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + / ... / +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Arguments: @@ -1412,53 +1317,44 @@ def _read_chunk_data(self, schema: 'Schema_DATAChunk', *, chunks: 'Chunks') -> ' Parsed chunk data. Raises: - ProtocolError: If ``length`` is **NOT** greater than ``16``, since - :rfc:`9260#section-3.3.1` requires at least one byte of user - data. + ProtocolError: If ``length`` does **NOT** match the declared number + of gap ack blocks and duplicate TSNs. """ - if schema.length <= 16: + if schema.length != 16 + schema.num_gap_blocks * 4 + schema.num_dup_tsn * 4: raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') - return Data_DATAChunk( + return Data_SACKChunk( type=schema.type, length=schema.length, - flags=Data_DATAChunkFlags( - reserved=schema.flags['reserved'], - I=bool(schema.flags['I']), - U=bool(schema.flags['U']), - B=bool(schema.flags['B']), - E=bool(schema.flags['E']), + flags=schema.flags, + cum_tsn_ack=schema.cum_tsn_ack, + a_rwnd=schema.a_rwnd, + num_gap_blocks=schema.num_gap_blocks, + num_dup_tsn=schema.num_dup_tsn, + gap_blocks=tuple( + Data_GapAckBlock( + start=block.start, + end=block.end, + ) for block in schema.gap_blocks ), - tsn=schema.tsn, - stream_id=schema.stream_id, - stream_seq=schema.stream_seq, - ppid=schema.ppid, - data=schema.data, + dup_tsn=tuple(schema.dup_tsn), ) - def _read_chunk_init(self, schema: 'Schema_INITChunk', *, chunks: 'Chunks') -> 'Data_INITChunk': # pylint: disable=unused-argument - """Read SCTP INIT chunk. + def _read_chunk_heartbeat(self, schema: 'Schema_HeartbeatChunk', *, chunks: 'Chunks') -> 'Data_HeartbeatChunk': # pylint: disable=unused-argument + """Read SCTP HEARTBEAT chunk. - Structure of SCTP INIT chunk [:rfc:`9260#section-3.3.2`]: + Structure of SCTP HEARTBEAT chunk [:rfc:`9260#section-3.3.5`]: .. code-block:: text 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type = 1 | Chunk Flags | Chunk Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Initiate Tag | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Advertised Receiver Window Credit (a_rwnd) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Number of Outbound Streams | Number of Inbound Streams | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Initial TSN | + | Type = 4 | Chunk Flags | Heartbeat Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ \\ \\ - / Optional/Variable-Length Parameters / + / Heartbeat Information TLV (Variable-Length) / \\ \\ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ @@ -1470,46 +1366,38 @@ def _read_chunk_init(self, schema: 'Schema_INITChunk', *, chunks: 'Chunks') -> ' Parsed chunk data. Raises: - ProtocolError: If ``length`` is **NOT** at least ``20``. + ProtocolError: If ``length`` is **NOT** at least ``4``. + + Note: + :rfc:`9260#section-3.3.5` mandates exactly one Heartbeat Info + parameter, but the parameters are modelled as a list so that a + sender emitting more (or none) still parses. """ - if schema.length < 20: + if schema.length < 4: raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') - return Data_INITChunk( + return Data_HeartbeatChunk( type=schema.type, length=schema.length, flags=schema.flags, - init_tag=schema.init_tag, - a_rwnd=schema.a_rwnd, - outbound_streams=schema.outbound_streams, - inbound_streams=schema.inbound_streams, - init_tsn=schema.init_tsn, parameters=self._read_sctp_parameters(schema.parameters), ) - def _read_chunk_init_ack(self, schema: 'Schema_INITACKChunk', *, chunks: 'Chunks') -> 'Data_INITACKChunk': # pylint: disable=unused-argument - """Read SCTP INIT ACK chunk. + def _read_chunk_heartbeat_ack(self, schema: 'Schema_HeartbeatACKChunk', *, chunks: 'Chunks') -> 'Data_HeartbeatACKChunk': # pylint: disable=unused-argument + """Read SCTP HEARTBEAT ACK chunk. - Structure of SCTP INIT ACK chunk [:rfc:`9260#section-3.3.3`]: + Structure of SCTP HEARTBEAT ACK chunk [:rfc:`9260#section-3.3.6`]: .. code-block:: text 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type = 2 | Chunk Flags | Chunk Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Initiate Tag | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Advertised Receiver Window Credit | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Number of Outbound Streams | Number of Inbound Streams | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Initial TSN | + | Type = 5 | Chunk Flags | Heartbeat Ack Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ \\ \\ - / Optional/Variable-Length Parameters / + / Heartbeat Information TLV (Variable-Length) / \\ \\ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ @@ -1521,179 +1409,34 @@ def _read_chunk_init_ack(self, schema: 'Schema_INITACKChunk', *, chunks: 'Chunks Parsed chunk data. Raises: - ProtocolError: If ``length`` is **NOT** at least ``20``. + ProtocolError: If ``length`` is **NOT** at least ``4``. """ - if schema.length < 20: + if schema.length < 4: raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') - return Data_INITACKChunk( + return Data_HeartbeatACKChunk( type=schema.type, length=schema.length, flags=schema.flags, - init_tag=schema.init_tag, - a_rwnd=schema.a_rwnd, - outbound_streams=schema.outbound_streams, - inbound_streams=schema.inbound_streams, - init_tsn=schema.init_tsn, parameters=self._read_sctp_parameters(schema.parameters), ) - def _read_chunk_sack(self, schema: 'Schema_SACKChunk', *, chunks: 'Chunks') -> 'Data_SACKChunk': # pylint: disable=unused-argument - """Read SCTP SACK chunk. + def _read_chunk_abort(self, schema: 'Schema_AbortChunk', *, chunks: 'Chunks') -> 'Data_AbortChunk': # pylint: disable=unused-argument + """Read SCTP ABORT chunk. - Structure of SCTP SACK chunk [:rfc:`9260#section-3.3.4`]: + Structure of SCTP ABORT chunk [:rfc:`9260#section-3.3.7`]: .. code-block:: text 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type = 3 | Chunk Flags | Chunk Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Cumulative TSN Ack | + | Type = 6 | Reserved |T| Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Advertised Receiver Window Credit (a_rwnd) | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Number of Gap Ack Blocks = N | Number of Duplicate TSNs = M | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Gap Ack Block #1 Start | Gap Ack Block #1 End | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - / ... / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Duplicate TSN 1 | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - / ... / - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - Arguments: - schema: parsed chunk schema - chunks: extracted SCTP chunks - - Returns: - Parsed chunk data. - - Raises: - ProtocolError: If ``length`` does **NOT** match the declared number - of gap ack blocks and duplicate TSNs. - - """ - if schema.length != 16 + schema.num_gap_blocks * 4 + schema.num_dup_tsn * 4: - raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') - - return Data_SACKChunk( - type=schema.type, - length=schema.length, - flags=schema.flags, - cum_tsn_ack=schema.cum_tsn_ack, - a_rwnd=schema.a_rwnd, - num_gap_blocks=schema.num_gap_blocks, - num_dup_tsn=schema.num_dup_tsn, - gap_blocks=tuple( - Data_GapAckBlock( - start=block.start, - end=block.end, - ) for block in schema.gap_blocks - ), - dup_tsn=tuple(schema.dup_tsn), - ) - - def _read_chunk_heartbeat(self, schema: 'Schema_HeartbeatChunk', *, chunks: 'Chunks') -> 'Data_HeartbeatChunk': # pylint: disable=unused-argument - """Read SCTP HEARTBEAT chunk. - - Structure of SCTP HEARTBEAT chunk [:rfc:`9260#section-3.3.5`]: - - .. code-block:: text - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type = 4 | Chunk Flags | Heartbeat Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - \\ \\ - / Heartbeat Information TLV (Variable-Length) / - \\ \\ - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - Arguments: - schema: parsed chunk schema - chunks: extracted SCTP chunks - - Returns: - Parsed chunk data. - - Raises: - ProtocolError: If ``length`` is **NOT** at least ``4``. - - Note: - :rfc:`9260#section-3.3.5` mandates exactly one Heartbeat Info - parameter, but the parameters are modelled as a list so that a - sender emitting more (or none) still parses. - - """ - if schema.length < 4: - raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') - - return Data_HeartbeatChunk( - type=schema.type, - length=schema.length, - flags=schema.flags, - parameters=self._read_sctp_parameters(schema.parameters), - ) - - def _read_chunk_heartbeat_ack(self, schema: 'Schema_HeartbeatACKChunk', *, chunks: 'Chunks') -> 'Data_HeartbeatACKChunk': # pylint: disable=unused-argument - """Read SCTP HEARTBEAT ACK chunk. - - Structure of SCTP HEARTBEAT ACK chunk [:rfc:`9260#section-3.3.6`]: - - .. code-block:: text - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type = 5 | Chunk Flags | Heartbeat Ack Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - \\ \\ - / Heartbeat Information TLV (Variable-Length) / - \\ \\ - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - Arguments: - schema: parsed chunk schema - chunks: extracted SCTP chunks - - Returns: - Parsed chunk data. - - Raises: - ProtocolError: If ``length`` is **NOT** at least ``4``. - - """ - if schema.length < 4: - raise ProtocolError(f'{self.alias}: [Chunk {schema.type}] invalid format') - - return Data_HeartbeatACKChunk( - type=schema.type, - length=schema.length, - flags=schema.flags, - parameters=self._read_sctp_parameters(schema.parameters), - ) - - def _read_chunk_abort(self, schema: 'Schema_AbortChunk', *, chunks: 'Chunks') -> 'Data_AbortChunk': # pylint: disable=unused-argument - """Read SCTP ABORT chunk. - - Structure of SCTP ABORT chunk [:rfc:`9260#section-3.3.7`]: - - .. code-block:: text - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Type = 6 | Reserved |T| Length | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - \\ \\ - / zero or more Error Causes / - \\ \\ + \\ \\ + / zero or more Error Causes / + \\ \\ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Arguments: @@ -3955,3 +3698,260 @@ def _make_cause_protocol_violation(self, code: 'Enum_CauseCode', cause: 'Optiona length=self._make_uint(len(info) + 4, bits=16, name=f'[Cause {code}] length'), info=info, ) + + def _get_payload(self) -> 'bytes': + """Get payload of the packet. + + SCTP has no payload field in its header schema -- user data travels + inside DATA chunks -- so this returns the user data of the *first* DATA + chunk found by :meth:`self.read `, which is also the chunk + whose payload protocol identifier selects the next layer. Should the + packet carry no DATA chunk, an empty :obj:`bytes` is returned and the + next layer resolves to + :class:`~pcapkit.protocols.misc.no_payload.protocol.NoPayload`. + + Returns: + Payload of the packet as :obj:`bytes`. + + """ + return self._payload + + def _make_sctp_chunk(self, code: 'Enum_Chunk', chunk: 'Optional[Data_Chunk]' = None, + **kwargs: 'Any') -> 'Schema_Chunk': + """Dispatch to the chunk constructor registered for ``code``. + + Args: + code: SCTP chunk type. + chunk: Chunk data, if constructing from a parsed data model. + **kwargs: Arbitrary keyword arguments for the constructor. + + Returns: + Constructed chunk schema. + + """ + name = self._lookup_registry(self.__chunk__, code) + if isinstance(name, str): + meth_name = f'_make_chunk_{name}' + meth = cast('ChunkConstructor', + getattr(self, meth_name, self._make_chunk_donone)) + else: + meth = name[1] + return _restore_padding(chunk, meth(code, chunk, **kwargs)) + + def _read_sctp_parameters(self, schemas: 'list[Schema_Parameter]') -> 'Parameters': + """Read SCTP chunk parameter list. + + Args: + schemas: Parsed parameter schemas. + + Returns: + Extracted SCTP chunk parameters. + + """ + parameters = OrderedMultiDict() # type: Parameters + + for schema in schemas: + code = schema.type + name = self._lookup_registry(self.__parameter__, code) + + if isinstance(name, str): + meth_name = f'_read_param_{name}' + meth = cast('ParameterParser', + getattr(self, meth_name, self._read_param_donone)) + else: + meth = name[0] + parameters.add(code, _keep_padding(schema, meth(schema, parameters=parameters))) + return parameters + + def _make_sctp_parameters(self, parameters: 'list[Schema_Parameter | tuple[Enum_Parameter | int, dict[str, Any]] | bytes] | Parameters') -> 'list[Schema_Parameter | bytes]': # pylint: disable=line-too-long + """Make chunk parameters for SCTP. + + Args: + parameters: SCTP chunk parameters. + + Returns: + Constructed parameter schemas. + + Raises: + ProtocolError: If the type of a parameter, given in a ``(type, + kwargs)`` pair or as the key of a container, is a name, a + :obj:`bool` or a code its field cannot carry, or if a parameter + given as raw octets is empty (:issue:`2723`). + + """ + parameters_list = [] # type: list[Schema_Parameter | bytes] + + if isinstance(parameters, list): + for schema in parameters: + if isinstance(schema, bytes) and not schema: + raise ProtocolError(f'{self.alias}: invalid parameter: {schema!r} holds no type field') + if isinstance(schema, (bytes, Schema)): + parameters_list.append(schema) + else: + kind, args = cast('tuple[Enum_Parameter | int, dict[str, Any]]', schema) + code = self._make_index(kind, Enum_Parameter, bits=16, name='parameter type') + parameters_list.append(self._make_sctp_parameter(code, None, **args)) + return _last_item_only(parameters_list) + + for key, parameter in parameters.items(multi=True): + code = self._make_index(key, Enum_Parameter, bits=16, name='parameter type') + parameters_list.append(self._make_sctp_parameter(code, parameter)) + return _last_item_only(parameters_list) + + def _make_sctp_parameter(self, code: 'Enum_Parameter', parameter: 'Optional[Data_Parameter]' = None, + **kwargs: 'Any') -> 'Schema_Parameter': + """Dispatch to the parameter constructor registered for ``code``. + + Args: + code: SCTP chunk parameter type. + parameter: Parameter data, if constructing from a parsed data model. + **kwargs: Arbitrary keyword arguments for the constructor. + + Returns: + Constructed parameter schema. + + """ + name = self._lookup_registry(self.__parameter__, code) + if isinstance(name, str): + meth_name = f'_make_param_{name}' + meth = cast('ParameterConstructor', + getattr(self, meth_name, self._make_param_donone)) + else: + meth = name[1] + return _restore_padding(parameter, meth(code, parameter, **kwargs)) + + def _read_sctp_causes(self, schemas: 'list[Schema_ErrorCause]') -> 'Causes': + """Read SCTP error cause list. + + Args: + schemas: Parsed error cause schemas. + + Returns: + Extracted SCTP error causes. + + """ + causes = OrderedMultiDict() # type: Causes + + for schema in schemas: + code = schema.code + name = self._lookup_registry(self.__cause__, code) + + if isinstance(name, str): + meth_name = f'_read_cause_{name}' + meth = cast('CauseParser', + getattr(self, meth_name, self._read_cause_donone)) + else: + meth = name[0] + causes.add(code, _keep_padding(schema, meth(schema, causes=causes))) + return causes + + def _make_sctp_causes(self, causes: 'list[Schema_ErrorCause | tuple[Enum_CauseCode | int, dict[str, Any]] | bytes] | Causes') -> 'list[Schema_ErrorCause | bytes]': # pylint: disable=line-too-long + """Make error causes for SCTP. + + Args: + causes: SCTP error causes. + + Returns: + Constructed error cause schemas. + + Raises: + ProtocolError: If the code of an error cause, given in a ``(code, + kwargs)`` pair or as the key of a container, is a name, a + :obj:`bool` or a code its field cannot carry, or if an error + cause given as raw octets is empty (:issue:`2723`). + + """ + causes_list = [] # type: list[Schema_ErrorCause | bytes] + + if isinstance(causes, list): + for schema in causes: + if isinstance(schema, bytes) and not schema: + raise ProtocolError(f'{self.alias}: invalid error cause: {schema!r} holds no code field') + if isinstance(schema, (bytes, Schema)): + causes_list.append(schema) + else: + kind, args = cast('tuple[Enum_CauseCode | int, dict[str, Any]]', schema) + code = self._make_index(kind, Enum_CauseCode, bits=16, name='cause code') + causes_list.append(self._make_sctp_cause(code, None, **args)) + return _last_item_only(causes_list) + + for key, cause in causes.items(multi=True): + code = self._make_index(key, Enum_CauseCode, bits=16, name='cause code') + causes_list.append(self._make_sctp_cause(code, cause)) + return _last_item_only(causes_list) + + def _make_sctp_cause(self, code: 'Enum_CauseCode', cause: 'Optional[Data_ErrorCause]' = None, + **kwargs: 'Any') -> 'Schema_ErrorCause': + """Dispatch to the error cause constructor registered for ``code``. + + Args: + code: SCTP error cause code. + cause: Cause data, if constructing from a parsed data model. + **kwargs: Arbitrary keyword arguments for the constructor. + + Returns: + Constructed error cause schema. + + """ + name = self._lookup_registry(self.__cause__, code) + if isinstance(name, str): + meth_name = f'_make_cause_{name}' + meth = cast('CauseConstructor', + getattr(self, meth_name, self._make_cause_donone)) + else: + meth = name[1] + return _restore_padding(cause, meth(code, cause, **kwargs)) + + def _make_nested_length(self, base: 'int', items: 'Sequence[Schema_Parameter | Schema_ErrorCause | bytes]', # pylint: disable=line-too-long + declared: 'Optional[int]' = None) -> 'int': + """Compute the length of a chunk carrying a nested parameter or error + cause list. + + Per :rfc:`9260#section-3.2`, the chunk length counts the padding of + every nested item except the last. The RFC also asks receivers to + accept a length that counts the last item's padding too, so a parsed + chunk declaring that longer form keeps it. + + Args: + base: size of the chunk's fixed-length fields, including the + four-byte chunk header + items: constructed nested items; a raw :obj:`bytes` item is + counted as given + declared: chunk length declared by the parsed chunk being + rebuilt, if any + + Returns: + Chunk length. + + """ + sizes = [len(item) if isinstance(item, bytes) else len(item.pack()) for item in items] + padded = base + sum(sizes) + if not items or isinstance(items[-1], bytes) or declared == padded: + return padded + return padded - max(sizes[-1] - items[-1].length, 0) + + def _make_param_types(self, types: 'Optional[list[Enum_Parameter | int]]') -> 'list[Enum_Parameter]': + """Resolve a list of parameter types. + + Shared by :meth:`_make_param_addrtypes` and + :meth:`_make_cause_missing_param`, whose ``types`` both list + parameter types. + + Args: + types: parameter types, each given as a + :class:`~pcapkit.const.sctp.parameter.Parameter` member or an + :obj:`int` code + + Returns: + The :class:`~pcapkit.const.sctp.parameter.Parameter` member each + item resolves to with ``get``, or the code itself where no member + holds it. + + Raises: + ProtocolError: If an item is a :obj:`bool` (:issue:`2288`), a name, + any other non-:obj:`int`, or a code the 16-bit parameter type + field cannot carry (:issue:`2594`). + + """ + return [self._make_index(item, Enum_Parameter, bits=16, name='parameter type') + for item in types or []] diff --git a/pcapkit/protocols/transport/tcp/protocol.py b/pcapkit/protocols/transport/tcp/protocol.py index 77dd4220ed..5068c54986 100644 --- a/pcapkit/protocols/transport/tcp/protocol.py +++ b/pcapkit/protocols/transport/tcp/protocol.py @@ -882,29 +882,6 @@ def _make_data(cls, data: 'Data_TCP') -> 'dict[str, Any]': # type: ignore[overr 'payload': cls._make_payload(data), } - def _verify_pseudo_checksum(self, pseudo: 'Optional[IP_Pseudo]') -> 'None': - """Verify the checksum over the pseudo-header of the enclosing IP layer. - - That layer calls this once parsed, see - :func:`~pcapkit.protocols.misc.checksum.ip_pseudo.protocol.verify_upper`. A zero - checksum, or the pseudo-header's sum alone, is what checksum offload - leaves on the sending host, and is - :attr:`~pcapkit.corekit.checksum.internet.ChecksumStatus.NOT_COMPUTED`. - - Args: - pseudo: Pseudo-header, or :data:`None` if the IP layer gives none. - - """ - from pcapkit.corekit.checksum.internet import ( # isort: skip # pylint: disable=import-outside-toplevel - ChecksumStatus, partial_checksum, verify_checksum) - - if pseudo is None: - status = ChecksumStatus.NO_PSEUDO_HEADER - else: - status = verify_checksum(self.alias, self._data, self._info.checksum, length=pseudo.info.len, - prefix=pseudo.data, offload=(b'\x00\x00', partial_checksum(pseudo.data))) - self._info.__update__(checksum_status=status) - def _read_tcp_options(self, size: 'int') -> 'Option': """Read TCP option list. @@ -1882,146 +1859,6 @@ def _read_mptcp_join(self, schema: 'Schema_MPTCPJoin', *, options: 'Option') -> return self._read_join_ack(schema, options=options) # type: ignore[arg-type] raise ProtocolError(f'{self.alias}: [OptNo {schema.kind}] {schema.subtype}: invalid flags combination') - def _read_join_syn(self, schema: 'Schema_MPTCPJoinSYN', *, options: 'Option') -> 'Data_MPTCPJoinSYN': # pylint: disable=unused-argument - """Read Join Connection option for Initial SYN. - - Structure of ``MP_JOIN-SYN`` [:rfc:`8684`, section 3.2, figure 5]: - - .. code-block:: text - - 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +---------------+---------------+-------+-----+-+---------------+ - | Kind | Length = 12 |Subtype| |B| Address ID | - +---------------+---------------+-------+-----+-+---------------+ - | Receiver's Token (32 bits) | - +---------------------------------------------------------------+ - | Sender's Random Number (32 bits) | - +---------------------------------------------------------------+ - - Arguments: - schema: parsed option schema - options: extracted TCP options - - Returns: - Parsed option data. - - Raises: - ProtocolError: If length is **NOT** ``12``. - - """ - if schema.length != 12: - raise ProtocolError(f'{self.alias}: [OptNo {schema.kind}] invalid format') - - data = Data_MPTCPJoinSYN( - kind=Enum_Option.Multipath_TCP, # type: ignore[arg-type] - length=schema.length, - subtype=schema.subtype, - connection=Enum_Flags.SYN, # type: ignore[arg-type] - reserved=schema.test['reserved'], - backup=bool(schema.test['backup']), - addr_id=schema.addr_id, - token=schema.token, - nonce=schema.nonce, - ) - return data - - def _read_join_synack(self, schema: 'Schema_MPTCPJoinSYNACK', options: 'Option') -> 'Data_MPTCPJoinSYNACK': # pylint: disable=unused-argument - """Read Join Connection option for Responding SYN/ACK. - - Structure of ``MP_JOIN-SYN/ACK`` [:rfc:`8684`, section 3.2, figure 6]: - - .. code-block:: text - - 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +---------------+---------------+-------+-----+-+---------------+ - | Kind | Length = 16 |Subtype| |B| Address ID | - +---------------+---------------+-------+-----+-+---------------+ - | | - | Sender's Truncated HMAC (64 bits) | - | | - +---------------------------------------------------------------+ - | Sender's Random Number (32 bits) | - +---------------------------------------------------------------+ - - Arguments: - schema: parsed option schema - options: extracted TCP options - - Returns: - Parsed option data. - - Raises: - ProtocolError: If length is **NOT** ``16``. - - Note: - The accepted length is ``16``, as in the figure above (:rfc:`8684` - section 3.2 figure 6) and in what - :class:`~pcapkit.protocols.transport.tcp.schema.MPTCPJoinSYNACK` - packs and unpacks: ``Kind`` (1) + ``Length`` (1) + subtype/flags (1) - + ``Address ID`` (1) + the truncated HMAC (8) + the random number (4). - - """ - if schema.length != 16: - raise ProtocolError(f'{self.alias}: [OptNo {schema.kind}] invalid format') - - data = Data_MPTCPJoinSYNACK( - kind=Enum_Option.Multipath_TCP, # type: ignore[arg-type] - length=schema.length, - subtype=schema.subtype, - connection=Enum_Flags.SYN | Enum_Flags.ACK, # type: ignore[arg-type] - reserved=schema.test['reserved'], - backup=bool(schema.test['backup']), - addr_id=schema.addr_id, - hmac=schema.hmac, - nonce=schema.nonce, - ) - return data - - def _read_join_ack(self, schema: 'Schema_MPTCPJoinACK', *, options: 'Option') -> 'Data_MPTCPJoinACK': # pylint: disable=unused-argument - """Read Join Connection option for Third ACK. - - Structure of ``MP_JOIN-ACK`` [:rfc:`8684`, section 3.2, figure 7]: - - .. code-block:: text - - 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +---------------+---------------+-------+-----------------------+ - | Kind | Length = 24 |Subtype| (reserved) | - +---------------+---------------+-------+-----------------------+ - | | - | | - | Sender's HMAC (160 bits) | - | | - | | - +---------------------------------------------------------------+ - - Arguments: - schema: parsed option schema - options: extracted TCP options - - Returns: - Parsed option data. - - Raises: - ProtocolError: If length is **NOT** ``24``. - - """ - if schema.length != 24: - raise ProtocolError(f'{self.alias}: [OptNo {schema.kind}] invalid format') - - data = Data_MPTCPJoinACK( - kind=Enum_Option.Multipath_TCP, # type: ignore[arg-type] - length=schema.length, - subtype=schema.subtype, - connection=Enum_Flags.ACK, # type: ignore[arg-type] - reserved=schema.test['reserved'], - hmac=schema.hmac, - ) - return data - def _read_mptcp_dss(self, schema: 'Schema_MPTCPDSS', *, options: 'Option') -> 'Data_MPTCPDSS': # pylint: disable=unused-argument """Read Data Sequence Signal (Data ACK and Data Sequence Mapping) option. @@ -3193,234 +3030,91 @@ def _make_mptcp_join(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTC return self._make_join_ack(subtype, opt, **kwargs) # type: ignore[arg-type] raise ProtocolError(f'{self.alias}: [OptNo {Enum_Option.Multipath_TCP}] {subtype}: invalid flags combination') - def _make_join_syn(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPJoinSYN]' = None, *, - reserved: 'int' = 0, - backup: 'bool' = False, - addr_id: 'int' = 0, - token: 'int' = 0, - nonce: 'int' = 0, - **kwargs: 'Any') -> 'Schema_MPTCPJoinSYN': - """Make multipath TCP join SYN option. + def _make_mptcp_dss(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPDSS]' = None, *, + reserved: 'int' = 0, + data_fin: 'bool' = False, + ack_wide: 'Optional[bool]' = None, + ack: 'Optional[int]' = None, + dsn_wide: 'Optional[bool]' = None, + dsn: 'Optional[int]' = None, + ssn: 'Optional[int]' = None, + dl_len: 'Optional[int]' = None, + checksum: 'Optional[bytes]' = None, + **kwargs: 'Any') -> 'Schema_MPTCPDSS': + """Make multipath TCP DSS option. Args: subtype: MPTCP subtype opt: option data - reserved: reserved bits - backup: backup flag - addr_id: address ID - token: receiver's token - nonce: sender's random number + reserved: reserved bits (7 bits) + data_fin: ``DATA_FIN`` flag + ack_wide: whether Data ACK is 8 octets; if not given, it is 8 octets + only when ``ack`` does not fit in 4 + ack: Data ACK + dsn_wide: whether data sequence number is 8 octets; if not given, it + is 8 octets only when ``dsn`` does not fit in 4 + dsn: data sequence number + ssn: subflow sequence number + dl_len: data-level length + checksum: checksum, or ``None`` if checksums were not negotiated **kwargs: arbitrary keyword arguments Returns: Constructed option schema. Raises: - ProtocolError: If a field does not fit its width. + ProtocolError: If the mapping fields are given only in part, a + 4-octet field is asked to hold a value wider than 32 bits, or a + field does not fit its width. """ if opt is not None: reserved = opt.reserved - backup = opt.backup - addr_id = opt.addr_id - token = opt.token - nonce = opt.nonce + data_fin = opt.data_fin + ack_wide = opt.ack_wide + ack = opt.ack + dsn_wide = opt.dsn_wide + dsn = opt.dsn + ssn = opt.ssn + dl_len = opt.dl_len + checksum = opt.checksum where = f'[OptNo {Enum_Option.Multipath_TCP}] {subtype.name}' - return Schema_MPTCPJoinSYN( + reserved = self._make_uint(reserved, bits=7, name=f'{where} reserved') + if ack is not None: + ack = self._make_uint(ack, bits=64, name=f'{where} ack') + if dsn is not None: + dsn = self._make_uint(dsn, bits=64, name=f'{where} dsn') + + flag_A = ack is not None + flag_a = ack_wide if ack_wide is not None else (flag_A and cast('int', ack).bit_length() > 32) + + flag_M = dsn is not None + flag_m = dsn_wide if dsn_wide is not None else (flag_M and cast('int', dsn).bit_length() > 32) + + if (flag_A and not flag_a and cast('int', ack).bit_length() > 32) or \ + (flag_M and not flag_m and cast('int', dsn).bit_length() > 32): + raise ProtocolError(f'{self.alias}: [OptNo {Enum_Option.Multipath_TCP}] {subtype}: value too wide for 4 octets') + if flag_M and (ssn is None or dl_len is None): + raise ProtocolError(f'{self.alias}: [OptNo {Enum_Option.Multipath_TCP}] {subtype}: missing required fields') + if not flag_M and (ssn is not None or dl_len is not None or checksum is not None): + raise ProtocolError(f'{self.alias}: [OptNo {Enum_Option.Multipath_TCP}] {subtype}: missing required fields') + + return Schema_MPTCPDSS( kind=cast('Enum_Option', Enum_Option.Multipath_TCP), - length=12, + # NOTE: this arithmetic follows :rfc:`8684` section 3.3 figure 9: + # 4 for ``Kind``/``Length``/subtype/flags, then the Data ACK (4, or + # 8 with ``a``) when ``A`` is set, and the mapping when ``M`` is set + # -- the DSN (4, or 8 with ``m``), the 4-octet Subflow Sequence + # Number, the 2-octet Data-Level Length and, only if checksums were + # negotiated, the 2-octet Checksum. ``a`` and ``m`` widen a field + # only when it is present, so a stray flag on the wire still packs + # back without changing the length. + length=4 + ((8 if flag_a else 4) if flag_A else 0) + + ((8 if flag_m else 4) + 6 + (2 if checksum is not None else 0) if flag_M else 0), test={ 'subtype': subtype.value, - 'reserved': self._make_uint(reserved, bits=3, name=f'{where} reserved'), - 'backup': backup, - }, - addr_id=self._make_uint(addr_id, bits=8, name=f'{where} addr_id'), - token=self._make_uint(token, bits=32, name=f'{where} token'), - nonce=self._make_uint(nonce, bits=32, name=f'{where} nonce'), - ) - - def _make_join_synack(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPJoinSYNACK]' = None, *, - reserved: 'int' = 0, - backup: 'bool' = False, - addr_id: 'int' = 0, - hmac: 'bytes' = bytes(8), - nonce: 'int' = 0, - **kwargs: 'Any') -> 'Schema_MPTCPJoinSYNACK': - """Make multipath TCP join SYN/ACK option. - - Args: - subtype: MPTCP subtype - opt: option data - reserved: reserved bits - backup: backup flag - addr_id: address ID - hmac: sender's truncated HMAC - nonce: sender's random number - **kwargs: arbitrary keyword arguments - - Returns: - Constructed option schema. - - Raises: - ProtocolError: If a field does not fit its width, or ``hmac`` is - not eight octets. - - """ - if opt is not None: - reserved = opt.reserved - backup = opt.backup - addr_id = opt.addr_id - # NOTE: ``hmac`` must be carried over here, or reconstructing a - # parsed MP_JOIN-SYN/ACK would substitute the ``bytes(8)`` default - # for the truncated HMAC that was on the wire. - hmac = opt.hmac - nonce = opt.nonce - - where = f'[OptNo {Enum_Option.Multipath_TCP}] {subtype.name}' - return Schema_MPTCPJoinSYNACK( - kind=cast('Enum_Option', Enum_Option.Multipath_TCP), - # NOTE: :rfc:`8684` section 3.2 figure 6 gives ``Length = 16`` for the - # SYN/ACK form: ``Kind`` (1) + ``Length`` (1) + subtype/flags (1) + - # ``Address ID`` (1) + the truncated HMAC (64 bits, 8) + the random - # number (32 bits, 4). The SYN form of figure 5 is 12, since it - # carries a 4-octet token where this one carries an 8-octet HMAC. - length=16, - test={ - 'subtype': subtype.value, - 'reserved': self._make_uint(reserved, bits=3, name=f'{where} reserved'), - 'backup': backup, - }, - addr_id=self._make_uint(addr_id, bits=8, name=f'{where} addr_id'), - hmac=self._make_octets(hmac, size=8, name=f'{where} hmac'), - nonce=self._make_uint(nonce, bits=32, name=f'{where} nonce'), - ) - - def _make_join_ack(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPJoinACK]' = None, *, - reserved: 'int' = 0, - hmac: 'bytes' = bytes(20), - **kwargs: 'Any') -> 'Schema_MPTCPJoinACK': - """Make multipath TCP join ACK option. - - Args: - subtype: MPTCP subtype - opt: option data - reserved: reserved bits - hmac: sender's HMAC - **kwargs: arbitrary keyword arguments - - Returns: - Constructed option schema. - - Raises: - ProtocolError: If ``reserved`` does not fit 12 bits, or ``hmac`` is - not 20 octets. - - """ - if opt is not None: - reserved = opt.reserved - hmac = opt.hmac - - where = f'[OptNo {Enum_Option.Multipath_TCP}] {subtype.name}' - return Schema_MPTCPJoinACK( - kind=cast('Enum_Option', Enum_Option.Multipath_TCP), - # NOTE: :rfc:`8684` section 3.2 figure 7 gives ``Length = 24`` for the - # ACK form: ``Kind`` (1) + ``Length`` (1) + subtype-and-reserved (2, - # being 4 subtype bits and 12 reserved) + the full HMAC (160 bits, - # 20). ``_read_join_ack``'s guard requires the same 24. - length=24, - test={ - 'subtype': subtype.value, - 'reserved': self._make_uint(reserved, bits=12, name=f'{where} reserved'), - }, - hmac=self._make_octets(hmac, size=20, name=f'{where} hmac'), - ) - - def _make_mptcp_dss(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPDSS]' = None, *, - reserved: 'int' = 0, - data_fin: 'bool' = False, - ack_wide: 'Optional[bool]' = None, - ack: 'Optional[int]' = None, - dsn_wide: 'Optional[bool]' = None, - dsn: 'Optional[int]' = None, - ssn: 'Optional[int]' = None, - dl_len: 'Optional[int]' = None, - checksum: 'Optional[bytes]' = None, - **kwargs: 'Any') -> 'Schema_MPTCPDSS': - """Make multipath TCP DSS option. - - Args: - subtype: MPTCP subtype - opt: option data - reserved: reserved bits (7 bits) - data_fin: ``DATA_FIN`` flag - ack_wide: whether Data ACK is 8 octets; if not given, it is 8 octets - only when ``ack`` does not fit in 4 - ack: Data ACK - dsn_wide: whether data sequence number is 8 octets; if not given, it - is 8 octets only when ``dsn`` does not fit in 4 - dsn: data sequence number - ssn: subflow sequence number - dl_len: data-level length - checksum: checksum, or ``None`` if checksums were not negotiated - **kwargs: arbitrary keyword arguments - - Returns: - Constructed option schema. - - Raises: - ProtocolError: If the mapping fields are given only in part, a - 4-octet field is asked to hold a value wider than 32 bits, or a - field does not fit its width. - - """ - if opt is not None: - reserved = opt.reserved - data_fin = opt.data_fin - ack_wide = opt.ack_wide - ack = opt.ack - dsn_wide = opt.dsn_wide - dsn = opt.dsn - ssn = opt.ssn - dl_len = opt.dl_len - checksum = opt.checksum - - where = f'[OptNo {Enum_Option.Multipath_TCP}] {subtype.name}' - reserved = self._make_uint(reserved, bits=7, name=f'{where} reserved') - if ack is not None: - ack = self._make_uint(ack, bits=64, name=f'{where} ack') - if dsn is not None: - dsn = self._make_uint(dsn, bits=64, name=f'{where} dsn') - - flag_A = ack is not None - flag_a = ack_wide if ack_wide is not None else (flag_A and cast('int', ack).bit_length() > 32) - - flag_M = dsn is not None - flag_m = dsn_wide if dsn_wide is not None else (flag_M and cast('int', dsn).bit_length() > 32) - - if (flag_A and not flag_a and cast('int', ack).bit_length() > 32) or \ - (flag_M and not flag_m and cast('int', dsn).bit_length() > 32): - raise ProtocolError(f'{self.alias}: [OptNo {Enum_Option.Multipath_TCP}] {subtype}: value too wide for 4 octets') - if flag_M and (ssn is None or dl_len is None): - raise ProtocolError(f'{self.alias}: [OptNo {Enum_Option.Multipath_TCP}] {subtype}: missing required fields') - if not flag_M and (ssn is not None or dl_len is not None or checksum is not None): - raise ProtocolError(f'{self.alias}: [OptNo {Enum_Option.Multipath_TCP}] {subtype}: missing required fields') - - return Schema_MPTCPDSS( - kind=cast('Enum_Option', Enum_Option.Multipath_TCP), - # NOTE: this arithmetic follows :rfc:`8684` section 3.3 figure 9: - # 4 for ``Kind``/``Length``/subtype/flags, then the Data ACK (4, or - # 8 with ``a``) when ``A`` is set, and the mapping when ``M`` is set - # -- the DSN (4, or 8 with ``m``), the 4-octet Subflow Sequence - # Number, the 2-octet Data-Level Length and, only if checksums were - # negotiated, the 2-octet Checksum. ``a`` and ``m`` widen a field - # only when it is present, so a stray flag on the wire still packs - # back without changing the length. - length=4 + ((8 if flag_a else 4) if flag_A else 0) - + ((8 if flag_m else 4) + 6 + (2 if checksum is not None else 0) if flag_M else 0), - test={ - 'subtype': subtype.value, - 'reserved': reserved >> 3, + 'reserved': reserved >> 3, }, flags={ 'reserved': reserved & 0b111, @@ -3713,3 +3407,309 @@ def _make_mode_fastopen(self, code: 'Enum_Option', opt: 'Optional[Data_FastOpenC length=2 + (len(cookie) if cookie is not None else 0), cookie=cookie, ) + + def _verify_pseudo_checksum(self, pseudo: 'Optional[IP_Pseudo]') -> 'None': + """Verify the checksum over the pseudo-header of the enclosing IP layer. + + That layer calls this once parsed, see + :func:`~pcapkit.protocols.misc.checksum.ip_pseudo.protocol.verify_upper`. A zero + checksum, or the pseudo-header's sum alone, is what checksum offload + leaves on the sending host, and is + :attr:`~pcapkit.corekit.checksum.internet.ChecksumStatus.NOT_COMPUTED`. + + Args: + pseudo: Pseudo-header, or :data:`None` if the IP layer gives none. + + """ + from pcapkit.corekit.checksum.internet import ( # isort: skip # pylint: disable=import-outside-toplevel + ChecksumStatus, partial_checksum, verify_checksum) + + if pseudo is None: + status = ChecksumStatus.NO_PSEUDO_HEADER + else: + status = verify_checksum(self.alias, self._data, self._info.checksum, length=pseudo.info.len, + prefix=pseudo.data, offload=(b'\x00\x00', partial_checksum(pseudo.data))) + self._info.__update__(checksum_status=status) + + def _read_join_syn(self, schema: 'Schema_MPTCPJoinSYN', *, options: 'Option') -> 'Data_MPTCPJoinSYN': # pylint: disable=unused-argument + """Read Join Connection option for Initial SYN. + + Structure of ``MP_JOIN-SYN`` [:rfc:`8684`, section 3.2, figure 5]: + + .. code-block:: text + + 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +---------------+---------------+-------+-----+-+---------------+ + | Kind | Length = 12 |Subtype| |B| Address ID | + +---------------+---------------+-------+-----+-+---------------+ + | Receiver's Token (32 bits) | + +---------------------------------------------------------------+ + | Sender's Random Number (32 bits) | + +---------------------------------------------------------------+ + + Arguments: + schema: parsed option schema + options: extracted TCP options + + Returns: + Parsed option data. + + Raises: + ProtocolError: If length is **NOT** ``12``. + + """ + if schema.length != 12: + raise ProtocolError(f'{self.alias}: [OptNo {schema.kind}] invalid format') + + data = Data_MPTCPJoinSYN( + kind=Enum_Option.Multipath_TCP, # type: ignore[arg-type] + length=schema.length, + subtype=schema.subtype, + connection=Enum_Flags.SYN, # type: ignore[arg-type] + reserved=schema.test['reserved'], + backup=bool(schema.test['backup']), + addr_id=schema.addr_id, + token=schema.token, + nonce=schema.nonce, + ) + return data + + def _read_join_synack(self, schema: 'Schema_MPTCPJoinSYNACK', options: 'Option') -> 'Data_MPTCPJoinSYNACK': # pylint: disable=unused-argument + """Read Join Connection option for Responding SYN/ACK. + + Structure of ``MP_JOIN-SYN/ACK`` [:rfc:`8684`, section 3.2, figure 6]: + + .. code-block:: text + + 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +---------------+---------------+-------+-----+-+---------------+ + | Kind | Length = 16 |Subtype| |B| Address ID | + +---------------+---------------+-------+-----+-+---------------+ + | | + | Sender's Truncated HMAC (64 bits) | + | | + +---------------------------------------------------------------+ + | Sender's Random Number (32 bits) | + +---------------------------------------------------------------+ + + Arguments: + schema: parsed option schema + options: extracted TCP options + + Returns: + Parsed option data. + + Raises: + ProtocolError: If length is **NOT** ``16``. + + Note: + The accepted length is ``16``, as in the figure above (:rfc:`8684` + section 3.2 figure 6) and in what + :class:`~pcapkit.protocols.transport.tcp.schema.MPTCPJoinSYNACK` + packs and unpacks: ``Kind`` (1) + ``Length`` (1) + subtype/flags (1) + + ``Address ID`` (1) + the truncated HMAC (8) + the random number (4). + + """ + if schema.length != 16: + raise ProtocolError(f'{self.alias}: [OptNo {schema.kind}] invalid format') + + data = Data_MPTCPJoinSYNACK( + kind=Enum_Option.Multipath_TCP, # type: ignore[arg-type] + length=schema.length, + subtype=schema.subtype, + connection=Enum_Flags.SYN | Enum_Flags.ACK, # type: ignore[arg-type] + reserved=schema.test['reserved'], + backup=bool(schema.test['backup']), + addr_id=schema.addr_id, + hmac=schema.hmac, + nonce=schema.nonce, + ) + return data + + def _read_join_ack(self, schema: 'Schema_MPTCPJoinACK', *, options: 'Option') -> 'Data_MPTCPJoinACK': # pylint: disable=unused-argument + """Read Join Connection option for Third ACK. + + Structure of ``MP_JOIN-ACK`` [:rfc:`8684`, section 3.2, figure 7]: + + .. code-block:: text + + 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +---------------+---------------+-------+-----------------------+ + | Kind | Length = 24 |Subtype| (reserved) | + +---------------+---------------+-------+-----------------------+ + | | + | | + | Sender's HMAC (160 bits) | + | | + | | + +---------------------------------------------------------------+ + + Arguments: + schema: parsed option schema + options: extracted TCP options + + Returns: + Parsed option data. + + Raises: + ProtocolError: If length is **NOT** ``24``. + + """ + if schema.length != 24: + raise ProtocolError(f'{self.alias}: [OptNo {schema.kind}] invalid format') + + data = Data_MPTCPJoinACK( + kind=Enum_Option.Multipath_TCP, # type: ignore[arg-type] + length=schema.length, + subtype=schema.subtype, + connection=Enum_Flags.ACK, # type: ignore[arg-type] + reserved=schema.test['reserved'], + hmac=schema.hmac, + ) + return data + + def _make_join_syn(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPJoinSYN]' = None, *, + reserved: 'int' = 0, + backup: 'bool' = False, + addr_id: 'int' = 0, + token: 'int' = 0, + nonce: 'int' = 0, + **kwargs: 'Any') -> 'Schema_MPTCPJoinSYN': + """Make multipath TCP join SYN option. + + Args: + subtype: MPTCP subtype + opt: option data + reserved: reserved bits + backup: backup flag + addr_id: address ID + token: receiver's token + nonce: sender's random number + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + Raises: + ProtocolError: If a field does not fit its width. + + """ + if opt is not None: + reserved = opt.reserved + backup = opt.backup + addr_id = opt.addr_id + token = opt.token + nonce = opt.nonce + + where = f'[OptNo {Enum_Option.Multipath_TCP}] {subtype.name}' + return Schema_MPTCPJoinSYN( + kind=cast('Enum_Option', Enum_Option.Multipath_TCP), + length=12, + test={ + 'subtype': subtype.value, + 'reserved': self._make_uint(reserved, bits=3, name=f'{where} reserved'), + 'backup': backup, + }, + addr_id=self._make_uint(addr_id, bits=8, name=f'{where} addr_id'), + token=self._make_uint(token, bits=32, name=f'{where} token'), + nonce=self._make_uint(nonce, bits=32, name=f'{where} nonce'), + ) + + def _make_join_synack(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPJoinSYNACK]' = None, *, + reserved: 'int' = 0, + backup: 'bool' = False, + addr_id: 'int' = 0, + hmac: 'bytes' = bytes(8), + nonce: 'int' = 0, + **kwargs: 'Any') -> 'Schema_MPTCPJoinSYNACK': + """Make multipath TCP join SYN/ACK option. + + Args: + subtype: MPTCP subtype + opt: option data + reserved: reserved bits + backup: backup flag + addr_id: address ID + hmac: sender's truncated HMAC + nonce: sender's random number + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + Raises: + ProtocolError: If a field does not fit its width, or ``hmac`` is + not eight octets. + + """ + if opt is not None: + reserved = opt.reserved + backup = opt.backup + addr_id = opt.addr_id + # NOTE: ``hmac`` must be carried over here, or reconstructing a + # parsed MP_JOIN-SYN/ACK would substitute the ``bytes(8)`` default + # for the truncated HMAC that was on the wire. + hmac = opt.hmac + nonce = opt.nonce + + where = f'[OptNo {Enum_Option.Multipath_TCP}] {subtype.name}' + return Schema_MPTCPJoinSYNACK( + kind=cast('Enum_Option', Enum_Option.Multipath_TCP), + # NOTE: :rfc:`8684` section 3.2 figure 6 gives ``Length = 16`` for the + # SYN/ACK form: ``Kind`` (1) + ``Length`` (1) + subtype/flags (1) + + # ``Address ID`` (1) + the truncated HMAC (64 bits, 8) + the random + # number (32 bits, 4). The SYN form of figure 5 is 12, since it + # carries a 4-octet token where this one carries an 8-octet HMAC. + length=16, + test={ + 'subtype': subtype.value, + 'reserved': self._make_uint(reserved, bits=3, name=f'{where} reserved'), + 'backup': backup, + }, + addr_id=self._make_uint(addr_id, bits=8, name=f'{where} addr_id'), + hmac=self._make_octets(hmac, size=8, name=f'{where} hmac'), + nonce=self._make_uint(nonce, bits=32, name=f'{where} nonce'), + ) + + def _make_join_ack(self, subtype: 'Enum_MPTCPOption', opt: 'Optional[Data_MPTCPJoinACK]' = None, *, + reserved: 'int' = 0, + hmac: 'bytes' = bytes(20), + **kwargs: 'Any') -> 'Schema_MPTCPJoinACK': + """Make multipath TCP join ACK option. + + Args: + subtype: MPTCP subtype + opt: option data + reserved: reserved bits + hmac: sender's HMAC + **kwargs: arbitrary keyword arguments + + Returns: + Constructed option schema. + + Raises: + ProtocolError: If ``reserved`` does not fit 12 bits, or ``hmac`` is + not 20 octets. + + """ + if opt is not None: + reserved = opt.reserved + hmac = opt.hmac + + where = f'[OptNo {Enum_Option.Multipath_TCP}] {subtype.name}' + return Schema_MPTCPJoinACK( + kind=cast('Enum_Option', Enum_Option.Multipath_TCP), + # NOTE: :rfc:`8684` section 3.2 figure 7 gives ``Length = 24`` for the + # ACK form: ``Kind`` (1) + ``Length`` (1) + subtype-and-reserved (2, + # being 4 subtype bits and 12 reserved) + the full HMAC (160 bits, + # 20). ``_read_join_ack``'s guard requires the same 24. + length=24, + test={ + 'subtype': subtype.value, + 'reserved': self._make_uint(reserved, bits=12, name=f'{where} reserved'), + }, + hmac=self._make_octets(hmac, size=20, name=f'{where} hmac'), + ) diff --git a/pcapkit/protocols/transport/udp/protocol.py b/pcapkit/protocols/transport/udp/protocol.py index f8bb4674ab..74b6fd94bc 100644 --- a/pcapkit/protocols/transport/udp/protocol.py +++ b/pcapkit/protocols/transport/udp/protocol.py @@ -121,7 +121,14 @@ class UDP(Transport[Data_UDP, Schema_UDP], # :class:`pcapkit.protocols.application.http.protocol.HTTP`, as TCP's table # does. See the note in the class docstring. 80: ModuleDescriptor('pcapkit.protocols.application.http.protocol', 'HTTP'), - 8080: ModuleDescriptor('pcapkit.protocols.application.http.protocol', 'HTTP'), + + # L2TPv2 (RFC 2661) is UDP-borne, and v2 is the only version + # implemented, so the concrete class is bound rather than the + # abstract L2TP base. IANA protocol number 115 (L2TPv3, RFC 3931) + # stays unbound because that version has no class; once it has, + # it takes 115 and this entry becomes a version switch on the Ver + # nibble. See pcapkit.protocols.link.l2tp.protocol. + 1701: ModuleDescriptor('pcapkit.protocols.link.l2tpv2.protocol', 'L2TPv2'), # CoAP over DTLS (RFC 7252) is bound here, but it is not the only # fixed port a DTLS-borne protocol is registered at -- IANA also @@ -132,13 +139,7 @@ class UDP(Transport[Data_UDP, Schema_UDP], # pcapkit.protocols.application.dtls.protocol. 5684: ModuleDescriptor('pcapkit.protocols.application.dtls.protocol', 'DTLS'), - # L2TPv2 (RFC 2661) is UDP-borne, and v2 is the only version - # implemented, so the concrete class is bound rather than the - # abstract L2TP base. IANA protocol number 115 (L2TPv3, RFC 3931) - # stays unbound because that version has no class; once it has, - # it takes 115 and this entry becomes a version switch on the Ver - # nibble. See pcapkit.protocols.link.l2tp.protocol. - 1701: ModuleDescriptor('pcapkit.protocols.link.l2tpv2.protocol', 'L2TPv2'), + 8080: ModuleDescriptor('pcapkit.protocols.application.http.protocol', 'HTTP'), }, ) diff --git a/tests/project/test_protocol_member_order_2490_unit.py b/tests/project/test_protocol_member_order_2490_unit.py index 890b6b6982..2bb58846e9 100644 --- a/tests/project/test_protocol_member_order_2490_unit.py +++ b/tests/project/test_protocol_member_order_2490_unit.py @@ -13,11 +13,21 @@ class under ``pcapkit/protocols``, less the ``NotImplemented`` placeholders, * ``helpers`` -- rule b: the utilities the bodies of ``read()`` and ``make()`` name keep the order of first use, each reader directly followed by its maker. A registry fallback, the default of a ``getattr()`` or ``.get()`` there, is a - specific reader rather than a helper (#2490). - -Rule c, the enum-value order of the specific readers and makers, and subgroup d, -the last place of every helper ``read()``, ``make()`` and the registries do not -reach, are checked once their PRs land. + specific reader rather than a helper (#2490); +* ``specific`` -- rule c: the readers and makers of each method registry follow the + helpers, its readers before its makers, each set opening with the registry's + default and then in the registry's order; +* ``unreached`` -- subgroup d: every utility that ``read()``, ``make()`` and the + registries do not reach comes after all those they do; +* ``registry`` -- a registry map's entries are in the order of their keys' values: + an enum member's value, or an :class:`int` key's own. + +A method registry is a class-level dunder map, other than ``__proto__``, whose +:obj:`str` values name the ``_read_*`` and ``_make_*`` methods an f-string in the +class builds. Ordered by value, a registry also orders rule c by enum value, as the +page asks. A key the audit cannot value, such as the call that builds each key of +``PCAPNG.__option__``, leaves its map out of ``registry``, and rule c then follows +the registry's own order, as the page asks where there is no enum. A class that breaks a rule is an entry in :data:`KNOWN_EXCEPTIONS`, with the rules it breaks and the issue or PR whose reorder removes it. The table is checked in @@ -25,12 +35,16 @@ class under ``pcapkit/protocols``, less the ``NotImplemented`` placeholders, and so does an entry naming a class or a rule that does not exist, or a rule its class no longer breaks, so a reorder deletes its entry in the same change. -This module is unit-tier: it parses source and imports nothing it scans. +This module is unit-tier: it parses source and imports nothing it scans. To value +an enum key it imports the :mod:`pcapkit.const` module the scanned module imports +the enum from. """ from __future__ import annotations import ast +import enum +import importlib import unittest from typing import TYPE_CHECKING @@ -38,7 +52,10 @@ class no longer breaks, so a reorder deletes its entry in the same change. if TYPE_CHECKING: import pathlib - from typing import Iterator + from typing import Any, Callable, Iterator, Optional + + #: Resolves a name in a scanned module to the object it imports, else :data:`None`. + Lookup = Callable[[str], Optional[Any]] #: Test tier (#1958): parses every protocol module for a question about the tree, #: which does not differ by interpreter. @@ -62,6 +79,9 @@ class no longer breaks, so a reorder deletes its entry in the same change. 'data': '_make_data() is the first utility', 'pair': 'each _read_xxx() comes before its _make_xxx()', 'helpers': 'the helpers read() and make() name, in first-use order, each reader then its maker', + 'specific': "after the helpers, each registry's readers then its makers, default first, in registry order", + 'unreached': 'the utilities read(), make() and the registries do not reach come last', + 'registry': "a registry map's entries in the order of their keys' enum or int values", } #: Classes that break the order, as ``qualified name: (rules, remover)``: the @@ -173,8 +193,141 @@ def helpers(cls: 'ast.ClassDef', utilities: 'list[str]') -> 'list[str]': return order -def audit(cls: 'ast.ClassDef') -> 'dict[str, str]': - """The rules ``cls`` breaks, as ``kind: detail``; empty when it keeps the order.""" +def maps(cls: 'ast.ClassDef') -> 'Iterator[tuple[str, ast.Dict, Optional[str]]]': + """Every class-level dunder map literal of ``cls``, as ``(name, entries, default)``. + + The map is a :obj:`dict` display, or a call such as a + :class:`~collections.defaultdict` holding one; ``default`` is the :obj:`str` its + ``lambda`` factory returns, else :data:`None`. + + """ + for stmt in cls.body: + if isinstance(stmt, ast.Assign) and len(stmt.targets) == 1: + target, value = stmt.targets[0], stmt.value # type: ast.expr, Optional[ast.expr] + elif isinstance(stmt, ast.AnnAssign): + target, value = stmt.target, stmt.value + else: + continue + if not isinstance(target, ast.Name) or not _is_dunder(target.id): + continue + default = None + if isinstance(value, ast.Call): + factory = value.args[0] if value.args else None + if isinstance(factory, ast.Lambda) and isinstance(factory.body, ast.Constant) \ + and isinstance(factory.body.value, str): + default = factory.body.value + value = next((arg for arg in value.args if isinstance(arg, ast.Dict)), None) + if isinstance(value, ast.Dict): + yield target.id, value, default + + +def _prefixes(cls: 'ast.ClassDef') -> 'set[str]': + """What follows ``_read_`` or ``_make_`` in each f-string of ``cls`` naming a method.""" + found = set() # type: set[str] + for node in ast.walk(cls): + if isinstance(node, ast.JoinedStr) and len(node.values) > 1 \ + and isinstance(node.values[0], ast.Constant) and isinstance(node.values[1], ast.FormattedValue): + head = node.values[0].value + if isinstance(head, str) and _stem(head) is not None: + found.add(head[6:]) + return found + + +def registries(cls: 'ast.ClassDef', utilities: 'list[str]') -> 'list[tuple[str, str, list[str]]]': + """The method registries of ``cls``, as ``(map, prefix, stems)``. + + ``stems`` opens with the registry's default, the ``lambda`` factory's value or the + stem of the ``getattr()`` fallback, and follows with the stems of its entries in + their order. ``prefix`` is the f-string prefix that names the most of them. + + """ + prefixes = _prefixes(cls) + fallbacks = _fallbacks(cls, set(utilities)) + found = [] # type: list[tuple[str, str, list[str]]] + for name, node, default in maps(cls): + if name == '__proto__': + continue + values = [value.value for value in node.values + if isinstance(value, ast.Constant) and isinstance(value.value, str)] + hits = {prefix: sum(1 for value in values if _method(utilities, '_read_', prefix, value) + or _method(utilities, '_make_', prefix, value)) for prefix in prefixes} + prefix = max(sorted(hits), key=hits.__getitem__, default=None) + if prefix is None or not hits[prefix]: + continue + if default is None: + default = next((fallback[6 + len(prefix):] for fallback in sorted(fallbacks) + if _stem(fallback) is not None and fallback[6:].startswith(prefix)), None) + stems = [default] if default is not None else [] + found.append((name, prefix, list(dict.fromkeys(stems + values)))) + return found + + +def _method(utilities: 'list[str]', kind: 'str', prefix: 'str', stem: 'str') -> 'Optional[str]': + """The utility ``kind + prefix + stem`` names, as written or lower-cased, else :data:`None`.""" + for name in (f'{kind}{prefix}{stem}', f'{kind}{prefix}{stem.lower()}'): + if name in utilities: + return name + return None + + +def specific(cls: 'ast.ClassDef', utilities: 'list[str]') -> 'list[tuple[str, list[str]]]': + """Rule c's sets of ``cls``: each registry's readers, then its makers, in the order asked. + + Each set is ``(label, members)``, and ``members`` keeps the registry's order: + the default first, then each entry's stem, a stem shared by several entries at + its first. + + """ + sets = [] # type: list[tuple[str, list[str]]] + for name, prefix, stems in registries(cls, utilities): + for kind in ('_read_', '_make_'): + found = (_method(utilities, kind, prefix, stem) for stem in stems) + sets.append((f'{name} {kind}{prefix}*', list(dict.fromkeys(member for member in found if member)))) + return sets + + +def _key_value(key: 'Optional[ast.expr]', lookup: 'Optional[Lookup]') -> 'Optional[int]': + """The value a registry key orders by: an :class:`int` literal, or an enum member's value. + + :data:`None` for a key of any other shape, which no rule on the page orders. + + """ + if isinstance(key, ast.Constant) and isinstance(key.value, int) and not isinstance(key.value, bool): + return key.value + if isinstance(key, ast.Attribute) and isinstance(key.value, ast.Name) and lookup is not None: + owner = lookup(key.value.id) + if owner is None: + raise LookupError(f'{key.value.id} is not imported from pcapkit.const') + member = getattr(owner, key.attr) + if not isinstance(member, enum.Enum): + raise TypeError(f'{key.value.id}.{key.attr} is not an enum member') + return int(member) + return None + + +def module_lookup(path: 'pathlib.Path') -> 'Lookup': + """Resolve each name ``path`` imports from :mod:`pcapkit.const` at its top level.""" + tree = ast.parse(path.read_text(encoding='utf-8'), filename=str(path)) + names = {alias.asname or alias.name: (stmt.module, alias.name) + for stmt in tree.body if isinstance(stmt, ast.ImportFrom) and stmt.module + and stmt.module.startswith('pcapkit.const.') for alias in stmt.names} + + def lookup(name: 'str') -> 'Optional[Any]': + if name not in names: + return None + module, attr = names[name] + return getattr(importlib.import_module(module), attr) + + return lookup + + +def audit(cls: 'ast.ClassDef', lookup: 'Optional[Lookup]' = None) -> 'dict[str, str]': + """The rules ``cls`` breaks, as ``kind: detail``; empty when it keeps the order. + + ``lookup`` resolves the names a registry key takes its enum from; without it, + only :class:`int` keys are valued. + + """ order = members(cls) findings = {} # type: dict[str, str] highest = 0 @@ -204,9 +357,49 @@ def audit(cls: 'ast.ClassDef') -> 'dict[str, str]': if name.startswith('_read_') and maker in expected and position[maker] != position[name] + 1: findings['helpers'] = f'{name} and {maker} split by {utilities[position[name] + 1]}' break + + sets = specific(cls, utilities) + registry = {member for _, names in sets for member in names} + for fallback in _fallbacks(cls, set(utilities)): + stem = _stem(fallback) + registry.update([fallback] if stem is None else + (name for name in (f'_read_{stem}', f'_make_{stem}') if name in position)) + for label, names in sets: + actual = [name for name in utilities if name in names] + if actual != names: + findings.setdefault('specific', f'{label}: expected {names}, found {actual}') + for (label, readers), (_, makers) in zip(sets[::2], sets[1::2]): + if readers and makers and position[readers[-1]] > position[makers[0]]: + findings.setdefault('specific', f'{label}: {makers[0]} before {readers[-1]}') + helper = [name for name in expected if name not in registry] + first = min((position[name] for name in registry), default=None) + if helper and first is not None and position[helper[-1]] > first: + findings.setdefault('specific', f'{utilities[first]} before the helper {helper[-1]}') + + reached = {'_make_data', *expected, *registry} + last = max((position[name] for name in reached if name in position), default=0) + early = [name for name in utilities[:last] if name not in reached] + if early: + findings['unreached'] = f'{early[0]} before {utilities[last]}' + + for name, node, _ in maps(cls): + if name != '__proto__' and name not in {label.split()[0] for label, _ in sets}: + continue + keys = [key for key in node.keys if key is not None] + values = [_key_value(key, lookup) for key in keys] + if len(keys) == len(node.keys) and None not in values and values != sorted(values): + out = next(index for index in range(1, len(values)) if values[index] < values[index - 1]) + findings.setdefault('registry', f'{name}: {_key_text(keys[out])} after {_key_text(keys[out - 1])}') return findings +def _key_text(key: 'ast.expr') -> 'str': + """A valued registry key as written: ``Enum.MEMBER``, or the :class:`int` literal.""" + if isinstance(key, ast.Attribute) and isinstance(key.value, ast.Name): + return f'{key.value.id}.{key.attr}' + return repr(getattr(key, 'value', key)) + + def protocol_modules(root: 'pathlib.Path' = PROTOCOLS) -> 'Iterator[pathlib.Path]': """Every module under ``root``, outside :data:`SKIPPED_DIRS`.""" for path in sorted(root.rglob('*.py')): @@ -232,7 +425,7 @@ def visit(node: 'ast.AST', prefix: 'str') -> 'Iterator[tuple[str, ast.ClassDef]] def audit_tree(root: 'pathlib.Path' = ROOT) -> 'dict[str, dict[str, str]]': """Every class under ``root``'s protocol package, mapped to what :func:`audit` finds.""" - return {name: audit(node) + return {name: audit(node, module_lookup(path)) for path in protocol_modules(root / 'pcapkit' / 'protocols') for name, node in classes(path, root)} @@ -240,14 +433,20 @@ def audit_tree(root: 'pathlib.Path' = ROOT) -> 'dict[str, dict[str, str]]': class TestAudit(unittest.TestCase): """The audit finds each rule broken in a planted class body, and nothing in a sound one.""" - #: A class body in the order: ``_make_data()``, a helper pair and a registry fallback. + #: A class body in the order: ``_make_data()``, a helper pair, a registry + #: fallback, a method registry's readers and makers, and a helper nothing reaches. SOUND = ''' class Sound: + __option__ = collections.defaultdict(lambda: 'unknown', { + E.ALPHA: 'alpha', + E.BETA: 'beta', + }) @property def name(self): ... def read(self): self._read_addr() getattr(self, 'x', self._read_unknown) + getattr(self, f'_read_opt_{name}') def make(self): self._make_addr() def __index__(self): ... @@ -257,28 +456,66 @@ def _read_addr(self): ... def _make_addr(self): ... def _read_unknown(self): ... def _make_unknown(self): ... + def _read_opt_unknown(self): ... + def _read_opt_alpha(self): ... + def _read_opt_beta(self): ... + def _make_opt_unknown(self): ... + def _make_opt_alpha(self): ... + def _make_opt_beta(self): ... + def _decode_text(self): ... ''' + class Code(enum.IntEnum): + """The enum :attr:`SOUND`'s registry is keyed by.""" + + ALPHA = 1 + BETA = 2 + def check(self, source: 'str') -> 'dict[str, str]': node = ast.parse(source).body[0] assert isinstance(node, ast.ClassDef) - return audit(node) + return audit(node, {'E': self.Code}.get) def test_a_sound_class_has_no_finding(self) -> 'None': self.assertEqual(self.check(self.SOUND), {}) def test_each_rule_is_found(self) -> 'None': + readers = ' def _read_opt_alpha(self): ...\n def _read_opt_beta(self): ...\n' planted = { 'group': self.SOUND.replace(' def __index__(self): ...\n', '') + ' def __index__(self): ...\n', 'data': self.SOUND.replace(' @classmethod\n def _make_data(cls, data): ...\n', '') + ' @classmethod\n def _make_data(cls, data): ...\n', 'pair': self.SOUND.replace(' def _read_unknown(self): ...\n', '') + ' def _read_unknown(self): ...\n', 'helpers': self.SOUND.replace(' def _make_addr(self): ...\n', '') + ' def _make_addr(self): ...\n', + 'specific': self.SOUND.replace(readers, ''.join(reversed(readers.splitlines(keepends=True)))), + 'unreached': self.SOUND.replace(' def _decode_text(self): ...\n', '').replace( + ' def _read_addr(self): ...\n', ' def _decode_text(self): ...\n def _read_addr(self): ...\n'), + 'registry': self.SOUND.replace(" E.ALPHA: 'alpha',\n E.BETA: 'beta',\n", + " E.BETA: 'alpha',\n E.ALPHA: 'beta',\n"), } self.assertEqual(sorted(planted), sorted(RULES)) for kind, source in planted.items(): with self.subTest(rule=kind): + self.assertNotEqual(source, self.SOUND) self.assertIn(kind, self.check(source)) + # Each of the checks for rule c, subgroup d and the registry order fires alone. + for kind in ('specific', 'unreached', 'registry'): + with self.subTest(alone=kind): + self.assertEqual(sorted(self.check(planted[kind])), [kind]) + + def test_the_specific_sets(self) -> 'None': + node = ast.parse(self.SOUND).body[0] + assert isinstance(node, ast.ClassDef) + utilities = [name for name, group in members(node) if group == 3] + self.assertEqual(specific(node, utilities), [ + ('__option__ _read_opt_*', ['_read_opt_unknown', '_read_opt_alpha', '_read_opt_beta']), + ('__option__ _make_opt_*', ['_make_opt_unknown', '_make_opt_alpha', '_make_opt_beta']), + ]) + + def test_a_computed_key_is_not_valued(self) -> 'None': + source = self.SOUND.replace(" E.ALPHA: 'alpha',\n E.BETA: 'beta',\n", + " key(E.BETA): 'alpha',\n key(E.ALPHA): 'beta',\n") + self.assertEqual(self.check(source), {}) def test_a_fallback_is_not_a_helper(self) -> 'None': node = ast.parse(self.SOUND).body[0] @@ -324,6 +561,38 @@ def test_the_reference_is_in_order(self) -> 'None': self.assertEqual(helpers(node, utilities), pairs) self.assertEqual(utilities, ['_make_data', *pairs]) + def test_the_reference_registries_are_checked(self) -> 'None': + # Every key of these maps is valued, so the registry rule orders them. + cases = { + ('internet/internet.py', 'Internet'): {'__proto__'}, + ('internet/ipv4/protocol.py', 'IPv4'): {'__option__'}, + ('link/link.py', 'Link'): {'__proto__'}, + ('transport/tcp/protocol.py', 'TCP'): {'__proto__', '__option__', '__mp_option__'}, + ('transport/udp/protocol.py', 'UDP'): {'__proto__'}, + } + for (rel, name), expected in cases.items(): + with self.subTest(cls=name): + path = PROTOCOLS / rel + node = next(node for qual, node in classes(path) if qual.endswith(f'.{name}')) + found = {label: [_key_value(key, module_lookup(path)) for key in entries.keys] + for label, entries, _ in maps(node)} + self.assertLessEqual(expected, set(found)) + for label in expected: + self.assertNotIn(None, found[label], label) + self.assertEqual(found[label], sorted(found[label]), label) + + def test_the_page_examples(self) -> 'None': + # Rule c: _read_opt_unassigned() opens the readers of IPv4. + path = PROTOCOLS / 'internet' / 'ipv4' / 'protocol.py' + node = dict(classes(path))['pcapkit.protocols.internet.ipv4.protocol.IPv4'] + utilities = [member for member, group in members(node) if group == 3] + readers = dict(specific(node, utilities))['__option__ _read_opt_*'] + self.assertEqual(readers[0], '_read_opt_unassigned') + # Subgroup d: _decode_text() of HTTPv1, which only a helper calls, is the last utility. + path = PROTOCOLS / 'application' / 'httpv1' / 'protocol.py' + node = dict(classes(path))['pcapkit.protocols.application.httpv1.protocol.HTTPv1'] + self.assertEqual([member for member, group in members(node) if group == 3][-1], '_decode_text') + def test_every_exception_is_still_broken(self) -> 'None': stale = sorted(f'{name} [{kind}] ({remover})' for name, (kinds, remover) in KNOWN_EXCEPTIONS.items()