Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ This resolves [#548](https://github.com/JarryShaw/PyPCAPKit/issues/548), which r
- message text: the `epb_flags` and `pack_flags` length errors say `expected 4`, the length they check, rather than `expected 8` ([#2168](https://github.com/JarryShaw/PyPCAPKit/issues/2168), [#2179](https://github.com/JarryShaw/PyPCAPKit/pull/2179)), and the IPv4 TS option's unknown-flag warning and the Mobility Header's prefix error spell `timestamp` and `mobile` ([#2171](https://github.com/JarryShaw/PyPCAPKit/issues/2171), [#2189](https://github.com/JarryShaw/PyPCAPKit/pull/2189)).
- IPX dissects its payload from a registry of its own, `IPX.__proto__`, keyed by IPX Packet Type, where it looked the Packet Type up as an IP protocol number, so a type 17 (NCP) payload parsed as `IPX:UDP` and type 4 as `IPX:IPv4`. No IPX-native protocol ships, so the payload is `Raw` unless one is registered ([#2169](https://github.com/JarryShaw/PyPCAPKit/issues/2169), [#2187](https://github.com/JarryShaw/PyPCAPKit/pull/2187)).
- HIP's `ECHO_RESPONSE_UNSIGNED` maker builds an `EchoResponseUnsignedParameter` schema, where it built the request's; the octets were already right ([#2170](https://github.com/JarryShaw/PyPCAPKit/issues/2170), [#2188](https://github.com/JarryShaw/PyPCAPKit/pull/2188)).
- PCAP-NG's `make(block=<bytes>)` parses the block out of the raw octets, as a block read from a capture is, where every block type raised a bare `AttributeError` (`'bytes' object has no attribute 'length'`) from `read()`; octets that hold no block raise `ProtocolError` ([#2354](https://github.com/JarryShaw/PyPCAPKit/issues/2354)).

### pcapkit.toolkit

Expand Down
5 changes: 5 additions & 0 deletions docs/source/changelog/1.5.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2755,6 +2755,11 @@ Fixed
* HIP's ``ECHO_RESPONSE_UNSIGNED`` maker builds an ``EchoResponseUnsignedParameter``
schema, where it built the request's; the octets were already right (:issue:`2170`,
:pr:`2188`).
* PCAP-NG's ``make(block=<bytes>)`` parses the block out of the raw octets, as a
block read from a capture is, where every block type raised a bare
``AttributeError`` (``'bytes' object has no attribute 'length'``) from
:meth:`~pcapkit.protocols.misc.pcapng.PCAPNG.read`; octets that hold no block raise
:exc:`.ProtocolError` (:issue:`2354`).

pcapkit.toolkit
---------------
Expand Down
17 changes: 17 additions & 0 deletions pcapkit/protocols/misc/pcapng.py
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,23 @@ def unpack(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_PCAPN
the packet data is not available in the current instance.

"""
# NOTE: A block given to ``make()`` as raw :obj:`bytes` is packed
# verbatim, so the made header holds those octets where :meth:`read`
# expects the block schema. Parse the block back out of the packed
# octets instead, as a block read from a capture would be, so that
# ``make(block=<bytes>)`` builds the block the octets describe, or
# fails with a :exc:`ProtocolError` when they describe none, as
# :meth:`Protocol.unpack <pcapkit.protocols.protocol.Protocol.unpack>`
# does for a raw item it cannot parse (#2354).
header = cast('Optional[Schema_PCAPNG]', self.__header__)
block = cast('object', header.block) if header is not None else None
if isinstance(block, bytes):
self.__header__ = None # type: ignore[assignment]
try:
return self.unpack(length, **kwargs)
except (BaseError, struct.error) as error: # pylint: disable=no-member
raise ProtocolError(f'{type(self).__name__}: malformed raw block given to make(): {error}') from error

if cast('Optional[Schema_PCAPNG]', self.__header__) is None:
packet = kwargs.get('__packet__', {}) # packet data

Expand Down
74 changes: 74 additions & 0 deletions tests/protocols/misc/test_pcapng_raw_bytes_block_2354_unit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
"""GitHub issue #2354: ``PCAPNG.make(block=<bytes>)`` parses the raw block.

``make()`` types ``block`` as accepting :obj:`bytes`, and packs a raw block
verbatim, but :meth:`~pcapkit.protocols.misc.pcapng.PCAPNG.read` then read the
made header, whose ``block`` was those :obj:`bytes` rather than a block schema,
and raised ``AttributeError: 'bytes' object has no attribute 'length'`` for
every block type. The raw block is now parsed out of the packed octets, as a
block read from a capture is, and octets that hold no block raise
:exc:`~pcapkit.utilities.exceptions.ProtocolError`.

:mod:`pcapkit` is imported inside each test, after
:func:`~tests._support.reimport_once_per_class`.

"""

import struct
import unittest

from tests.protocols.misc.test_pcapng_roundtrip2_unit import PCAPNGTestCase

#: Secrets Type no registry names.
UNKNOWN_SECRETS = 0x12345678


def _body(body: 'bytes') -> 'bytes':
"""A little-endian block body around ``body``, after the Block Type."""
body += bytes(-len(body) % 4)
total = 12 + len(body)
return struct.pack('<I', total) + body + struct.pack('<I', total)


class TestPCAPNGRawBytesBlock(PCAPNGTestCase):
"""Pin how a block given to ``make()`` as raw bytes builds."""

def _make(self, btype: 'int', block: 'bytes') -> 'object':
from pcapkit.protocols.misc.pcapng import PCAPNG

return PCAPNG(num=2, sct=1, ctx=self._context(), type=btype, block=block)

def test_dsb_from_raw_bytes(self) -> None:
raw = _body(struct.pack('<II', UNKNOWN_SECRETS, 5) + b'abcde')
made = self._make(10, raw)
self.assertEqual(made.data, struct.pack('<I', 10) + raw)
self.assertEqual(made.info.secrets_type, UNKNOWN_SECRETS)
self.assertEqual(made.info.secrets_length, 5)
self.assertEqual(made.info.secrets_data.data, b'abcde')

def test_matches_the_parsed_block(self) -> None:
cases = {
1: _body(struct.pack('<HHI', 1, 0, 0x40000)),
6: _body(struct.pack('<IIIII', 0, 0, 0, 4, 4) + b'\x01\x02\x03\x04'),
10: _body(struct.pack('<II', UNKNOWN_SECRETS, 4) + b'abcd'),
}
for btype, raw in cases.items():
with self.subTest(type=btype):
octets = struct.pack('<I', btype) + raw
made = self._make(btype, raw)
parsed = self._parse(octets)
self.assertEqual(made.data, octets)
self.assertEqual(made.info, parsed.info)

def test_raw_bytes_holding_no_block(self) -> None:
from pcapkit.utilities.exceptions import ProtocolError

for raw in (b'', bytes(8)):
with self.subTest(raw=raw):
with self.assertRaises(ProtocolError) as context:
self._make(10, raw)
self.assertIn('malformed raw block given to make()', str(context.exception))


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