From 901e123d440e4e11e43cd07f0cbcc087a3a04094 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Wed, 7 Oct 2026 14:58:38 -0400 Subject: [PATCH] fix(pcapng): constructed and rebuilt blocks round-trip (#1267, #1268, #1269, #1270, #1271) - #1267: reset the option counter between the make pass and the re-parse in PCAPNG.__post_init__, so an option with an "only one" guard no longer trips it on itself. - #1268: if_IPv6addr declares its 17-octet length, not 8. - #1269: _make_block_isb sets _isb_interface_id before building the isb_starttime / isb_endtime options. - #1270: EPB, SPB and PB rebuilt from their data model keep the packet data, taken from the parsed info's packet octets cut to captured_len. - #1271: the TLS and WireGuard key-log writers end lines with a newline and no longer stamp the current time; a parsed log keeps its text and is written back verbatim, comments and line order included. Removes the 33 EXPECTED_FAILURES entries these close. The two key-log entries now record #1279 (the DSB is never padded) instead. --- pcapkit/protocols/data/misc/pcapng.py | 8 + pcapkit/protocols/misc/pcapng.py | 79 ++++++- .../misc/test_pcapng_roundtrip_unit.py | 209 ++++++++++++++++++ tests/protocols/misc/test_pcapng_unit.py | 4 +- tests/protocols/test_option_roundtrip_unit.py | 99 +++------ 5 files changed, 315 insertions(+), 84 deletions(-) create mode 100644 tests/protocols/misc/test_pcapng_roundtrip_unit.py diff --git a/pcapkit/protocols/data/misc/pcapng.py b/pcapkit/protocols/data/misc/pcapng.py index a2e7c4d663..0df72152a0 100644 --- a/pcapkit/protocols/data/misc/pcapng.py +++ b/pcapkit/protocols/data/misc/pcapng.py @@ -769,6 +769,10 @@ class TLSKeyLog(DSBSecrets): entries: 'dict[TLSKeyLabel, OrderedMultiDict[bytes, bytes]]' if TYPE_CHECKING: + #: TLS key log text as the block carries it, comments and line order + #: included. Set on parsing only, and written back verbatim on rebuild. + data: 'str' + def __init__(self, entries: 'dict[TLSKeyLabel, OrderedMultiDict[bytes, bytes]]') -> 'None': ... @@ -780,6 +784,10 @@ class WireGuardKeyLog(DSBSecrets): entries: 'OrderedMultiDict[WireGuardKeyLabel, bytes]' if TYPE_CHECKING: + #: WireGuard key log text as the block carries it, comments included. + #: Set on parsing only, and written back verbatim on rebuild. + data: 'str' + def __init__(self, entries: 'OrderedMultiDict[WireGuardKeyLabel, bytes]') -> 'None': ... diff --git a/pcapkit/protocols/misc/pcapng.py b/pcapkit/protocols/misc/pcapng.py index 54ab9e509c..d0a057e764 100644 --- a/pcapkit/protocols/misc/pcapng.py +++ b/pcapkit/protocols/misc/pcapng.py @@ -20,7 +20,6 @@ import enum import io import math -import os import platform import re import struct @@ -1237,6 +1236,12 @@ def __post_init__(self, file: 'Optional[IO[bytes] | bytes]' = None, length: 'Opt self._data = self.pack(**kwargs) #: io.BytesIO: Source packet stream. self._file = io.BytesIO(self._data) + + # NOTE: The block just built is parsed again below on this same + # instance, and the parse counts each option afresh against its + # "only one" guard -- so the count left by the make pass must not + # carry over into it. + self._opt = collections.Counter() else: _read = True #: io.BytesIO: Source packet stream. @@ -1572,6 +1577,34 @@ def _make_data(cls, data: 'Data_PCAPNG') -> 'dict[str, Any]': # type: ignore[ov 'block': data, } + @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. + + Args: + block: Block data model. + + Returns: + The captured packet octets the block was parsed from, as + :attr:`~pcapkit.protocols.data.protocol.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. @@ -3492,6 +3525,10 @@ def _read_secrets_tls(self, schema: 'Schema_TLSKeyLog', *, secrets = Data_TLSKeyLog( 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_wireguard(self, schema: 'Schema_WireGuardKeyLog', *, @@ -3509,6 +3546,10 @@ def _read_secrets_wireguard(self, schema: 'Schema_WireGuardKeyLog', *, 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', *, @@ -3757,6 +3798,7 @@ def _make_block_epb(self, block: 'Optional[Data_EnhancedPacketBlock]' = None, *, timestamp = block.timestamp_epoch captured_len = block.captured_len original_len = block.original_len + packet_data = self._make_packet_data(block) options = block.options timestamp_high, timestamp_low = self._make_timestamp(timestamp, interface_id=interface_id) @@ -3805,6 +3847,7 @@ def _make_block_spb(self, block: 'Optional[Data_SimplePacketBlock]' = None, *, """ if block is not None: original_len = block.original_len + packet_data = self._make_packet_data(block) if original_len is None: original_len = len(packet_data) @@ -3878,6 +3921,9 @@ def _make_block_isb(self, block: 'Optional[Data_InterfaceStatisticsBlock]' = Non options = 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 options is not None: options_value, total_length = self._make_pcapng_options(options, namespace='isb') @@ -4094,6 +4140,7 @@ def _make_block_packet(self, block: 'Optional[Data_PacketBlock]' = None, *, timestamp = block.timestamp_epoch captured_len = block.captured_len original_len = block.original_len + packet_data = self._make_packet_data(block) options = block.options timestamp_high, timestamp_low = self._make_timestamp(timestamp, interface_id=interface_id) @@ -4427,7 +4474,7 @@ def _make_option_if_ipv6(self, type: 'Enum_OptionType', option: 'Optional[Data_I return Schema_IF_IPv6AddrOption( type=type, - length=8, + length=17, interface=interface, ) @@ -5774,7 +5821,10 @@ def _make_secrets_tls(self, type: 'Enum_SecretsType', secrets: 'Optional[Data_TL Args: type: Secrets type. - secrets: Secrets data model. + 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. **kwargs: Arbitrary keyword arguments. @@ -5785,15 +5835,20 @@ def _make_secrets_tls(self, type: 'Enum_SecretsType', secrets: 'Optional[Data_TL from pcapkit import __version__ if secrets is not None: + text = secrets.get('data') + if isinstance(text, str): + return Schema_TLSKeyLog( + data=text, + ) entries = secrets.entries if entries is None: entries = {} - data = [f'# generated by PyPCAPKit v{__version__} at {datetime.datetime.now().isoformat()}{os.sep}'] # type: list[str] + data = [f'# generated by PyPCAPKit v{__version__}\n'] # type: list[str] for label, entry in entries.items(): for k, v in entry.items(multi=True): - data.append(f'{label.name} {k.hex()} {v.hex()}{os.sep}') + data.append(f'{label.name} {k.hex()} {v.hex()}\n') return Schema_TLSKeyLog( data=''.join(data), @@ -5806,7 +5861,10 @@ def _make_secrets_wireguard(self, type: 'Enum_SecretsType', secrets: 'Optional[D Args: type: Secrets type. - secrets: Secrets data model. + 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. **kwargs: Arbitrary keyword arguments. @@ -5817,14 +5875,19 @@ def _make_secrets_wireguard(self, type: 'Enum_SecretsType', secrets: 'Optional[D from pcapkit import __version__ if secrets is not None: + text = secrets.get('data') + if isinstance(text, str): + return Schema_WireGuardKeyLog( + data=text, + ) entries = secrets.entries if entries is None: entries = OrderedMultiDict() - data = [f'# generated by PyPCAPKit v{__version__} at {datetime.datetime.now().isoformat()}{os.sep}'] # type: list[str] + data = [f'# generated by PyPCAPKit v{__version__}\n'] # type: list[str] for label, value in entries.items(multi=True): - data.append(f'{label.name} = {base64.b64encode(value).decode()}{os.sep}') + data.append(f'{label.name} = {base64.b64encode(value).decode()}\n') return Schema_WireGuardKeyLog( data=''.join(data), diff --git a/tests/protocols/misc/test_pcapng_roundtrip_unit.py b/tests/protocols/misc/test_pcapng_roundtrip_unit.py new file mode 100644 index 0000000000..08675f86bc --- /dev/null +++ b/tests/protocols/misc/test_pcapng_roundtrip_unit.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +"""PCAP-NG blocks build from keywords, and rebuild from their data model. + +Five defects, each of which stopped a block from surviving +:meth:`~pcapkit.protocols.protocol.ProtocolBase.from_data`: + +* #1267 -- :meth:`PCAPNG.__post_init__ ` + packs a constructed block and then parses it on the same instance, and the + make pass's option count carried into the parse pass, so every option with + an "only one" guard tripped it on itself. +* #1268 -- ``if_IPv6addr`` declared a length of 8 for its 17-octet value. +* #1269 -- ``isb_starttime`` / ``isb_endtime`` read an interface ID only the + parse path set. +* #1270 -- an EPB, SPB or PB rebuilt from its data model lost its packet data. +* #1271 -- the TLS and WireGuard key-log writers ended lines with + :data:`os.sep` and stamped the current time, and the data model kept neither + the comments nor the order of the lines. + +Every case builds its own octets in memory and reads no capture. + +:mod:`pcapkit` is imported inside each test, after +:func:`~tests._support.reimport_once_per_class`, not at module load, so that a +module that purged it earlier does not leave these tests holding stale classes. + +""" + +import unittest +from typing import TYPE_CHECKING + +from tests._support import reimport_once_per_class + +if TYPE_CHECKING: + from typing import Any + +#: A little-endian Enhanced Packet Block on interface 0, timestamp 0, carrying +#: the four octets ``abcd`` -- the repro of #1270. +EPB = bytes.fromhex('06000000 24000000 00000000 00000000 00000000' + '04000000 04000000 61626364 24000000') +#: A Simple Packet Block carrying ``abcde``, so that it needs three pad octets. +SPB = bytes.fromhex('03000000 18000000 05000000 6162636465000000 18000000') +#: A Packet Block (obsolete) on interface 0 carrying ``abcd``. +PB = bytes.fromhex('02000000 24000000 0000 0000 00000000 00000000' + '04000000 04000000 61626364 24000000') +#: An Interface Statistics Block carrying ``isb_starttime`` -- the repro of +#: #1269. +ISB = bytes.fromhex('05000000 28000000 00000000 00000000 00000000' + '0200 0800 00000000 00000000 0000 0000 28000000') + +#: A TLS key log whose labels interleave and which carries comments -- the +#: order and the comments are what #1271's data model used to drop. Padded to a +#: multiple of four octets, so that the Decryption Secrets Block needs no pad. +TLS_LOG = ('# a comment\n' + 'CLIENT_RANDOM 00 11\n' + 'SERVER_HANDSHAKE_TRAFFIC_SECRET 22 33\n' + '# another\n' + 'CLIENT_RANDOM 44 55\n') +#: A WireGuard key log with a comment, likewise four-octet aligned. +WG_LOG = ('# wg ok\n' + 'LOCAL_STATIC_PRIVATE_KEY = AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=\n') + + +def _dsb(code: 'int', text: 'str') -> 'bytes': + """A little-endian Decryption Secrets Block carrying ``text``.""" + body = text.encode('ascii') + assert len(body) % 4 == 0, len(body) + length = 20 + len(body) + return b''.join(( + (10).to_bytes(4, 'little'), length.to_bytes(4, 'little'), + code.to_bytes(4, 'little'), len(body).to_bytes(4, 'little'), + body, length.to_bytes(4, 'little'), + )) + + +class TestPCAPNGRoundTrip(unittest.TestCase): + """Pin that a PCAP-NG block survives being built and rebuilt.""" + + def setUp(self) -> None: + reimport_once_per_class(self) + + def _context(self) -> 'Any': + """A one-section context with one Ethernet interface, as the engine builds it.""" + from pcapkit.const.pcapng.block_type import BlockType + from pcapkit.const.reg.linktype import LinkType + from pcapkit.foundation.engines.pcapng import Context + from pcapkit.protocols.misc.pcapng import PCAPNG + + section = PCAPNG(num=0, sct=1, ctx=None, type=BlockType.Section_Header_Block, block={}) + context = Context(section.info) # type: Any + interface = PCAPNG(num=1, sct=1, ctx=context, type=BlockType.Interface_Description_Block, + block={'linktype': LinkType.ETHERNET, 'snaplen': 0x40000}) + context.interfaces.append(interface.info) + return context + + def _rebuild(self, octets: 'bytes') -> 'bytes': + """Parse ``octets`` as one block and rebuild it from its data model.""" + import warnings + + from pcapkit.protocols.misc.pcapng import PCAPNG + + context = self._context() + with warnings.catch_warnings(): + # the Packet Block's maker warns that the block is obsolete + warnings.simplefilter('ignore') + parsed = PCAPNG(octets, len(octets), num=2, sct=1, ctx=context) + return PCAPNG.from_data(parsed.info, num=2, sct=1, ctx=context).data + + def test_an_option_with_an_only_one_guard_builds(self) -> None: + """#1267: the make pass's option count does not reach the parse pass.""" + from pcapkit.const.pcapng.block_type import BlockType + from pcapkit.const.pcapng.option_type import OptionType + from pcapkit.protocols.misc.pcapng import PCAPNG + + block = PCAPNG(num=2, sct=1, ctx=self._context(), type=BlockType.Interface_Description_Block, + block={'linktype': 1, 'snaplen': 65535, + 'options': [(OptionType.if_name, {'name': 'eth0'})]}) + self.assertEqual(block.info.options[OptionType.if_name].name, 'eth0') + self.assertEqual(self._rebuild(block.data), block.data) + + def test_a_repeated_only_one_option_is_still_rejected(self) -> None: + """#1267: the guard itself still holds within one pass.""" + from pcapkit.const.pcapng.block_type import BlockType + from pcapkit.const.pcapng.option_type import OptionType + from pcapkit.protocols.misc.pcapng import PCAPNG + from pcapkit.utilities.exceptions import ProtocolError + + with self.assertRaisesRegex(ProtocolError, r'\[if_name\] option must be only one, but 2 found'): + PCAPNG(num=2, sct=1, ctx=self._context(), type=BlockType.Interface_Description_Block, + block={'linktype': 1, 'snaplen': 65535, + 'options': [(OptionType.if_name, {'name': 'eth0'}), + (OptionType.if_name, {'name': 'eth1'})]}) + + def test_if_ipv6addr_is_seventeen_octets(self) -> None: + """#1268: ``if_IPv6addr`` declares the 17 octets it carries.""" + from pcapkit.const.pcapng.block_type import BlockType + from pcapkit.const.pcapng.option_type import OptionType + from pcapkit.protocols.misc.pcapng import PCAPNG + + block = PCAPNG(num=2, sct=1, ctx=self._context(), type=BlockType.Interface_Description_Block, + block={'linktype': 1, 'snaplen': 65535, + 'options': [(OptionType.if_IPv6addr, {'interface': '2001:db8::1/64'})]}) + # 20 octets of block (16 before the options), a 4-octet option header, 17 octets of value and + # 3 of padding + self.assertEqual(len(block.data), 44) + self.assertEqual(block.data[16:20], bytes.fromhex('0500 1100')) + self.assertEqual(str(block.info.options[OptionType.if_IPv6addr].interface), '2001:db8::1/64') + + def test_isb_starttime_rebuilds(self) -> None: + """#1269: the ISB time options build from the block's own interface ID.""" + self.assertEqual(self._rebuild(ISB), ISB) + + def test_packet_blocks_keep_their_packet_data(self) -> None: + """#1270: the packet octets survive ``from_data``.""" + for name, octets in (('EPB', EPB), ('SPB', SPB), ('PB', PB)): + with self.subTest(block=name): + self.assertEqual(self._rebuild(octets), octets) + + def test_an_empty_epb_rebuilds_without_growing(self) -> None: + """#1270: a ``captured_len`` of zero rebuilds as zero packet octets. + + The parsed ``packet`` of such a block can hold the trailing Block Total + Length (#1275), and writing that back would make the block 36 octets. + + """ + import struct + + for original_len in (0, 60): + with self.subTest(original_len=original_len): + body = struct.pack(' None: + """#1271: a parsed key log is written back as it was read.""" + from pcapkit.const.pcapng.secrets_type import SecretsType + + for name, code, text in (('TLS', SecretsType.TLS_Key_Log, TLS_LOG), + ('WireGuard', SecretsType.WireGuard_Key_Log, WG_LOG)): + with self.subTest(secrets=name): + octets = _dsb(code, text) + self.assertEqual(self._rebuild(octets), octets) + + def test_key_log_writers_end_lines_with_newlines(self) -> None: + """#1271: entries built from keywords parse back, with no wall-clock time.""" + from pcapkit.const.pcapng.secrets_type import SecretsType + from pcapkit.corekit.multidict import OrderedMultiDict + from pcapkit.protocols.misc.pcapng import PCAPNG, TLSKeyLabel, WireGuardKeyLabel + from pcapkit.protocols.schema.misc.pcapng import TLSKeyLog, WireGuardKeyLog + + proto = PCAPNG.__new__(PCAPNG) + tls = proto._make_secrets_tls(SecretsType.TLS_Key_Log, entries={ + TLSKeyLabel.CLIENT_RANDOM: OrderedMultiDict([(b'\x00\x11', b'\x22\x33')])}) + self.assertTrue(tls.data.endswith('\nCLIENT_RANDOM 0011 2233\n'), tls.data) + self.assertNotRegex(tls.data, r'\d{4}-\d\d-\d\dT') + parsed_tls = TLSKeyLog.unpack(tls.pack(), packet={'__length__': len(tls.pack())}) + self.assertEqual(list(parsed_tls.entries[TLSKeyLabel.CLIENT_RANDOM].items(multi=True)), + [(b'\x00\x11', b'\x22\x33')]) + + key = bytes(range(32)) + wg = proto._make_secrets_wireguard(SecretsType.WireGuard_Key_Log, entries=OrderedMultiDict( + [(WireGuardKeyLabel.LOCAL_STATIC_PRIVATE_KEY, key)])) + self.assertTrue(wg.data.endswith('\n'), wg.data) + self.assertNotRegex(wg.data, r'\d{4}-\d\d-\d\dT') + parsed_wg = WireGuardKeyLog.unpack(wg.pack(), packet={'__length__': len(wg.pack())}) + self.assertEqual(list(parsed_wg.entries.items(multi=True)), + [(WireGuardKeyLabel.LOCAL_STATIC_PRIVATE_KEY, key)]) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/protocols/misc/test_pcapng_unit.py b/tests/protocols/misc/test_pcapng_unit.py index e8eb129b2f..14ef160670 100644 --- a/tests/protocols/misc/test_pcapng_unit.py +++ b/tests/protocols/misc/test_pcapng_unit.py @@ -812,7 +812,7 @@ def test_pcapng_interface_option_constructors_and_scope_guards(self) -> None: self.assertEqual(pcapng._make_option_if_ipv4(OptionType.if_IPv4addr, interface='192.0.2.1/24').to_dict()['length'], 8) self.assertEqual(pcapng._make_option_if_ipv6(OptionType.if_IPv6addr, - interface='2001:db8::1/64').to_dict()['length'], 8) + interface='2001:db8::1/64').to_dict()['length'], 17) self.assertEqual(pcapng._make_option_if_mac(OptionType.if_MACaddr, interface='00:01:02:03:04:05').to_dict()['interface'], b'\x00\x01\x02\x03\x04\x05') @@ -2228,7 +2228,7 @@ def assert_duplicate(method: str, code: OptionType, block: BlockType, **kwargs: ('_make_option_if_ipv4', OptionType.if_IPv4addr, DummyData(interface='192.0.2.9/24'), 'length', 8), ('_make_option_if_ipv6', OptionType.if_IPv6addr, - DummyData(interface='2001:db8::9/64'), 'length', 8), + DummyData(interface='2001:db8::9/64'), 'length', 17), ('_make_option_if_mac', OptionType.if_MACaddr, DummyData(interface='00:01:02:03:04:06'), 'interface', b'\x00\x01\x02\x03\x04\x06'), ('_make_option_if_eui', OptionType.if_EUIaddr, diff --git a/tests/protocols/test_option_roundtrip_unit.py b/tests/protocols/test_option_roundtrip_unit.py index 467911a47a..845fd7372d 100644 --- a/tests/protocols/test_option_roundtrip_unit.py +++ b/tests/protocols/test_option_roundtrip_unit.py @@ -393,82 +393,33 @@ class Gap(NamedTuple): # -- PCAP-NG -------------------------------------------------------------- - # ``PCAPNG.__post_init__`` packs and then re-parses *on the same instance*, - # and both halves share the per-instance option counter: the make path - # increments it, the read path then trips its own "only one of these" - # guard on the option it just built. Twenty-seven options have such a - # guard; the ten that construct are the ones that do not. - **{ - f'pcapng-option/{name}': Gap( - 'CONSTRUCT', 'option must be only one', - 'pcapkit/protocols/misc/pcapng.py:1078 packs and :1088 re-parses on ' - 'one instance, sharing self._opt; incremented at :3941, checked at ' - ':2175') - for name in ( - 'if_name_2', 'if_description_3', 'if_MACaddr_6', 'if_EUIaddr_7', - 'if_speed_8', 'if_tsresol_9', 'if_tzone_10', 'if_filter_11', - 'if_os_12', 'if_fcslen_13', 'if_tsoffset_14', 'if_hardware_15', - 'if_txspeed_16', 'if_rxspeed_17', - 'epb_flags_2', 'epb_dropcount_4', 'epb_packetid_5', 'epb_queue_6', - 'ns_dnsname_2', 'ns_dnsIP4addr_3', 'ns_dnsIP6addr_4', - 'isb_ifrecv_4', 'isb_ifdrop_5', 'isb_filteraccept_6', - 'isb_osdrop_7', 'isb_usrdeliv_8', - 'pack_flags_2', - ) - }, - - # ``_make_option_if_ipv6`` hardcodes ``length=8``, copied from its IPv4 - # sibling where 8 is right, while ``IPv6InterfaceField`` is 17 octets. The - # option packs to 21, the block total becomes 41, and no argument the caller - # can pass changes it. - 'pcapng-option/if_IPv6addr_5': Gap( - 'CONSTRUCT', 'invalid length: 41', - 'pcapkit/protocols/misc/pcapng.py:4190 -- length=8 for a 17-octet ' - 'IPv6InterfaceField'), - - # ``_isb_interface_id`` is read by these two make-side constructors but - # assigned only by ``_read_block_isb``. - 'pcapng-option/isb_starttime_2': Gap( - 'CONSTRUCT', "no attribute '_isb_interface_id'", - 'pcapkit/protocols/misc/pcapng.py:4991 -- reads an attribute set only ' - 'at :1790, on the parse path'), - 'pcapng-option/isb_endtime_3': Gap( - 'CONSTRUCT', "no attribute '_isb_interface_id'", - 'pcapkit/protocols/misc/pcapng.py:5024 -- as above'), - - # The three packet-carrying blocks lose their payload on the way back: - # ``_make_block_*`` never restores ``packet_data``, and it could not, since - # the data model has no field to keep it in -- the octets go to the - # next-layer dissector and survive only as the decoded chain. The rebuilt - # block keeps ``captured_len`` while carrying no data, so it is malformed - # rather than merely shorter. + # Thirty-three PCAP-NG entries used to be recorded here, for four defects + # that #1267-#1270 fixed. ``PCAPNG.__post_init__`` carried the make pass's + # option count into its own re-parse, so every option with an "only one" + # guard tripped it (27 options). ``if_IPv6addr`` declared 8 octets for 17. + # ``isb_starttime``/``isb_endtime`` read an interface ID that only the parse + # path set. And an EPB, SPB or PB rebuilt from its data model lost its + # packet data. All of them round-trip now. + + # The two key-log secrets used to fail for #1271, which is fixed: lines + # ended with ``os.sep``, and the body carried ``datetime.now()``. That + # unmasked #1279: ``_make_block_dsb`` never pads the secrets data, so the + # newline-terminated logs this suite builds give a Block Total Length that + # is not a multiple of four. The logs' header comment embeds + # ``pcapkit.__version__``, so the length varies by release and the + # fragment names only the block and the check. + # + # A release whose version string happens to make the log a multiple of four + # octets long turns these 'OK' without #1279 being fixed. If they start + # passing, check the log length before deleting them. **{ - f'pcapng-block/{name}': Gap( - 'MISMATCH', '', - 'pcapkit/protocols/misc/pcapng.py:3514, :3565, :3851 -- ' - 'packet_data is not restored, and ' - 'pcapkit/protocols/data/misc/pcapng.py:442, :474, :895 have no ' - 'field to restore it from') - for name in ('Enhanced_Packet_Block', 'Simple_Packet_Block', 'Packet_Block') + f'pcapng-secrets/{name}': Gap( + 'CONSTRUCT', ('PCAP-NG: [Block 10]', 'invalid length'), + 'pcapkit/protocols/misc/pcapng.py _make_block_dsb -- the block ' + 'length omits the pad that DecryptionSecretsBlock.padding_data in ' + 'pcapkit/protocols/schema/misc/pcapng.py reads (#1279)') + for name in ('TLS_Key_Log', 'WireGuard_Key_Log') }, - - # The two key-log secrets writers terminate each line with ``os.sep`` -- a - # forward slash on POSIX -- where a newline is meant, while the readers - # split on newlines. So the whole log parses as one comment line and every - # entry is lost. They also stamp the current time into the body with no way - # to override it, which is why these two are the only cases in the suite - # whose failure detail is not stable, and why the generator's - # Decryption Secrets Block case uses a ZigBee key instead. - 'pcapng-secrets/TLS_Key_Log': Gap( - 'MISMATCH', '', - 'pcapkit/protocols/misc/pcapng.py:5553 and :5556 -- os.sep as a line ' - 'terminator, against splitlines() at ' - 'pcapkit/protocols/schema/misc/pcapng.py:1480; and datetime.now() in ' - 'the payload'), - 'pcapng-secrets/WireGuard_Key_Log': Gap( - 'MISMATCH', '', - 'pcapkit/protocols/misc/pcapng.py:5585 and :5587, against ' - 'pcapkit/protocols/schema/misc/pcapng.py:1519; and datetime.now()'), }